Skip to content

Commit 2f77b31

Browse files
authored
Analysis of the imprecision of MapToIndex (#4509)
1 parent 6259af0 commit 2f77b31

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,107 @@ public void ZeroHandling()
466466

467467
Assert.Equal(2, histogram.ZeroCount);
468468
}
469+
470+
[Fact]
471+
public void ScaleOneIndexesWithPowerOfTwoLowerBound()
472+
{
473+
/*
474+
The range of indexes tested is fixed to evaluate values recorded
475+
within [2^-25, 2^25] or approximately [0.00000002980232239, 33554432].
476+
477+
For perspective, assume the unit of values recorded is seconds then this
478+
test represents the imprecision of MapToIndex for values recorded between
479+
approximately 29.80 nanoseconds and 1.06 years.
480+
481+
The output of this test is as follows:
482+
483+
Scale: 1
484+
Indexes per power of 2: 2
485+
Index range: [-50, 50]
486+
Value range: [2.9802322387695312E-08, 33554432]
487+
Successes: 18
488+
Failures: 33
489+
Average number of values near a bucket boundary that are off by one: 2.878787878787879
490+
Average range of values near a bucket boundary that are off by one: 3.0880480139955346E-09
491+
492+
That is, there are ~2.89 values near each bucket boundary tested that
493+
are mapped to an index off by one. The range of these incorrectly mapped
494+
values, assuming seconds as the unit, is ~3.09 nanoseconds.
495+
*/
496+
497+
// This test only tests scale 1, but it can be adjusted to test any
498+
// positive scale by changing the scale of the histogram. The output
499+
// and results are identical for all positive scales.
500+
var scale = 1;
501+
var histogram = new Base2ExponentialBucketHistogram(scale: scale);
502+
503+
// These are used to capture stats for an analysis for where MapToIndex is off by one.
504+
var successes = 0;
505+
var failures = 0;
506+
var diffs = new List<double>();
507+
var numValuesOffByOne = new List<int>();
508+
509+
// Only indexes with a lower bound that is an exact power of two are tested.
510+
var indexesPerPowerOf2 = 1 << scale;
511+
var exp = -25;
512+
513+
var index = exp * indexesPerPowerOf2;
514+
var endIndex = Math.Abs(index);
515+
var lowerBound = Math.Pow(2, exp);
516+
517+
this.output.WriteLine(string.Empty);
518+
this.output.WriteLine($"Scale: {scale}");
519+
this.output.WriteLine($"Indexes per power of 2: {indexesPerPowerOf2}");
520+
this.output.WriteLine($"Index range: [{index}, {endIndex}]");
521+
this.output.WriteLine($"Value range: [{lowerBound}, {Math.Pow(2, Math.Abs(exp))}]");
522+
523+
for (; index <= endIndex; index += indexesPerPowerOf2, lowerBound = Math.Pow(2, ++exp))
524+
{
525+
// Buckets are lower bound exclusive, therefore
526+
// MapToIndex(LowerBoundOfBucketN) = IndexOfBucketN - 1.
527+
Assert.Equal(index - 1, histogram.MapToIndex(lowerBound));
528+
529+
// If MapToIndex was mathematically precise, the following assertion would pass.
530+
// BitIncrement(lowerBound) increments lowerBound by the smallest increment possible.
531+
// MapToIndex(BitIncrement(LowerBoundOfBucketN)) should equal IndexOfBucketN.
532+
// However, because MapToIndex at positive scales is imprecise, the assertion can fail
533+
// for values very close to a bucket boundary.
534+
535+
// Assert.Equal(index, histogram.MapToIndex(BitIncrement(lowerBound)));
536+
537+
// Knowing that MapToIndex is imprecise near bucket boundaries,
538+
// the following produces an analysis of the magnitude of imprecision.
539+
540+
var incremented = BitIncrement(lowerBound);
541+
542+
if (index == histogram.MapToIndex(incremented))
543+
{
544+
// This is a scenario where the assertion above would have passed.
545+
++successes;
546+
}
547+
else
548+
{
549+
// This is a scenario where the assertion above would have failed.
550+
++failures;
551+
552+
// Count the number of values near the bucket boundary
553+
// for which MapToIndex produces a result that is off by one.
554+
var increments = 1;
555+
while (index != histogram.MapToIndex(incremented))
556+
{
557+
incremented = BitIncrement(incremented);
558+
increments++;
559+
}
560+
561+
// Capture stats for this bucket index.
562+
numValuesOffByOne.Add(increments - 1);
563+
diffs.Add(incremented - lowerBound);
564+
}
565+
}
566+
567+
this.output.WriteLine($"Successes: {successes}");
568+
this.output.WriteLine($"Failures: {failures}");
569+
this.output.WriteLine($"Average number of values near a bucket boundary that are off by one: {numValuesOffByOne.Average()}");
570+
this.output.WriteLine($"Average range of values near a bucket boundary that are off by one: {diffs.Average()}");
571+
}
469572
}

0 commit comments

Comments
 (0)