should... be done

This commit is contained in:
Erik Dunteman
2024-07-03 22:04:05 -07:00
parent 7d66a545e8
commit 7d5cd8a3f1
6 changed files with 751 additions and 221 deletions

View File

@@ -0,0 +1,36 @@
#include "root.h"
#include "blob.h"
#include "headers-handwritten.h"
#include "JavaScriptCore/JSCJSValue.h"
#include "JavaScriptCore/JSCast.h"
#include <JavaScriptCore/PropertySlot.h>
#include <JavaScriptCore/JSMap.h>
#include "JavaScriptCore/JSMapInlines.h"
#include <JavaScriptCore/JSString.h>
#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);
}

View File

@@ -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);
}

View File

@@ -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: [],

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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) => {