From 7d5cd8a3f10bf694bf702cfa63810f651bfb1a96 Mon Sep 17 00:00:00 2001 From: Erik Dunteman Date: Wed, 3 Jul 2024 22:04:05 -0700 Subject: [PATCH] should... be done --- src/bun.js/bindings/JSMapObject.cpp | 36 ++ src/bun.js/node/hdr_histogram.zig | 287 +++++++++++++++ src/bun.js/node/node.classes.ts | 32 +- src/bun.js/node/node_perf_hooks_histogram.zig | 246 ++++++++++++- .../node_perf_hooks_histogram_binding.zig | 344 ++++++++---------- src/erik.js | 27 +- 6 files changed, 751 insertions(+), 221 deletions(-) create mode 100644 src/bun.js/bindings/JSMapObject.cpp create mode 100644 src/bun.js/node/hdr_histogram.zig diff --git a/src/bun.js/bindings/JSMapObject.cpp b/src/bun.js/bindings/JSMapObject.cpp new file mode 100644 index 0000000000..29edfc7acd --- /dev/null +++ b/src/bun.js/bindings/JSMapObject.cpp @@ -0,0 +1,36 @@ +#include "root.h" + +#include "blob.h" +#include "headers-handwritten.h" + +#include "JavaScriptCore/JSCJSValue.h" +#include "JavaScriptCore/JSCast.h" + +#include +#include +#include "JavaScriptCore/JSMapInlines.h" +#include + +#include "ZigGlobalObject.h" + +extern "C" JSC::EncodedJSValue Bun__createMapFromDoubleUint64TupleArray(Zig::GlobalObject* globalObject, const double* doubles, size_t length) +{ + // JS:Map map = JSMap::create(globalObject->vm()); + + // create map + JSC::JSMap* map + = JSC::JSMap::create(globalObject->vm(), globalObject->mapStructure()); + + for (size_t i = 0; i < length; i += 2) { + // we passed doubles in from Zig, with this doubles.appendSlice(&.{ percentile, @bitCast(val) }); + // where percentile is a f64 and val is a u64 + + uint64_t value_as_u64; + std::memcpy(&value_as_u64, &doubles[i + 1], sizeof(double)); // cast double to u64 + + map->set(globalObject, JSC::jsDoubleNumber(doubles[i]), JSC::jsNumber(value_as_u64)); + } + + // do stuff, create good map + return JSC::JSValue::encode(map); +} diff --git a/src/bun.js/node/hdr_histogram.zig b/src/bun.js/node/hdr_histogram.zig new file mode 100644 index 0000000000..00c463a54f --- /dev/null +++ b/src/bun.js/node/hdr_histogram.zig @@ -0,0 +1,287 @@ +const std = @import("std"); + +pub const HistogramOptions = struct { + lowest_trackable_value: u64 = 1, + highest_trackable_value: u64 = 9007199254740991, // Number.MAX_SAFE_INTEGER + significant_figures: u8 = 3, +}; + +// Zig port of High Dynamic Range (HDR) Histogram algorithm +// Only supports recording values for now +pub const Histogram = struct { + allocator: std.mem.Allocator, + lowest_trackable_value: u64, + highest_trackable_value: u64, + significant_figures: u64, + sub_bucket_count: u64, + sub_bucket_half_count: u64, + sub_bucket_half_count_magnitude: u6, + unit_magnitude: u8, + sub_bucket_mask: u64, + bucket_count: u64, + counts: []u64, + total_count: u64, + min_value: u64, + max_value: u64, + + pub fn deinit(self: *Histogram) void { + self.allocator.free(self.counts); + } + + pub fn init(allocator: std.mem.Allocator, options: HistogramOptions) !Histogram { + // dummy input: lowest=1, highest=1000, sigfig=3 + + // Validate input + if (options.significant_figures < 1 or options.significant_figures > 5) { + return error.InvalidSignificantFigures; + } + + if (options.lowest_trackable_value < 1) { + return error.InvalidLowestTrackableValue; + } + + // Calculate derived values for efficient bucketing + + // upper bound of each bucket + const largest_value_in_bucket = 2 * std.math.pow(u64, 10, options.significant_figures); + const log2largest_value = std.math.log2(@as(f64, @floatFromInt(largest_value_in_bucket))); + const sub_bucket_count_magnitude: u8 = @intFromFloat(@ceil(log2largest_value)); // bits required to represent largest value, rounded up + + const sub_bucket_count = std.math.pow(u64, 2, sub_bucket_count_magnitude); // actual quantity of sub-buckets to fit largest value + const sub_bucket_half_count = sub_bucket_count / 2; + const sub_bucket_half_count_magnitude: u6 = @truncate(sub_bucket_count_magnitude - 1); + + // lower bound of each bucket + const log2lowest_value = std.math.log2(@as(f64, @floatFromInt(options.lowest_trackable_value))); + const unit_magnitude = @as(u8, @intFromFloat(std.math.floor(log2lowest_value))); + + // represent this as a mask of 1s for efficient bitwise operations + const sub_bucket_mask = (sub_bucket_count - 1) * std.math.pow(u64, 2, unit_magnitude); + + // add more buckets if we need to track higher values + var bucket_count: u32 = 1; + var smallest_untrackable_value = sub_bucket_count * std.math.pow(u64, 2, unit_magnitude); + while (smallest_untrackable_value <= options.highest_trackable_value) { + if (smallest_untrackable_value > std.math.maxInt(u64) / 2) { + // next step would overflow, so we just increment the bucket count and break + bucket_count += 1; + break; + } + smallest_untrackable_value = 2 * smallest_untrackable_value; + bucket_count += 1; + } + const counts_len = (bucket_count + 1) * sub_bucket_half_count; + const counts = try allocator.alloc(u64, counts_len); + for (0..counts_len) |i| { + counts[i] = 0; + } + + return Histogram{ + .allocator = allocator, + .lowest_trackable_value = options.lowest_trackable_value, + .highest_trackable_value = options.highest_trackable_value, + .significant_figures = options.significant_figures, + .sub_bucket_count = sub_bucket_count, + .sub_bucket_half_count = sub_bucket_half_count, + .sub_bucket_half_count_magnitude = sub_bucket_half_count_magnitude, + .unit_magnitude = unit_magnitude, + .sub_bucket_mask = sub_bucket_mask, + .bucket_count = bucket_count, + .counts = counts, + .total_count = 0, + .min_value = std.math.maxInt(u64), + .max_value = 0, + }; + } + + pub fn record_value(self: *Histogram, value: u64, count: u64) void { + if (value < self.lowest_trackable_value or value > self.highest_trackable_value) return; + const counts_index = self.calculate_index(value); + if (counts_index >= self.counts.len) return; + self.counts[counts_index] += count; + self.total_count += count; + if (self.min_value > value) self.min_value = value; + if (self.max_value < value) self.max_value = value; + } + + fn calculate_index(self: *const Histogram, value: u64) usize { + const bucket_index = self.get_bucket_index(value); + const sub_bucket_index = self.get_sub_bucket_index(value, bucket_index); + return self.get_counts_index(bucket_index, sub_bucket_index); + } + + fn get_counts_index(self: *const Histogram, bucket_index: u64, sub_bucket_index: u64) usize { + const bucket_base_index = (bucket_index + 1) << self.sub_bucket_half_count_magnitude; + return @as(usize, bucket_base_index + sub_bucket_index - self.sub_bucket_half_count); + } + + fn get_bucket_index(self: *const Histogram, value: u64) u8 { + const pow2ceiling = 64 - @clz(value | self.sub_bucket_mask); + return pow2ceiling - self.unit_magnitude - (self.sub_bucket_half_count_magnitude + 1); + } + + fn get_sub_bucket_index(self: *const Histogram, value: u64, bucket_index: u8) u64 { + return value >> @as(u6, @intCast(bucket_index + self.unit_magnitude)); + } +}; + +test "record_value" { + const significant_figures = 3; + const lowest_trackable_value = 1; + const highest_trackable_value = 1000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + histogram.record_value(1, 1); + try std.testing.expect(histogram.total_count == 1); + try std.testing.expect(histogram.min_value == 1); + try std.testing.expect(histogram.max_value == 1); + try std.testing.expect(histogram.counts.len == 2048); + try std.testing.expect(histogram.counts[1] == 1); + histogram.record_value(1, 1); + try std.testing.expect(histogram.total_count == 2); + try std.testing.expect(histogram.min_value == 1); + try std.testing.expect(histogram.max_value == 1); + try std.testing.expect(histogram.counts[1] == 2); + histogram.record_value(100, 1); + histogram.record_value(900, 1); + try std.testing.expect(histogram.total_count == 4); + try std.testing.expect(histogram.min_value == 1); + try std.testing.expect(histogram.max_value == 900); + try std.testing.expect(histogram.counts[1] == 2); + try std.testing.expect(histogram.counts[100] == 1); + try std.testing.expect(histogram.counts[900] == 1); +} + +test "record_value_multiple_buckets" { + const significant_figures = 1; + const lowest_trackable_value = 1; + const highest_trackable_value = 10000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + histogram.record_value(1, 1); + histogram.record_value(2, 1); + histogram.record_value(3, 1); + histogram.record_value(4, 1); + histogram.record_value(5, 1); + histogram.record_value(10, 1); + histogram.record_value(100, 1); + histogram.record_value(1000, 1); + try std.testing.expect(histogram.total_count == 8); + try std.testing.expect(histogram.min_value == 1); + try std.testing.expect(histogram.max_value == 1000); + try std.testing.expect(histogram.counts[1] == 1); + try std.testing.expect(histogram.counts[2] == 1); + try std.testing.expect(histogram.counts[3] == 1); + try std.testing.expect(histogram.counts[4] == 1); + try std.testing.expect(histogram.counts[5] == 1); + try std.testing.expect(histogram.counts[10] == 1); + try std.testing.expect(histogram.counts[57] == 1); // indices pulled from official implementation + try std.testing.expect(histogram.counts[111] == 1); // indices pulled from official implementation +} + +test "init sigfig=3 lowest=1 highest=1000" { + // used official implementation to verify the values + const significant_figures = 3; + const lowest_trackable_value = 1; + const highest_trackable_value = 1000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); + try std.testing.expect(histogram.sub_bucket_count == 2048); + try std.testing.expect(histogram.sub_bucket_half_count == 1024); + try std.testing.expect(histogram.unit_magnitude == 0); + try std.testing.expect(histogram.sub_bucket_mask == 2047); + try std.testing.expect(histogram.bucket_count == 1); + try std.testing.expect(histogram.counts.len == 2048); +} + +test "init sigfig=3 lowest=1 highest=10_000" { + const significant_figures = 3; + const lowest_trackable_value = 1; + const highest_trackable_value = 10_000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); + try std.testing.expect(histogram.sub_bucket_count == 2048); + try std.testing.expect(histogram.sub_bucket_half_count == 1024); + try std.testing.expect(histogram.unit_magnitude == 0); + try std.testing.expect(histogram.sub_bucket_mask == 2047); + try std.testing.expect(histogram.bucket_count == 4); + try std.testing.expect(histogram.counts.len == 5120); +} + +test "init sigfig=4 lowest=1 highest=10_000" { + const significant_figures = 4; + const lowest_trackable_value = 1; + const highest_trackable_value = 10_000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + //&{lowestDiscernibleValue:1 highestTrackableValue:10000 unitMagnitude:0 significantFigures:4 subBucketHalfCountMagnitude:14 subBucketHalfCount:16384 subBucketMask:32767 subBucketCount:32768 bucketCount:1 countsLen:32768 totalCount:0 counts + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); + try std.testing.expect(histogram.sub_bucket_count == 32768); + try std.testing.expect(histogram.sub_bucket_half_count == 16384); + try std.testing.expect(histogram.unit_magnitude == 0); + try std.testing.expect(histogram.sub_bucket_mask == 32767); + try std.testing.expect(histogram.bucket_count == 1); + try std.testing.expect(histogram.counts.len == 32768); +} + +test "init sigfig=4 lowest=5 highest=1000" { + const significant_figures = 4; + const lowest_trackable_value = 5; + const highest_trackable_value = 1000; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); + try std.testing.expect(histogram.sub_bucket_count == 32768); + try std.testing.expect(histogram.sub_bucket_half_count == 16384); + try std.testing.expect(histogram.unit_magnitude == 2); + try std.testing.expect(histogram.sub_bucket_mask == 131068); + try std.testing.expect(histogram.bucket_count == 1); + try std.testing.expect(histogram.counts.len == 32768); +} + +test "init sigfig=5 lowest=10 highest=200" { + const significant_figures = 5; + const lowest_trackable_value = 10; + const highest_trackable_value = 200; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); + try std.testing.expect(histogram.sub_bucket_count == 262144); + try std.testing.expect(histogram.sub_bucket_half_count == 131072); + try std.testing.expect(histogram.unit_magnitude == 3); + try std.testing.expect(histogram.sub_bucket_mask == 2097144); + try std.testing.expect(histogram.bucket_count == 1); + try std.testing.expect(histogram.counts.len == 262144); +} + +// default node timerify histogram +test "init sigfig=3 lowest=1 highest=9007199254740991" { + const significant_figures = 3; + const lowest_trackable_value = 1; + const highest_trackable_value = 9007199254740991; + const allocator = std.testing.allocator; + var histogram = try Histogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + defer histogram.deinit(); + try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); + try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); + try std.testing.expect(histogram.significant_figures == significant_figures); +} diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index ef5fc5686e..55d99f67dc 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -560,23 +560,23 @@ export default [ JSType: "0b11101110", proto: { min: { getter: "min" }, - // max: { getter: "max" }, - // mean: { getter: "mean" }, - // exceeds: { getter: "exceeds" }, - // stddev: { getter: "stddev" }, - // count: { getter: "count" }, - // percentiles: { getter: "percentiles" }, - // reset: { fn: "reset", length: 0 }, + max: { getter: "max" }, + mean: { getter: "mean" }, + // exceeds: { getter: "exceeds" }, // not implemented + stddev: { getter: "stddev" }, + count: { getter: "count" }, + percentiles: { getter: "percentiles" }, + reset: { fn: "reset", length: 0 }, record: { fn: "record", length: 1 }, - // recordDelta: { fn: "recordDelta", length: 0 }, - // add: { fn: "add", length: 1 }, - // percentile: { fn: "percentile", length: 1 }, - // minBigInt: { fn: "minBigInt", length: 0 }, - // maxBigInt: { fn: "maxBigInt", length: 0 }, - // exceedsBigInt: { fn: "exceedsBigInt", length: 0 }, - // countBigInt: { fn: "countBigInt", length: 0 }, - // percentilesBigInt: { fn: "percentilesBigInt", length: 0 }, - // percentileBigInt: { fn: "percentileBigInt", length: 1 }, + recordDelta: { fn: "recordDelta", length: 0 }, + add: { fn: "add", length: 1 }, + percentile: { fn: "percentile", length: 1 }, + minBigInt: { fn: "minBigInt", length: 0 }, + maxBigInt: { fn: "maxBigInt", length: 0 }, + // exceedsBigInt: { fn: "exceedsBigInt", length: 0 }, // not implemented + countBigInt: { fn: "countBigInt", length: 0 }, + percentilesBigInt: { fn: "percentilesBigInt", length: 0 }, + percentileBigInt: { fn: "percentileBigInt", length: 1 }, // toJSON: { fn: "toJSON", length: 0 }, }, values: [], diff --git a/src/bun.js/node/node_perf_hooks_histogram.zig b/src/bun.js/node/node_perf_hooks_histogram.zig index 0a69cb7ef4..5b4ac2c0b0 100644 --- a/src/bun.js/node/node_perf_hooks_histogram.zig +++ b/src/bun.js/node/node_perf_hooks_histogram.zig @@ -7,9 +7,15 @@ pub const HistogramOptions = struct { }; // Zig port of High Dynamic Range (HDR) Histogram algorithm -// Only supports recording values for now pub const HDRHistogram = struct { + // TLDR: an HDR histogram has buckets, with each bucket having a fixed number of sub-buckets + // Using default sig-figure of 3, the first bucket has 2048 sub-buckets + // In the 0th bucket, each sub-bucket represents a value range of 1 + // In the 1st bucket, each sub-bucket represents a value range of 2 + // In the 2nd bucket, each sub-bucket represents a value range of 4 and so on + // The sub-buckets are used to track the frequency of values within their range + // visible to user min: u64, max: u64, @@ -31,7 +37,6 @@ pub const HDRHistogram = struct { const This = @This(); pub fn init(allocator: std.mem.Allocator, options: HistogramOptions) !This { - // dummy input: lowest=1, highest=1000, sigfig=3 // Validate input if (options.significant_figures < 1 or options.significant_figures > 5) { @@ -43,13 +48,14 @@ pub const HDRHistogram = struct { } // Calculate derived values for efficient bucketing + // HDR Histogram is optimized for writes using bitwise operations and bit shifting, so we precalculate bitmasks and other helpful values - // upper bound of each bucket + // upper value bound of each bucket const largest_value_in_bucket = 2 * std.math.pow(u64, 10, options.significant_figures); const log2largest_value = std.math.log2(@as(f64, @floatFromInt(largest_value_in_bucket))); const sub_bucket_count_magnitude: u8 = @intFromFloat(@ceil(log2largest_value)); // bits required to represent largest value, rounded up - const sub_bucket_count = std.math.pow(u64, 2, sub_bucket_count_magnitude); // actual quantity of sub-buckets to fit largest value + const sub_bucket_count = std.math.pow(u64, 2, sub_bucket_count_magnitude); // actual quantity of sub-buckets per bucket, defaults to 2048 const sub_bucket_half_count = sub_bucket_count / 2; const sub_bucket_half_count_magnitude: u6 = @truncate(sub_bucket_count_magnitude - 1); @@ -91,7 +97,7 @@ pub const HDRHistogram = struct { .bucket_count = bucket_count, .counts = counts, .total_count = 0, - .min = std.math.maxInt(u64), + .min = 9223372036854776000, .max = 0, }; } @@ -100,6 +106,68 @@ pub const HDRHistogram = struct { self.allocator.free(self.counts); } + pub fn mean(self: *This) ?f64 { + if (self.total_count == 0) { + return null; + } + + var total_sum: u64 = 0; + + for (self.counts, 0..) |count, index| { + if (count > 0) { + const median_equiv_value = self.value_from_index(index); + total_sum += count * median_equiv_value; + } + } + + return @as(f64, @floatFromInt(total_sum)) / @as(f64, @floatFromInt(self.total_count)); + } + + pub fn stddev(self: *This) ?f64 { + if (self.total_count == 0) { + return null; + } + + const m = self.mean() orelse return null; + var geometric_dev_total: f64 = 0.0; + for (self.counts, 0..) |count, index| { + if (count > 0) { + const median_equiv_value = self.value_from_index(index); + const dev = @as(f64, @floatFromInt(median_equiv_value)) - m; + geometric_dev_total += (dev * dev) * @as(f64, @floatFromInt(count)); + } + } + + return std.math.sqrt(geometric_dev_total / @as(f64, @floatFromInt(self.total_count))); + } + + pub fn reset(self: *This) void { + for (0..self.counts.len) |index| { + self.counts[index] = 0; + } + self.total_count = 0; + self.min = 9223372036854776000; + self.max = 0; + } + + pub fn add(self: *This, other: *const This) !void { + if (self.lowest_trackable_value != other.lowest_trackable_value or self.highest_trackable_value != other.highest_trackable_value or self.significant_figures != other.significant_figures) { + return error.InvalidHistograms; + } + + for (other.counts, 0..) |count, index| { + self.counts[index] += count; + } + + self.total_count += other.total_count; + if (self.min > other.min) self.min = other.min; + if (self.max < other.max) self.max = other.max; + } + + // + // Writes to the histogram + // + pub fn record_value(self: *This, value: u64, quanity: u64) void { if (value < self.lowest_trackable_value or value > self.highest_trackable_value) return; const counts_index = self.calculate_index(value); @@ -113,7 +181,8 @@ pub const HDRHistogram = struct { fn calculate_index(self: *const This, value: u64) usize { const bucket_index = self.get_bucket_index(value); const sub_bucket_index = self.get_sub_bucket_index(value, bucket_index); - return self.get_counts_index(bucket_index, sub_bucket_index); + const counts_index = self.get_counts_index(bucket_index, sub_bucket_index); + return counts_index; } fn get_counts_index(self: *const This, bucket_index: u64, sub_bucket_index: u64) usize { @@ -129,8 +198,144 @@ pub const HDRHistogram = struct { fn get_sub_bucket_index(self: *const This, value: u64, bucket_index: u8) u64 { return value >> @as(u6, @intCast(bucket_index + self.unit_magnitude)); } + + // + // Reads from the histogram + // + + fn value_from_index(self: *This, index: u64) u64 { + const bucket_index = self.get_bucket_index_from_idx(index); + const sub_bucket_index = self.get_sub_bucket_index_from_idx(index, bucket_index); + + // Directly compute the value from the bucket index and sub-bucket index + return @as(u64, sub_bucket_index) << @as(u6, @truncate(bucket_index + self.unit_magnitude)); + + // // now we need to find the value at this index + // const lower_bound = sub_bucket_index * std.math.pow(u64, 2, @intCast(bucket_index + self.unit_magnitude)); + // const width = std.math.pow(u64, 2, @intCast(bucket_index + self.unit_magnitude)); + // const upper_bound = lower_bound + width - 1; + // return lower_bound + @as(u64, @intFromFloat(@floor(@as(f64, @floatFromInt(upper_bound - lower_bound)) / 2.0))); + } + + fn get_bucket_index_from_idx(self: *const This, index: u64) u8 { + var bucket_index: u8 = 0; + var remaining_index = index; + + while (remaining_index >= self.sub_bucket_count) { + bucket_index += 1; + remaining_index -= self.sub_bucket_half_count; + } + return bucket_index; + } + + fn get_sub_bucket_index_from_idx(self: *const This, index: u64, bucket_index: u8) u64 { + const sub_bucket_index = index - (bucket_index * self.sub_bucket_half_count); + return sub_bucket_index; + } + + // percentile is a value between 0 and 100 + pub fn value_at_percentile(self: *This, percentile: f64) ?u64 { + if (percentile < 0 or percentile > 100) { + return null; + } + if (percentile == 0 and self.total_count > 0) { + return self.min; + } + const total = self.total_count; + const target_count = @as(f64, @floatFromInt(total)) * percentile / 100; + var running_total: f64 = 0; + for (self.counts, 0..) |count, index| { + running_total += @floatFromInt(count); + if (running_total >= target_count) { + // we found the index that corresponds to the percentile + return self.value_from_index(index); + } + } + return null; + } }; +test "value_at_percentile" { + const allocator = std.testing.allocator; + var histogram = try HDRHistogram.init(allocator, .{}); + defer histogram.deinit(); + histogram.record_value(100, 9000); // 0-90% + histogram.record_value(200, 990); // 90-99.9% + histogram.record_value(1000, 9); // 99.9-99.99% + histogram.record_value(2000, 1); // 99.99-100% + try std.testing.expect(histogram.value_at_percentile(0) == 100); + try std.testing.expect(histogram.value_at_percentile(50) == 100); + try std.testing.expect(histogram.value_at_percentile(90) == 100); + try std.testing.expect(histogram.value_at_percentile(99) == 200); + try std.testing.expect(histogram.value_at_percentile(99.9) == 200); + try std.testing.expect(histogram.value_at_percentile(99.99) == 1000); +} + +test "value_from_index" { + const allocator = std.testing.allocator; + var histogram = try HDRHistogram.init(allocator, .{}); + defer histogram.deinit(); + histogram.record_value(100, 1); // -> bucket_index: 0, sub_bucket_index: 100 ==> counts_index: 100 + var value = histogram.value_from_index(100); + // first bucket has a unit width of 1, so the value returned is guaranteed to be 100 + try std.testing.expect(value == 100); + + histogram.record_value(5000, 1); // -> bucket_index: 2, sub_bucket_index: 1250 ==> counts_index: 3298 + value = histogram.value_from_index(3298); + // value could be a range since higher buckets have subbuckets of larger value range + // this subbucket has a value range of 4, so the value could be between 5000 and 5003 + // average of 5000 and 5003 is 5001.5, so we expect the value to be floored to 5001 + try std.testing.expect(value == 5001); +} + +test "get_indices_from_idx" { + const allocator = std.testing.allocator; + var histogram = try HDRHistogram.init(allocator, .{}); + defer histogram.deinit(); + histogram.record_value(100, 1); // -> bucket_index: 0, sub_bucket_index: 100 ==> counts_index: 100 + histogram.record_value(200, 1); // -> bucket_index: 0, sub_bucket_index: 200 ==> counts_index: 200 + histogram.record_value(1000, 1); // -> bucket_index: 0, sub_bucket_index: 1000 ==> counts_index: 1000 + histogram.record_value(2000, 1); // -> bucket_index: 0, sub_bucket_index: 2000 ==> counts_index: 2000 + histogram.record_value(3000, 1); // -> bucket_index: 1, sub_bucket_index: 1500 ==> counts_index: 2524 + histogram.record_value(5000, 1); // -> bucket_index: 2, sub_bucket_index: 1250 ==> counts_index: 3298 + histogram.record_value(1000000, 1); // -> bucket_index: 9, sub_bucket_index: 1953 ==> counts_index: 11169 + + var bucket_index = histogram.get_bucket_index_from_idx(100); + try std.testing.expect(bucket_index == 0); + var sub_bucket_index = histogram.get_sub_bucket_index_from_idx(100, bucket_index); + try std.testing.expect(sub_bucket_index == 100); + + bucket_index = histogram.get_bucket_index_from_idx(200); + try std.testing.expect(bucket_index == 0); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(200, bucket_index); + try std.testing.expect(sub_bucket_index == 200); + + bucket_index = histogram.get_bucket_index_from_idx(1000); + try std.testing.expect(bucket_index == 0); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(1000, bucket_index); + try std.testing.expect(sub_bucket_index == 1000); + + bucket_index = histogram.get_bucket_index_from_idx(2000); + try std.testing.expect(bucket_index == 0); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(2000, bucket_index); + try std.testing.expect(sub_bucket_index == 2000); + + bucket_index = histogram.get_bucket_index_from_idx(2524); + try std.testing.expect(bucket_index == 1); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(2524, bucket_index); + try std.testing.expect(sub_bucket_index == 1500); + + bucket_index = histogram.get_bucket_index_from_idx(3298); + try std.testing.expect(bucket_index == 2); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(3298, bucket_index); + try std.testing.expect(sub_bucket_index == 1250); + + bucket_index = histogram.get_bucket_index_from_idx(11169); + try std.testing.expect(bucket_index == 9); + sub_bucket_index = histogram.get_sub_bucket_index_from_idx(11169, bucket_index); + try std.testing.expect(sub_bucket_index == 1953); +} + test "record_value" { const significant_figures = 3; const lowest_trackable_value = 1; @@ -280,14 +485,27 @@ test "init sigfig=5 lowest=10 highest=200" { } // default node timerify histogram -test "init sigfig=3 lowest=1 highest=9007199254740991" { - const significant_figures = 3; - const lowest_trackable_value = 1; - const highest_trackable_value = 9007199254740991; +test "default init" { const allocator = std.testing.allocator; - var histogram = try HDRHistogram.init(allocator, .{ .lowest_trackable_value = lowest_trackable_value, .highest_trackable_value = highest_trackable_value, .significant_figures = significant_figures }); + var histogram = try HDRHistogram.init(allocator, .{}); defer histogram.deinit(); - try std.testing.expect(histogram.lowest_trackable_value == lowest_trackable_value); - try std.testing.expect(histogram.highest_trackable_value == highest_trackable_value); - try std.testing.expect(histogram.significant_figures == significant_figures); + + histogram.record_value(100, 1); // -> bucket_index: 0, sub_bucket_index: 100 ==> counts_index: 100 + histogram.record_value(200, 1); // -> bucket_index: 0, sub_bucket_index: 200 ==> counts_index: 200 + histogram.record_value(1000, 1); // -> bucket_index: 0, sub_bucket_index: 1000 ==> counts_index: 1000 + histogram.record_value(2000, 1); // -> bucket_index: 0, sub_bucket_index: 2000 ==> counts_index: 2000 + histogram.record_value(3000, 1); // -> bucket_index: 1, sub_bucket_index: 1500 ==> counts_index: 2524 + histogram.record_value(5000, 1); // -> bucket_index: 2, sub_bucket_index: 1250 ==> counts_index: 3298 + histogram.record_value(1000000, 1); // -> bucket_index: 9, sub_bucket_index: 1953 ==> counts_index: 11169 + + try std.testing.expect(histogram.total_count == 7); + try std.testing.expect(histogram.min == 100); + try std.testing.expect(histogram.max == 1000000); + try std.testing.expect(histogram.counts[100] == 1); + try std.testing.expect(histogram.counts[200] == 1); + try std.testing.expect(histogram.counts[1000] == 1); + try std.testing.expect(histogram.counts[2000] == 1); + try std.testing.expect(histogram.counts[2524] == 1); + try std.testing.expect(histogram.counts[3298] == 1); + try std.testing.expect(histogram.counts[11169] == 1); } diff --git a/src/bun.js/node/node_perf_hooks_histogram_binding.zig b/src/bun.js/node/node_perf_hooks_histogram_binding.zig index fb97528fa2..3168f15ae8 100644 --- a/src/bun.js/node/node_perf_hooks_histogram_binding.zig +++ b/src/bun.js/node/node_perf_hooks_histogram_binding.zig @@ -10,67 +10,124 @@ pub const RecordableHistogram = struct { pub usingnamespace JSC.Codegen.JSRecordableHistogram; hdrHist: HDRHistogram = undefined, + // RecordableHistogram specific internals + delta_start: ?bun.timespec = null, + const This = @This(); + const PropertyGetter = fn (this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; - //todo: these should also be explicit functions, IE both .max and .max() work - // pub const exceeds = getter(.exceeds); + pub const min_fn = struct { + pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { + return globalThis.toJS(this.hdrHist.min, .temporary); + } + }; + pub const min = @as(PropertyGetter, min_fn.callback); + pub const minBigInt = getterAsFn(min_fn.callback); - pub const min = @as( + const max_fn = struct { + pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { + return globalThis.toJS(this.hdrHist.max, .temporary); + } + }; + pub const max = @as(PropertyGetter, max_fn.callback); + pub const maxBigInt = getterAsFn(max_fn.callback); + + const count_fn = struct { + pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { + return globalThis.toJS(this.hdrHist.total_count, .temporary); + } + }; + pub const count = @as(PropertyGetter, count_fn.callback); + pub const countBigInt = getterAsFn(count_fn.callback); + + pub const mean = @as( PropertyGetter, struct { pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - return globalThis.toJS(this.hdrHist.min, .temporary); + if (this.hdrHist.mean()) |m| { + return globalThis.toJS(m, .temporary); + } + return globalThis.toJS(std.math.nan(f64), .temporary); } }.callback, ); - // pub const max = @as( - // PropertyGetter, - // struct { - // pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - // return globalThis.toJS(this._histogram.max, .temporary); - // } - // }.callback, - // ); + pub const stddev = @as( + PropertyGetter, + struct { + pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { + if (this.hdrHist.stddev()) |sd| { + return globalThis.toJS(sd, .temporary); + } + return globalThis.toJS(std.math.nan(f64), .temporary); + } + }.callback, + ); - // pub const count = @as( - // PropertyGetter, - // struct { - // pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - // return globalThis.toJS(this._histogram.total_count, .temporary); - // } - // }.callback, - // ); + pub const percentile = struct { + pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + const args = callframe.arguments(1).slice(); + if (args.len != 1) { + globalThis.throwInvalidArguments("Expected 1 argument", .{}); + return .zero; + } + const percent = args[0].getNumber() orelse { + globalThis.throwInvalidArguments("Expected a number", .{}); + return .zero; + }; + const value = this.hdrHist.value_at_percentile(percent) orelse return .undefined; + return globalThis.toJS(value, .temporary); + } + }.callback; + pub const percentileBigInt = percentile; - // pub const mean = @as( - // PropertyGetter, - // struct { - // pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - // return globalThis.toJS(this._histogram.mean(), .temporary); - // } - // }.callback, - // ); + extern fn Bun__createMapFromDoubleUint64TupleArray( + globalObject: *JSC.JSGlobalObject, + doubles: [*]const f64, + length: usize, + ) JSC.JSValue; - // pub const stddev = @as( - // PropertyGetter, - // struct { - // pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - // return globalThis.toJS(this._histogram.stddev(), .temporary); - // } - // }.callback, - // ); + const percentiles_fn = struct { + pub fn callback(this: *This, globalObject: *JSC.JSGlobalObject) callconv(.C) JSValue { - // pub const percentiles = @as( - // PropertyGetter, - // struct { - // pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue { - // _ = globalThis; - // _ = this; - // return .undefined; - // } - // }.callback, - // ); + // 2 arrays with percent and value + // make a cpp version of this file, extern C function. accepts array, length, creates search JSMap:: (search) + // first get 100th percentile, and loop 0, 50, 75, 82.5, ... until we find the highest percentile + const maxPercentileValue = this.hdrHist.value_at_percentile(100) orelse return .undefined; + var percent: f64 = 0; + var stack_allocator = std.heap.stackFallback(4096, bun.default_allocator); + var doubles = std.ArrayList(f64).init(stack_allocator.get()); + defer doubles.deinit(); + + while (true) { + if (this.hdrHist.value_at_percentile(percent)) |val| { + doubles.appendSlice(&.{ percent, @bitCast(val) }) catch |err| { + globalObject.throwError(err, "failed to append to array"); + return .undefined; + }; + if (val >= maxPercentileValue) { + break; + } + } + percent += ((100 - percent) / 2); + } + + doubles.appendSlice(&.{ 100, @bitCast(maxPercentileValue) }) catch |err| { + globalObject.throwError(err, "failed to append max value to array"); + return .undefined; + }; + + return Bun__createMapFromDoubleUint64TupleArray(globalObject, @as([*]const f64, @ptrCast(doubles.items)), doubles.items.len); + } + }; + pub const percentiles = @as(PropertyGetter, percentiles_fn.callback); + pub const percentilesBigInt = getterAsFn(percentiles_fn.callback); + + // + // additional functions + + // record duration in nanoseconds pub fn record(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { const args = callframe.arguments(1).slice(); if (args.len != 1) { @@ -82,150 +139,65 @@ pub const RecordableHistogram = struct { return .undefined; } - // pub fn recordDelta(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } + // record time since last call to recordDelta + pub fn recordDelta(this: *This, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + if (this.delta_start) |start| { + const end = bun.timespec.now(); + const diff = end.duration(&start); + this.hdrHist.record_value(@intCast(diff.nsec), 1); + this.delta_start = end; + return .undefined; + } - // pub fn add(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // const args = callframe.arguments(1).slice(); - // if (args.len != 1) { - // globalThis.throwInvalidArguments("Expected 1 argument", .{}); - // return .zero; - // } - // // todo: make below work - // // const other = args[0].to(RecordableHistogram); - // // _ = other; - // return .undefined; - // } + // first call no-ops + this.delta_start = bun.timespec.now(); - // pub fn reset(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn countBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn minBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn maxBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn exceedsBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn percentile(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // const args = callframe.arguments(1).slice(); - // if (args.len != 1) { - // globalThis.throwInvalidArguments("Expected 1 argument", .{}); - // return .zero; - // } - // // todo: make below work - // // const percent = args[0].to(f64); - // // _ = percent; - // return .undefined; - // } - - // pub fn percentileBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // const args = callframe.arguments(1).slice(); - // if (args.len != 1) { - // globalThis.throwInvalidArguments("Expected 1 argument", .{}); - // return .zero; - // } - // // todo: make below work - // // const percent = args[0].to(f64); - // // _ = percent; - // return .undefined; - // } - - // pub fn percentilesBigInt(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - // pub fn toJSON(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - // _ = this; - // _ = globalThis; - // _ = callframe; - // return .undefined; - // } - - const PropertyGetter = fn (this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; - fn getter(comptime field: meta.FieldEnum(This)) PropertyGetter { - return struct { - pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - const v = @field(this, @tagName(field)); - return globalThis.toJS(v, .temporary); - } - }.callback; + return .undefined; } - // const PropertySetter = fn (this: *This, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(.C) bool; - // fn setter(comptime field: meta.FieldEnum(This)) PropertySetter { - // return struct { - // pub fn callback( - // this: *This, - // globalThis: *JSC.JSGlobalObject, - // value: JSC.JSValue, - // ) callconv(.C) bool { - // const fieldType = @TypeOf(@field(this, @tagName(field))); - // switch (fieldType) { - // u64, i64 => |T| { - // if (!value.isNumber()) { - // globalThis.throwInvalidArguments("Expected a number", .{}); // protect users from themselves - // return false; - // } - // @field(this, @tagName(field)) = value.to(T); - // return true; - // }, - // f64 => { - // if (!value.isNumber()) { - // globalThis.throwInvalidArguments("Expected a number", .{}); - // return false; - // } - // @field(this, @tagName(field)) = value.asNumber(); - // return true; - // }, - // bool => { - // if (!value.isBoolean()) { - // globalThis.throwInvalidArguments("Expected a boolean", .{}); - // return false; - // } - // @field(this, @tagName(field)) = value.to(bool); - // return true; - // }, - // else => @compileError("Unsupported setter field type"), // protect us from ourselves - // } - // } - // }.callback; - // } + pub fn reset(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + _ = globalThis; + _ = callframe; + this.hdrHist.reset(); + return .undefined; + } + + pub fn add(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + const args = callframe.arguments(1).slice(); + if (args.len != 1) { + globalThis.throwInvalidArguments("Expected 1 argument", .{}); + return .zero; + } + const other = RecordableHistogram.fromJS(args[0]) orelse { + globalThis.throwInvalidArguments("Expected a RecordableHistogram", .{}); + return .zero; + }; + this.hdrHist.add(&other.hdrHist) catch |err| { + globalThis.throwError(err, "failed to add histograms"); + return .zero; + }; + + return .undefined; + } + + // the bigInt variants of these functions are simple getters without arguments, but we want them as methods + // so this function strips the callframe argument so we can use the same callback as we do with our actual getters + fn getterAsFn(callback: fn ( + this: *This, + globalThis: *JSC.JSGlobalObject, + ) callconv(.C) JSValue) fn ( + this: *This, + globalThis: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) JSValue { + const outer = struct { + pub fn inner(this: *This, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + // we don't need the callframe, so we can just call the callback + return callback(this, globalThis); + } + }; + return outer.inner; + } // since we create this with bun.new, we need to have it be destroyable // our node.classes.ts has finalize=true to generate the call to finalize diff --git a/src/erik.js b/src/erik.js index f4aa168bc7..e7cfa7efd7 100644 --- a/src/erik.js +++ b/src/erik.js @@ -7,13 +7,30 @@ const fn = () => { }; let h = createHistogram(); -console.log("before", h); - -let wrapped = performance.timerify(fn, { histogram: h }); -wrapped(); +h.record(100, 1); +h.record(200, 100); +h.record(1000, 1000); +h.record(2000, 1000); +h.record(3000, 1000); +h.record(5000, 1000); +h.record(1000000, 1); console.log("after", h); -// wrapped(400); +let otherH = createHistogram(); +otherH.record(1, 300); +h.add(otherH); + +console.log("after add", h); + +h.reset(); + +let wrapped = performance.timerify(fn, { histogram: h }); +for (let i = 0; i < 1000; i++) { + wrapped(); +} +console.log(h); + +// // wrapped(400); // console.log(h); // h.percentiles.forEach((value, key) => {