diff --git a/build.zig b/build.zig index b51f4b6..243ec08 100644 --- a/build.zig +++ b/build.zig @@ -277,6 +277,18 @@ pub fn build(b: *std.Build) void { const run_http2_handler_tests = b.addRunArtifact(http2_handler_tests); test_step.dependOn(&run_http2_handler_tests.step); + // HDR Histogram tests + const hdr_histogram_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("tests/unit/hdr_histogram_test.zig"), + .target = target, + .optimize = optimize, + }), + }); + hdr_histogram_tests.root_module.addImport("z6", z6_module); + const run_hdr_histogram_tests = b.addRunArtifact(hdr_histogram_tests); + test_step.dependOn(&run_hdr_histogram_tests.step); + // Integration tests const integration_test_step = b.step("test-integration", "Run integration tests"); diff --git a/src/hdr_histogram.zig b/src/hdr_histogram.zig new file mode 100644 index 0000000..1ff7e0d --- /dev/null +++ b/src/hdr_histogram.zig @@ -0,0 +1,408 @@ +//! HDR Histogram Implementation +//! +//! High Dynamic Range Histogram for latency percentile calculations with +//! bounded memory usage. Based on Gil Tene's HdrHistogram algorithm. +//! +//! Key properties: +//! - Bounded memory: O(log(highest/lowest) * precision) regardless of sample count +//! - O(1) recording: Single index calculation and increment +//! - Configurable precision: 1-5 significant figures +//! - Accurate percentiles: Error bounded by significant figures +//! +//! Tiger Style compliance: +//! - Minimum 2 assertions per public function +//! - All loops have explicit bounds +//! - No unbounded allocations after init + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const math = std.math; + +/// Errors that can occur during histogram operations +pub const HdrError = error{ + /// Significant figures must be 1-5 + InvalidSignificantFigures, + /// Value range is invalid (lowest must be >= 1, highest must be > lowest * 2) + InvalidValueRange, + /// Value exceeds the trackable range + ValueOutOfRange, + /// Memory allocation failed + OutOfMemory, +}; + +/// HDR Histogram for latency percentile calculations +pub const HdrHistogram = struct { + /// Configuration for histogram initialization + pub const Config = struct { + /// Lowest trackable value (default: 1 nanosecond) + lowest_trackable_value: u64 = 1, + /// Highest trackable value (default: 1 hour in nanoseconds) + highest_trackable_value: u64 = 3_600_000_000_000, + /// Number of significant figures for precision (1-5) + significant_figures: u8 = 3, + }; + + // Configuration + lowest_trackable_value: u64, + highest_trackable_value: u64, + significant_figures: u8, + + // Derived constants for index calculation + unit_magnitude: u8, + sub_bucket_half_count_magnitude: u8, + sub_bucket_count: u32, + sub_bucket_half_count: u32, + sub_bucket_mask: u64, + bucket_count: u32, + counts_array_length: u32, + + // Counts array (allocated) + counts: []u64, + + // Running totals + total_count: u64, + min_value: u64, + max_value: u64, + + // Allocator for cleanup + allocator: Allocator, + + /// Initialize a new HDR Histogram + pub fn init(allocator: Allocator, config: Config) HdrError!HdrHistogram { + // Tiger Style: Assert preconditions + assert(config.lowest_trackable_value >= 1 or config.lowest_trackable_value == 0); + assert(config.significant_figures <= 10); + + // Validate significant figures (1-5) + if (config.significant_figures < 1 or config.significant_figures > 5) { + return error.InvalidSignificantFigures; + } + + // Validate value range + if (config.lowest_trackable_value < 1) { + return error.InvalidValueRange; + } + if (config.highest_trackable_value <= config.lowest_trackable_value * 2) { + return error.InvalidValueRange; + } + + // Calculate derived constants + const unit_magnitude = calculateUnitMagnitude(config.lowest_trackable_value); + const sub_bucket_half_count_magnitude = calculateSubBucketHalfCountMagnitude(config.significant_figures); + + const sub_bucket_count: u32 = @as(u32, 1) << @intCast(sub_bucket_half_count_magnitude + 1); + const sub_bucket_half_count: u32 = sub_bucket_count >> 1; + const sub_bucket_mask: u64 = (@as(u64, sub_bucket_count) - 1) << @as(u6, @intCast(unit_magnitude)); + + const bucket_count = calculateBucketCount(config.highest_trackable_value, sub_bucket_count, unit_magnitude); + const counts_array_length = calculateCountsArrayLength(bucket_count, sub_bucket_half_count); + + // Tiger Style: Assert derived values are reasonable + assert(sub_bucket_count >= 2); + assert(bucket_count >= 1); + assert(counts_array_length >= 1); + + // Allocate counts array + const counts = allocator.alloc(u64, counts_array_length) catch { + return error.OutOfMemory; + }; + @memset(counts, 0); + + return HdrHistogram{ + .lowest_trackable_value = config.lowest_trackable_value, + .highest_trackable_value = config.highest_trackable_value, + .significant_figures = config.significant_figures, + .unit_magnitude = unit_magnitude, + .sub_bucket_half_count_magnitude = sub_bucket_half_count_magnitude, + .sub_bucket_count = sub_bucket_count, + .sub_bucket_half_count = sub_bucket_half_count, + .sub_bucket_mask = sub_bucket_mask, + .bucket_count = bucket_count, + .counts_array_length = counts_array_length, + .counts = counts, + .total_count = 0, + .min_value = math.maxInt(u64), + .max_value = 0, + .allocator = allocator, + }; + } + + /// Free histogram memory + pub fn deinit(self: *const HdrHistogram) void { + // Tiger Style: Assert valid state + assert(self.counts.len > 0); + assert(self.counts.len == self.counts_array_length); + + self.allocator.free(self.counts); + } + + /// Reset histogram to empty state + pub fn reset(self: *HdrHistogram) void { + // Tiger Style: Assert valid state + assert(self.counts.len == self.counts_array_length); + assert(self.counts_array_length > 0); + + @memset(self.counts, 0); + self.total_count = 0; + self.min_value = math.maxInt(u64); + self.max_value = 0; + } + + /// Record a single value + pub fn recordValue(self: *HdrHistogram, value: u64) HdrError!void { + // Tiger Style: Assert valid state + assert(self.counts.len == self.counts_array_length); + assert(self.counts_array_length > 0); + + if (value > self.highest_trackable_value) { + return error.ValueOutOfRange; + } + + const index = self.getCountsIndexForValue(value); + if (index >= self.counts_array_length) { + return error.ValueOutOfRange; + } + + self.counts[index] += 1; + self.total_count += 1; + + if (value < self.min_value) { + self.min_value = value; + } + if (value > self.max_value) { + self.max_value = value; + } + } + + /// Record a value multiple times + pub fn recordValues(self: *HdrHistogram, value: u64, count: u64) HdrError!void { + // Tiger Style: Assert valid state + assert(self.counts.len == self.counts_array_length); + assert(count <= math.maxInt(u64) - self.total_count); + + if (count == 0) { + return; + } + + if (value > self.highest_trackable_value) { + return error.ValueOutOfRange; + } + + const index = self.getCountsIndexForValue(value); + if (index >= self.counts_array_length) { + return error.ValueOutOfRange; + } + + self.counts[index] += count; + self.total_count += count; + + if (value < self.min_value) { + self.min_value = value; + } + if (value > self.max_value) { + self.max_value = value; + } + } + + /// Get minimum recorded value + pub fn min(self: *const HdrHistogram) u64 { + // Tiger Style: Assert valid state + assert(self.counts.len > 0); + assert(self.min_value == math.maxInt(u64) or self.min_value <= self.highest_trackable_value); + + return self.min_value; + } + + /// Get maximum recorded value + pub fn max(self: *const HdrHistogram) u64 { + // Tiger Style: Assert valid state + assert(self.counts.len > 0); + assert(self.max_value <= self.highest_trackable_value or self.max_value == 0); + + return self.max_value; + } + + /// Calculate mean of recorded values + pub fn mean(self: *const HdrHistogram) f64 { + // Tiger Style: Assert valid state + assert(self.counts.len == self.counts_array_length); + assert(self.counts_array_length > 0); + + if (self.total_count == 0) { + return 0.0; + } + + var total: u128 = 0; + var index: u32 = 0; + + // Bounded loop: iterate through counts array + while (index < self.counts_array_length) : (index += 1) { + const count = self.counts[index]; + if (count > 0) { + const value = self.valueFromIndex(index); + total += @as(u128, count) * @as(u128, value); + } + } + + return @as(f64, @floatFromInt(total)) / @as(f64, @floatFromInt(self.total_count)); + } + + /// Get total number of recorded values + pub fn totalCount(self: *const HdrHistogram) u64 { + // Tiger Style: Assert valid state + assert(self.counts.len > 0); + assert(self.total_count <= math.maxInt(u64)); + + return self.total_count; + } + + /// Get value at a given percentile (0-100) + pub fn valueAtPercentile(self: *const HdrHistogram, percentile: f64) u64 { + // Tiger Style: Assert valid state and percentile range + assert(self.counts.len == self.counts_array_length); + assert(percentile >= 0.0 and percentile <= 100.0); + + if (self.total_count == 0) { + return 0; + } + + // Calculate target count for percentile + const requested_percentile = @min(percentile, 100.0); + const count_at_percentile = @as(u64, @intFromFloat( + (requested_percentile / 100.0) * @as(f64, @floatFromInt(self.total_count)) + 0.5, + )); + const target_count = @max(count_at_percentile, 1); + + var cumulative_count: u64 = 0; + var index: u32 = 0; + + // Bounded loop: find the value at target count + while (index < self.counts_array_length) : (index += 1) { + cumulative_count += self.counts[index]; + if (cumulative_count >= target_count) { + return self.valueFromIndex(index); + } + } + + // Should not reach here if total_count > 0 + return self.max_value; + } + + // ========================================================================= + // Private helper functions + // ========================================================================= + + /// Calculate counts array index for a value + fn getCountsIndexForValue(self: *const HdrHistogram, value: u64) u32 { + const bucket_index = self.getBucketIndex(value); + const sub_bucket_index = self.getSubBucketIndex(value, bucket_index); + return self.countsArrayIndex(bucket_index, sub_bucket_index); + } + + /// Get bucket index for a value + fn getBucketIndex(self: *const HdrHistogram, value: u64) u32 { + // Use leading zeros to find bucket + const shift: u6 = @intCast(self.unit_magnitude); + const pow2_ceiling = 64 - @as(u32, @clz(value | self.sub_bucket_mask)); + const bucket_index_offset: i32 = @as(i32, @intCast(pow2_ceiling)) - @as(i32, shift) - + @as(i32, @intCast(self.sub_bucket_half_count_magnitude + 1)); + return @intCast(@max(bucket_index_offset, 0)); + } + + /// Get sub-bucket index for a value + fn getSubBucketIndex(self: *const HdrHistogram, value: u64, bucket_index: u32) u32 { + const shift_amount: u6 = @intCast(bucket_index + self.unit_magnitude); + return @intCast(value >> shift_amount); + } + + /// Calculate counts array index from bucket and sub-bucket + fn countsArrayIndex(self: *const HdrHistogram, bucket_index: u32, sub_bucket_index: u32) u32 { + assert(sub_bucket_index < self.sub_bucket_count); + + // Handle the special case where bucket 0's first half overlaps with negative bucket indices + if (sub_bucket_index < self.sub_bucket_half_count) { + // Values in the first half of bucket 0 + return sub_bucket_index; + } + + // Standard formula for all other cases + const shift: u5 = @intCast(self.sub_bucket_half_count_magnitude); + const bucket_base_index = (bucket_index + 1) << shift; + const offset_in_bucket = sub_bucket_index - self.sub_bucket_half_count; + return bucket_base_index + offset_in_bucket; + } + + /// Convert counts array index back to a value + fn valueFromIndex(self: *const HdrHistogram, index: u32) u64 { + const shift: u5 = @intCast(self.sub_bucket_half_count_magnitude); + const bucket_index: u32 = (index >> shift) -| 1; + const sub_bucket_index: u32 = if (bucket_index == 0 and index < self.sub_bucket_half_count) + index + else + (index & (self.sub_bucket_half_count - 1)) + self.sub_bucket_half_count; + + const value_shift: u6 = @intCast(bucket_index + self.unit_magnitude); + return @as(u64, sub_bucket_index) << value_shift; + } +}; + +// ============================================================================= +// Module-level helper functions +// ============================================================================= + +/// Calculate unit magnitude from lowest trackable value +fn calculateUnitMagnitude(lowest_trackable_value: u64) u8 { + // Unit magnitude is the number of trailing zeros (floor of log2) + const leading_zeros = @clz(lowest_trackable_value); + const magnitude = 63 - leading_zeros; + return @intCast(magnitude); +} + +/// Calculate sub-bucket half count magnitude from significant figures +fn calculateSubBucketHalfCountMagnitude(significant_figures: u8) u8 { + // For N significant figures, we need 10^N sub-buckets per bucket half + // Use log2(10^N) = N * log2(10) ≈ N * 3.32 + const largest_significant = math.pow(f64, 10.0, @floatFromInt(significant_figures)); + const sub_bucket_magnitude = @as(u8, @intFromFloat(@ceil(math.log2(largest_significant)))); + return sub_bucket_magnitude; +} + +/// Calculate number of buckets needed for value range +fn calculateBucketCount(highest_trackable_value: u64, sub_bucket_count: u32, unit_magnitude: u8) u32 { + // Smallest value that needs second bucket + const shift: u6 = @intCast(unit_magnitude); + const smallest_untrackable: u64 = @as(u64, sub_bucket_count) << shift; + + var buckets_needed: u32 = 1; + var value = smallest_untrackable; + + // Bounded loop: max 64 iterations (log2 of u64 max) + while (value <= highest_trackable_value and buckets_needed < 64) { + buckets_needed += 1; + value <<= 1; + } + + return buckets_needed; +} + +/// Calculate total counts array length +fn calculateCountsArrayLength(bucket_count: u32, sub_bucket_half_count: u32) u32 { + return (bucket_count + 1) * sub_bucket_half_count; +} + +// ============================================================================= +// Tests +// ============================================================================= + +test "hdr_histogram: unit magnitude calculation" { + try std.testing.expectEqual(@as(u8, 0), calculateUnitMagnitude(1)); + try std.testing.expectEqual(@as(u8, 10), calculateUnitMagnitude(1024)); + try std.testing.expectEqual(@as(u8, 9), calculateUnitMagnitude(1000)); +} + +test "hdr_histogram: sub bucket magnitude calculation" { + try std.testing.expectEqual(@as(u8, 4), calculateSubBucketHalfCountMagnitude(1)); // 10^1 = 10, log2(10) ≈ 3.32 -> 4 + try std.testing.expectEqual(@as(u8, 7), calculateSubBucketHalfCountMagnitude(2)); // 10^2 = 100, log2(100) ≈ 6.64 -> 7 + try std.testing.expectEqual(@as(u8, 10), calculateSubBucketHalfCountMagnitude(3)); // 10^3 = 1000, log2(1000) ≈ 9.97 -> 10 +} diff --git a/src/z6.zig b/src/z6.zig index f02eb9c..6046c98 100644 --- a/src/z6.zig +++ b/src/z6.zig @@ -122,3 +122,7 @@ pub const EngineError = @import("vu_engine.zig").EngineError; pub const VUContext = @import("vu_engine.zig").VUContext; pub const VU_ENGINE_MAX_VUS = @import("vu_engine.zig").MAX_VUS; pub const DEFAULT_THINK_TIME_TICKS = @import("vu_engine.zig").DEFAULT_THINK_TIME_TICKS; + +// HDR Histogram +pub const HdrHistogram = @import("hdr_histogram.zig").HdrHistogram; +pub const HdrError = @import("hdr_histogram.zig").HdrError; diff --git a/tests/unit/hdr_histogram_test.zig b/tests/unit/hdr_histogram_test.zig new file mode 100644 index 0000000..6901533 --- /dev/null +++ b/tests/unit/hdr_histogram_test.zig @@ -0,0 +1,458 @@ +//! HDR Histogram Tests +//! +//! Test-Driven Development: These tests are written BEFORE implementation. +//! Following Tiger Style: Test before implement. +//! +//! HDR Histogram provides high dynamic range histograms with bounded memory +//! and configurable precision for latency percentile calculations. + +const std = @import("std"); +const testing = std.testing; +const z6 = @import("z6"); +const HdrHistogram = z6.HdrHistogram; + +// ============================================================================= +// Phase 1: Init/Deinit Tests +// ============================================================================= + +test "hdr_histogram: init with default config" { + const histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Default config: 1ns to 1 hour, 3 significant figures + try testing.expectEqual(@as(u64, 1), histogram.lowest_trackable_value); + try testing.expectEqual(@as(u64, 3_600_000_000_000), histogram.highest_trackable_value); + try testing.expectEqual(@as(u8, 3), histogram.significant_figures); + + // Should start empty + try testing.expectEqual(@as(u64, 0), histogram.totalCount()); +} + +test "hdr_histogram: init with custom config" { + const histogram = try HdrHistogram.init(testing.allocator, .{ + .lowest_trackable_value = 1000, // 1 microsecond in ns + .highest_trackable_value = 60_000_000_000, // 1 minute in ns + .significant_figures = 2, + }); + defer histogram.deinit(); + + try testing.expectEqual(@as(u64, 1000), histogram.lowest_trackable_value); + try testing.expectEqual(@as(u64, 60_000_000_000), histogram.highest_trackable_value); + try testing.expectEqual(@as(u8, 2), histogram.significant_figures); +} + +test "hdr_histogram: init validates significant figures" { + // Significant figures must be 1-5 + try testing.expectError(error.InvalidSignificantFigures, HdrHistogram.init(testing.allocator, .{ + .significant_figures = 0, + })); + try testing.expectError(error.InvalidSignificantFigures, HdrHistogram.init(testing.allocator, .{ + .significant_figures = 6, + })); + + // Valid range + const h1 = try HdrHistogram.init(testing.allocator, .{ .significant_figures = 1 }); + defer h1.deinit(); + const h5 = try HdrHistogram.init(testing.allocator, .{ .significant_figures = 5 }); + defer h5.deinit(); +} + +test "hdr_histogram: init validates value range" { + // Lowest must be >= 1 + try testing.expectError(error.InvalidValueRange, HdrHistogram.init(testing.allocator, .{ + .lowest_trackable_value = 0, + })); + + // Highest must be > lowest * 2 + try testing.expectError(error.InvalidValueRange, HdrHistogram.init(testing.allocator, .{ + .lowest_trackable_value = 100, + .highest_trackable_value = 100, + })); +} + +test "hdr_histogram: reset clears all data" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Record some values + try histogram.recordValue(1000); + try histogram.recordValue(2000); + try histogram.recordValue(3000); + try testing.expectEqual(@as(u64, 3), histogram.totalCount()); + + // Reset + histogram.reset(); + try testing.expectEqual(@as(u64, 0), histogram.totalCount()); + try testing.expectEqual(@as(u64, 0), histogram.max()); + try testing.expectEqual(@as(u64, std.math.maxInt(u64)), histogram.min()); +} + +// ============================================================================= +// Phase 2: Value Recording Tests +// ============================================================================= + +test "hdr_histogram: record single value" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(1000); + try testing.expectEqual(@as(u64, 1), histogram.totalCount()); + try testing.expectEqual(@as(u64, 1000), histogram.min()); + try testing.expectEqual(@as(u64, 1000), histogram.max()); +} + +test "hdr_histogram: record multiple values" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(100); + try histogram.recordValue(200); + try histogram.recordValue(300); + + try testing.expectEqual(@as(u64, 3), histogram.totalCount()); + try testing.expectEqual(@as(u64, 100), histogram.min()); + try testing.expectEqual(@as(u64, 300), histogram.max()); +} + +test "hdr_histogram: record values with count" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValues(1000, 5); + try testing.expectEqual(@as(u64, 5), histogram.totalCount()); + try testing.expectEqual(@as(u64, 1000), histogram.min()); + try testing.expectEqual(@as(u64, 1000), histogram.max()); +} + +test "hdr_histogram: record value at lowest trackable" { + var histogram = try HdrHistogram.init(testing.allocator, .{ + .lowest_trackable_value = 1, + }); + defer histogram.deinit(); + + try histogram.recordValue(1); + try testing.expectEqual(@as(u64, 1), histogram.totalCount()); + try testing.expectEqual(@as(u64, 1), histogram.min()); +} + +test "hdr_histogram: record value at highest trackable" { + var histogram = try HdrHistogram.init(testing.allocator, .{ + .highest_trackable_value = 1_000_000, + }); + defer histogram.deinit(); + + try histogram.recordValue(1_000_000); + try testing.expectEqual(@as(u64, 1), histogram.totalCount()); + try testing.expectEqual(@as(u64, 1_000_000), histogram.max()); +} + +test "hdr_histogram: record value exceeding range returns error" { + var histogram = try HdrHistogram.init(testing.allocator, .{ + .highest_trackable_value = 1_000_000, + }); + defer histogram.deinit(); + + // Value exceeds highest trackable + try testing.expectError(error.ValueOutOfRange, histogram.recordValue(2_000_000)); + + // Original state should be unchanged + try testing.expectEqual(@as(u64, 0), histogram.totalCount()); +} + +// ============================================================================= +// Phase 3: Query Tests +// ============================================================================= + +test "hdr_histogram: min returns minimum recorded value" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(500); + try histogram.recordValue(100); + try histogram.recordValue(300); + + try testing.expectEqual(@as(u64, 100), histogram.min()); +} + +test "hdr_histogram: max returns maximum recorded value" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(100); + try histogram.recordValue(500); + try histogram.recordValue(300); + + try testing.expectEqual(@as(u64, 500), histogram.max()); +} + +test "hdr_histogram: mean calculation" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Record values: 100, 200, 300, 400, 500 + // Mean = (100+200+300+400+500) / 5 = 300 + try histogram.recordValue(100); + try histogram.recordValue(200); + try histogram.recordValue(300); + try histogram.recordValue(400); + try histogram.recordValue(500); + + // Mean should be approximately 300 (within histogram resolution) + const mean = histogram.mean(); + try testing.expect(mean >= 290.0 and mean <= 310.0); +} + +test "hdr_histogram: mean on empty histogram" { + const histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Empty histogram should return 0 mean + try testing.expectEqual(@as(f64, 0.0), histogram.mean()); +} + +test "hdr_histogram: totalCount accumulates correctly" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(100); + try testing.expectEqual(@as(u64, 1), histogram.totalCount()); + + try histogram.recordValues(200, 10); + try testing.expectEqual(@as(u64, 11), histogram.totalCount()); + + try histogram.recordValue(300); + try testing.expectEqual(@as(u64, 12), histogram.totalCount()); +} + +// ============================================================================= +// Phase 4: Percentile Tests +// ============================================================================= + +test "hdr_histogram: p0 returns minimum" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(100); + try histogram.recordValue(200); + try histogram.recordValue(300); + + const p0 = histogram.valueAtPercentile(0.0); + try testing.expectEqual(@as(u64, 100), p0); +} + +test "hdr_histogram: p100 returns maximum" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(100); + try histogram.recordValue(200); + try histogram.recordValue(300); + + const p100 = histogram.valueAtPercentile(100.0); + try testing.expectEqual(@as(u64, 300), p100); +} + +test "hdr_histogram: p50 on uniform distribution" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Record values 1-100 + var i: u64 = 1; + while (i <= 100) : (i += 1) { + try histogram.recordValue(i); + } + + // p50 should be around 50 + const p50 = histogram.valueAtPercentile(50.0); + try testing.expect(p50 >= 49 and p50 <= 51); +} + +test "hdr_histogram: p99 on uniform distribution" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Record values 1-100 + var i: u64 = 1; + while (i <= 100) : (i += 1) { + try histogram.recordValue(i); + } + + // p99 should be around 99 + const p99 = histogram.valueAtPercentile(99.0); + try testing.expect(p99 >= 98 and p99 <= 100); +} + +test "hdr_histogram: p999 on large distribution" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Record values 1-10000 + var i: u64 = 1; + while (i <= 10000) : (i += 1) { + try histogram.recordValue(i); + } + + // p99.9 should be around 9990 + const p999 = histogram.valueAtPercentile(99.9); + try testing.expect(p999 >= 9980 and p999 <= 10000); +} + +test "hdr_histogram: percentile on single value" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + try histogram.recordValue(12345); + + // All percentiles should return the same value (within quantization precision) + const p0 = histogram.valueAtPercentile(0.0); + const p50 = histogram.valueAtPercentile(50.0); + const p100 = histogram.valueAtPercentile(100.0); + + // All percentiles should be equal (single value) + try testing.expectEqual(p0, p50); + try testing.expectEqual(p50, p100); + + // Value should be within 0.1% of original (3 significant figures) + const expected: u64 = 12345; + const error_ratio = @as(f64, @floatFromInt(if (p0 > expected) p0 - expected else expected - p0)) / @as(f64, @floatFromInt(expected)); + try testing.expect(error_ratio < 0.001); +} + +test "hdr_histogram: percentile on empty histogram" { + const histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Empty histogram should return 0 + try testing.expectEqual(@as(u64, 0), histogram.valueAtPercentile(50.0)); +} + +// ============================================================================= +// Memory Bounds Tests +// ============================================================================= + +test "hdr_histogram: bounded memory for default config" { + const histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Default config (1ns to 1hr, 3 sig figs) should use bounded memory + // counts_array_length should be calculable and bounded + try testing.expect(histogram.counts_array_length > 0); + try testing.expect(histogram.counts_array_length <= 100_000); // Reasonable upper bound +} + +test "hdr_histogram: memory does not grow with record count" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + const initial_length = histogram.counts_array_length; + + // Record many values + var i: u64 = 0; + while (i < 100_000) : (i += 1) { + try histogram.recordValue((i % 1_000_000) + 1); + } + + // Array length should not change + try testing.expectEqual(initial_length, histogram.counts_array_length); +} + +// ============================================================================= +// Accuracy Tests +// ============================================================================= + +test "hdr_histogram: accuracy within significant figures" { + var histogram = try HdrHistogram.init(testing.allocator, .{ + .significant_figures = 3, + }); + defer histogram.deinit(); + + // Record a specific value + const test_value: u64 = 123_456_789; + try histogram.recordValue(test_value); + + // The recorded value should be within 0.1% (3 sig figs) of original + const recorded_max = histogram.max(); + const error_ratio = @as(f64, @floatFromInt(if (recorded_max > test_value) recorded_max - test_value else test_value - recorded_max)) / @as(f64, @floatFromInt(test_value)); + try testing.expect(error_ratio < 0.001); // 0.1% error +} + +test "hdr_histogram: percentile accuracy on bimodal distribution" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // 90% fast responses (1ms), 10% slow responses (100ms) + const fast_count: u64 = 9000; + const slow_count: u64 = 1000; + + try histogram.recordValues(1_000_000, fast_count); // 1ms in ns + try histogram.recordValues(100_000_000, slow_count); // 100ms in ns + + // p50 should be fast (around 1ms) + const p50 = histogram.valueAtPercentile(50.0); + try testing.expect(p50 <= 2_000_000); // Should be close to 1ms + + // p95 should still be fast + const p95 = histogram.valueAtPercentile(95.0); + try testing.expect(p95 <= 100_000_000); // Should be at or below 100ms + + // p99 should be slow (around 100ms) + const p99 = histogram.valueAtPercentile(99.0); + try testing.expect(p99 >= 50_000_000); // Should be close to 100ms +} + +// ============================================================================= +// Edge Cases +// ============================================================================= + +test "hdr_histogram: handles zero count for recordValues" { + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // Recording zero count should be a no-op + try histogram.recordValues(1000, 0); + try testing.expectEqual(@as(u64, 0), histogram.totalCount()); +} + +test "hdr_histogram: handles value exactly at unit magnitude boundary" { + var histogram = try HdrHistogram.init(testing.allocator, .{ + .lowest_trackable_value = 1, + .highest_trackable_value = 1_000_000_000, + .significant_figures = 3, + }); + defer histogram.deinit(); + + // Record values at power of 2 boundaries + try histogram.recordValue(1); + try histogram.recordValue(2); + try histogram.recordValue(4); + try histogram.recordValue(1024); + try histogram.recordValue(65536); + + try testing.expectEqual(@as(u64, 5), histogram.totalCount()); +} + +// ============================================================================= +// Tiger Style Tests +// ============================================================================= + +test "hdr_histogram: Tiger Style - all public functions have assertions" { + // This test documents that the implementation should include + // at least 2 assertions per public function as per Tiger Style + var histogram = try HdrHistogram.init(testing.allocator, .{}); + defer histogram.deinit(); + + // The implementation should assert: + // - init: significant_figures in range, value range valid + // - recordValue: value in range + // - valueAtPercentile: percentile 0-100 + + // These operations should work + try histogram.recordValue(100); + _ = histogram.valueAtPercentile(50.0); + _ = histogram.min(); + _ = histogram.max(); + _ = histogram.mean(); + _ = histogram.totalCount(); + + try testing.expect(histogram.totalCount() > 0); +}