Skip to content

Commit a69fa24

Browse files
hassiladimlio
andauthored
fix(patch): Clamp percentiles to actually recorded min/max instead of rounding to equivalent levels (#14)
Clamp percentile result to min/max actual recorded range as we have perfect fidelity there, unexpected p0/p100 otherwise. Co-authored-by: dimlio <[email protected]>
1 parent e7107ca commit a69fa24

File tree

5 files changed

+52
-6
lines changed

5 files changed

+52
-6
lines changed

Sources/Histogram/Histogram.swift

+28-5
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ public struct Histogram<Count: FixedWidthInteger & Codable>: Codable {
326326
* in the histogram are either larger than or equivalent to. Returns 0 if no recorded values exist.
327327
*/
328328
public func valueAtPercentile(_ percentile: Double) -> UInt64 {
329+
guard percentile != 0.0 else {
330+
return self.min
331+
}
332+
333+
guard percentile < 100.0 else {
334+
return maxRecorded
335+
}
336+
329337
// Truncate to 0..100%, and remove 1 ulp to avoid roundoff overruns into next bucket when we
330338
// subsequently round up to the nearest integer:
331339
let requestedPercentile = Swift.min(Swift.max(percentile.nextDown, 0.0), 100.0)
@@ -342,9 +350,8 @@ public struct Histogram<Count: FixedWidthInteger & Codable>: Codable {
342350
totalToCurrentIndex += UInt64(counts[i])
343351
if totalToCurrentIndex >= countAtPercentile {
344352
let valueAtIndex = valueFromIndex(i)
345-
return (percentile == 0.0) ?
346-
lowestEquivalentForValue(valueAtIndex) :
347-
highestEquivalentForValue(valueAtIndex)
353+
let returnValue = highestEquivalentForValue(valueAtIndex)
354+
return Swift.max(Swift.min(returnValue, maxRecorded), self.min)
348355
}
349356
}
350357
return 0
@@ -435,7 +442,7 @@ public struct Histogram<Count: FixedWidthInteger & Codable>: Codable {
435442
}
436443

437444
/**
438-
* Get the lowest recorded value level in the histogram.
445+
* Get the lowest recorded value in the histogram.
439446
*
440447
* If the histogram has no recorded values, the value returned is undefined.
441448
*
@@ -446,12 +453,25 @@ public struct Histogram<Count: FixedWidthInteger & Codable>: Codable {
446453
}
447454

448455
/**
449-
* Get the highest recorded value level in the histogram.
456+
* Get the highest recorded value in the histogram.
450457
*
451458
* If the histogram has no recorded values, the value returned is undefined.
452459
*
453460
* - Returns: The maximum value recorded in the histogram.
454461
*/
462+
public var maxRecorded: UInt64 {
463+
maxValue
464+
}
465+
466+
/**
467+
* Get the highest recorded value level in the histogram.
468+
*
469+
* If the histogram has no recorded values, the value returned is undefined.
470+
*
471+
* NB this can be larger than the maximum recorded value due to precision.
472+
*
473+
* - Returns: The maximum value level recorded in the histogram.
474+
*/
455475
public var max: UInt64 {
456476
maxValue == 0 ? 0 : highestEquivalentForValue(maxValue)
457477
}
@@ -460,6 +480,7 @@ public struct Histogram<Count: FixedWidthInteger & Codable>: Codable {
460480
* Get the lowest recorded non-zero value level in the histogram.
461481
*
462482
* If the histogram has no recorded values, the value returned is undefined.
483+
* NB this can be smaller than the minimum recorded value due to precision.
463484
*
464485
* - Returns: The lowest recorded non-zero value level in the histogram.
465486
*/
@@ -1347,3 +1368,5 @@ extension Histogram: TextOutputStreamable {
13471368
outputPercentileDistribution(to: &to, outputValueUnitScalingRatio: 1.0)
13481369
}
13491370
}
1371+
1372+
// swiftlint:enable file_length type_body_length line_length identifier_name

Sources/HistogramExample/HistogramExample.swift

+2
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@ enum HistogramExample {
5353
print("\n", histogram)
5454
}
5555
}
56+
57+
// swiftlint:enable identifier_name

Tests/HistogramTests/HistogramAutosizingTests.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// http://www.apache.org/licenses/LICENSE-2.0
99
//
1010

11-
// swiftlint:disable identifier_name todo
11+
// swiftlint:disable todo
1212

1313
@testable import Histogram
1414
import XCTest
@@ -86,3 +86,5 @@ final class HistogramAutosizingTests: XCTestCase {
8686
}
8787
}
8888
}
89+
90+
// swiftlint:enable todo

Tests/HistogramTests/HistogramDataAccessTests.swift

+2
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,5 @@ final class HistogramDataAccessTests: XCTestCase {
491491
XCTAssertEqual(4_104, snapshots.count)
492492
}
493493
}
494+
495+
// swiftlint:enable file_length identifier_name line_length trailing_comma

Tests/HistogramTests/HistogramTests.swift

+17
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ final class HistogramTests: XCTestCase {
2020
private static let numberOfSignificantValueDigits = SignificantDigits.three
2121
private static let value: UInt64 = 4
2222

23+
// This is a check for valueAtPercentiles that would return 999936
24+
// instead of 1000_001 for p0 (which would return lowestEquivalentForValue instead of min)
25+
// and it would return 1000447 for p100, instead of max
26+
func testPercentilesMinMax() throws {
27+
var histogram = Histogram<UInt64>(lowestDiscernibleValue: 1, highestTrackableValue: 3_600_000_000, numberOfSignificantValueDigits: .three)
28+
for _ in 0 ..< 90 {
29+
histogram.record(1_000_001)
30+
}
31+
XCTAssertEqual(histogram.minNonZeroValue, 1_000_001)
32+
XCTAssertEqual(histogram.min, 1_000_001)
33+
XCTAssertEqual(histogram.min, histogram.valueAtPercentile(0.0))
34+
XCTAssertEqual(histogram.maxRecorded, 1_000_001)
35+
XCTAssertEqual(histogram.maxRecorded, histogram.valueAtPercentile(100.0))
36+
}
37+
2338
func testCreate() throws {
2439
let h = Histogram<UInt64>(lowestDiscernibleValue: 1, highestTrackableValue: 3_600_000_000, numberOfSignificantValueDigits: .three)
2540
XCTAssertEqual(h.counts.count, 23_552)
@@ -504,3 +519,5 @@ final class HistogramTests: XCTestCase {
504519
XCTAssertEqual(computedMaxValue, h.maxValue)
505520
}
506521
}
522+
523+
// swiftlint:enable file_length identifier_name line_length

0 commit comments

Comments
 (0)