massive progress

This commit is contained in:
Erik Dunteman
2024-06-28 18:17:32 -07:00
parent 34cd762581
commit a722a21fe7
9 changed files with 424 additions and 8 deletions

View File

@@ -73,4 +73,5 @@ pub const Classes = struct {
pub const BytesInternalReadableStreamSource = JSC.WebCore.ByteStream.Source;
pub const BrotliEncoder = JSC.API.BrotliEncoder;
pub const BrotliDecoder = JSC.API.BrotliDecoder;
pub const RecordableHistogram = JSC.Node.RecordableHistogram; // for new class, be sure to add here
};

View File

@@ -209,7 +209,10 @@ static const HashTableValue JSPerformancePrototypeTableValues[] = {
// { "clearResourceTimings"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_clearResourceTimings, 0 } },
// { "setResourceTimingBufferSize"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_setResourceTimingBufferSize, 1 } },
{ "mark"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_mark, 1 } },
{ "timerify"_s, static_cast<unsigned>(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, performanceTimerifyCodeGenerator, 1 } },
{ "timerify"_s, static_cast<unsigned>(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, performanceTimerifyCodeGenerator, 2 } }, // this routes through codegen'ed CPP to timerify in Performance.ts
// ERIK note because performanceTimerifyCodeGenerator has "CodeGenerator" at the end, we know the C++ bindings are generated for it based on the existance of "Performance.ts" in src/js/builtins "export timerify"
{ "clearMarks"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_clearMarks, 0 } },
{ "measure"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_measure, 1 } },
{ "clearMeasures"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsPerformancePrototypeFunction_clearMeasures, 0 } },

View File

@@ -549,4 +549,36 @@ export default [
// createWriteStream: { fn: "createWriteStream", length: 2 },
},
}),
define({
name: "RecordableHistogram",
construct: false,
noConstructor: true,
finalize: true, // this triggers the deallocation for bun.destroy
configurable: false,
hasPendingActivity: false,
klass: {},
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 },
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 },
toJSON: { fn: "toJSON", length: 0 },
},
values: [],
}),
];

View File

@@ -0,0 +1,181 @@
const std = @import("std");
const bun = @import("root").bun;
const meta = bun.meta;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
pub const RecordableHistogram = struct {
pub usingnamespace JSC.Codegen.JSRecordableHistogram;
min: u64 = 1,
max: u64 = 2,
mean: f64 = 3, // todo: make this optional
exceeds: u64 = 4,
stddev: f64 = 5, // todo: make this optional
count: u64 = 6,
percentilesInternal: std.AutoHashMap(f32, f32) = std.AutoHashMap(f32, f32).init(bun.default_allocator),
const This = @This();
//todo: these should also be explicit functions, IE both .max and .max() work
pub const min = getter(.min);
pub const max = getter(.max);
pub const mean = getter(.mean);
pub const exceeds = getter(.exceeds);
pub const stddev = getter(.stddev);
pub const count = getter(.count);
// we need a special getter for percentiles because it's a hashmap
pub const percentiles = @as(
PropertyGetter,
struct {
pub fn callback(this: *This, globalThis: *JSC.JSGlobalObject) callconv(.C) JSValue {
_ = this;
_ = globalThis;
return .undefined;
}
}.callback,
);
pub fn record(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
_ = this;
_ = globalThis;
_ = callframe;
return .undefined;
}
pub fn recordDelta(this: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
_ = this;
_ = globalThis;
_ = callframe;
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;
}
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, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue;
fn getter(comptime field: meta.FieldEnum(This)) PropertyGetter {
return struct {
pub fn callback(this: *This, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
const v = @field(this, @tagName(field));
return globalObject.toJS(v, .temporary);
}
}.callback;
}
pub const value = getter(.value);
// 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
pub fn finalize(this: *This) callconv(.C) void {
this.percentilesInternal.deinit();
bun.destroy(this);
}
};
fn createHistogram(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
return bun.new(RecordableHistogram, .{}).toJS(globalThis);
}
pub fn createPerfHooksHistogramBinding(global: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
const histogram = JSC.JSValue.createEmptyObject(global, 1);
histogram.put(
global,
bun.String.init("createHistogram"),
JSC.JSFunction.create(
global,
"createHistogram",
&createHistogram,
3, // function length
.{},
),
);
return histogram;
}

View File

@@ -1,8 +1,10 @@
import { performance } from "node:perf_hooks";
// import { performance, createHistogram } from "node:perf_hooks";
// cannot use import statement outside a module, so we do:
const { performance, createHistogram } = require("perf_hooks");
const fn = () => {
console.log("this is the function that will be timed");
};
let wrapped = performance.timerify(fn);
wrapped();
const histogram = createHistogram();
histogram.update(2);

View File

@@ -1,10 +1,23 @@
export function timerify(fn: Function) {
export function timerify(fn: Function, options) {
const { histogram } = options;
// create histogram class
class Histogram {
record(duration: number) {
console.log(`Recording duration: ${duration}`);
}
}
// wrap fn in a timer and return the wrapped function
return function (...args: any[]) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`Function took ${end - start}ms`);
if (histogram) {
console.log("recorded");
histogram.record(Math.ceil((end - start) * 1e6));
}
return result;
};
}

View File

@@ -98,6 +98,10 @@ Object.setPrototypeOf(PerformanceResourceTiming, PerformanceEntry);
export default {
performance: {
// perf_hooks is a builtin global, so JSC is aware of it through C++ bindings
// see JSPerformance.cpp HashTableValue for more details
// perf_hooks has a performance module with these exported functions,
// most implemented in C++ but I decide to do timerify in JS
mark(f) {
return performance.mark(...arguments);
},
@@ -105,7 +109,9 @@ export default {
return performance.measure(...arguments);
},
timerify(f) {
return performance.timerify(...arguments);
// in this case, we want it to go back to JS function since we're passing a JS function
// go to JSPerformance.cpp HashTableValue to see how this is handled differently from the rest
return performance.timerify(...arguments); // this routes to JSPerformance.cpp
},
clearMarks(f) {
return performance.clearMarks(...arguments);
@@ -162,7 +168,8 @@ export default {
throwNotImplemented("perf_hooks.monitorEventLoopDelay");
},
createHistogram() {
throwNotImplemented("perf_hooks.createHistogram");
const { createHistogram } = $zig("node_perf_hooks_histogram_binding.zig", "createPerfHooksHistogramBinding");
return createHistogram();
},
PerformanceResourceTiming,
};

View File

@@ -61,6 +61,7 @@ pub const Node = struct {
pub usingnamespace @import("./bun.js/node/node_fs_stat_watcher.zig");
pub usingnamespace @import("./bun.js/node/node_fs_binding.zig");
pub usingnamespace @import("./bun.js/node/node_os.zig");
pub usingnamespace @import("./bun.js/node/node_perf_hooks_histogram_binding.zig");
pub const fs = @import("./bun.js/node/node_fs_constant.zig");
pub const Util = struct {
pub const parseArgs = @import("./bun.js/node/util/parse_args.zig").parseArgs;

176
src/notes.md Normal file
View File

@@ -0,0 +1,176 @@
# performance.timerify project
goal: implement timerify:
timerify(fn, {histogram: Histogram})
timerify modifies the passed-in histogram by registering events into it. The histogram can later print performance distribution after many runs.
Two main parts of this so far:
- implement timerify function in JS (because it wraps a user's function and that's annoying to pass across language boundaries)
- implement Histogram class in Zig (used by timerify but nice to have performance and it's in CPP in node)
## How I got timerify to work
Thanks Jarred for the help on this one.
performance.timerify is part of the `node:perf_hooks` global, so importing it takes this code path:
- `perf_hooks.ts`
- this file was already here
- note we get "Performance" from globalThis, so that's proof it's a global
- `performance.timerify` calls via `JSPerformance.cpp`
- `JSPerformance.cpp`
- this file was already here
- name prefix "JS" implies codegen but this case is exception since it's part of webcore which we'd copied in and now manually edit
- note the `JSPerformancePrototypeTableValues`:
- for the other performance functions, they're implemented in C++ so we use `JSC::PropertyAttribute::Function` and `HashTableValue::NativeFunctionType` which call their respective C++ implementations
- for timerify, we use `PropertyAttribute::Builtin` and `HashTableValue::BuiltinGeneratorType` which will call through an _autogenerated_ C++ file to arrive back at a JS/TS file. It uses the `performanceTimerifyCodeGenerator` identifier, which I'll explain below.
- `Performance.ts`
- I created this file
- the existance of this TS file in `src/js/builtins` means it'll receive codegen for any exported functions
- we `export timerify`
- codegen will generate this glue function: (camelCased) {filename}{functionName}CodeGenerator, which is why we used `performanceTimerifyCodeGenerator` in `JSPerformance.cpp`
Calling `timerify` from JS will just feel like you're calling `Performance.ts``timerify` directly. It "just works" as if the glue code weren't there.
## how I got Histogram to work
This one was a bit harder, because I implement the class in Zig.
I followed `node_fs.zig` for inspiration. Thanks Dave as well for walking me through initial setup.
importing Histogram from node:perf_hooks follows this code path:
- `perf_hooks.ts`
- note the key bit here: `$zig("node_perf_hooks_histogram_binding.zig", "createPerfHooksHistogramBinding");`
- `$zig()` calls into a zig binding
- `node_perf_hooks_histogram_binding.zig` is in `src/bun.js/node`
- `createPerfHooksHistogramBinding` is the function name
- `node_perf_hooks_histogram_binding.zig`
- I made this file
- let's look at `createPerfHooksHistogramBinding`:
```javascript
pub fn createPerfHooksHistogramBinding(global: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
const histogram = JSC.JSValue.createEmptyObject(global, 1);
histogram.put(
global,
bun.String.init("createHistogram"),
JSC.JSFunction.create(
global,
"createHistogram",
&createHistogram,
3, // function length
.{},
),
);
return histogram;
}
```
- it must be pub fn
- it must receive `(global: *JSC.JSGlobalObject)`
- it must use `callconv(.C)` since it needs to speak C ABI for JS interop
- it must return a `JSC.JSValue`
- we want to create an object in JSC, which is the `JSC.JSValue` that's passed back to `perf_hooks.ts`
- we add the `createHistogram` as a function onto it. This is the name JS sees. The `3` is the max quantity of args.
- let's look at `createHistogram` itself, which is the function actually called by JS.
```javascript
fn createHistogram(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
return bun.new(Histogram, .{}).toJS(globalThis);
}
```
- `bun.new(T, init_value)` creates the object on heap, and `.toJS(globalThis)` makes it a `JSC.JSValue` for return
- you'll see later where the destroy happens.
- following this pattern, we can have an arbitrary Histogram struct, fully in Zig.
But there's more to do. We have the createHistogram function but we need to codegen a way to represent the Histogram class itself and pass it across Zig/JS boundaries.
- `src/bun.js/node/node.classes.ts`
- this file defines classes that need to be codegen'ed
- I add the histogram class, important fields have comments:
```javascript
define({
name: "Histogram", // the name in JS
construct: false,
noConstructor: true,
finalize: true, // this triggers the deallocation for bun.destroy
configurable: false,
hasPendingActivity: false,
klass: {},
JSType: "0b11101110",
proto: {
// we put methods here
update: { // name in JS
fn: "update", // name in Zig
length: 1, // quantity of args
},
foo: { getter: "foo" }, // public getters, so that histogram.foo is gettable from JS
},
values: [],
}),
```
- lastly, add the class to `generated_class_list.zig` as `JSC.Node.Histogram`
Ok, we're almost there. Now we need to look at the Histogram struct for a few subtle things we need to do.
```javascript
pub const Histogram = struct {
pub usingnamespace JSC.Codegen.JSHistogram; // Important, makes the JS interop work
// Zig native stuff
foo: u64 = 0,
const This = @This();
// We can a custom, arbitrary method, make sure to add it to the class in node.classes.ts
// It must have this function signature: (*Self, globalThis, callframe) callconv(.C) JSValue
pub fn update(self: *This, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
// args come from the callframe
const args = callframe.arguments(1).slice();
if (args.len != 1) {
globalThis.throwInvalidArguments("Expected 1 argument", .{});
return .zero;
}
self.foo = args[0].to(u64);
return .undefined; // JSValue equivalent of a void return
}
// Below is a bit of boilerplate we need
// This boilerplate is for writting getters, at least for trivial types.
const PropertyGetter = fn (this: *This, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue;
fn getter(comptime field: meta.FieldEnum(This)) PropertyGetter {
return struct {
pub fn callback(this: *This, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
const v = @field(this, @tagName(field));
return globalObject.toJS(v, .temporary);
}
}.callback;
}
// We can add a getter to make self.foo public.
// Make sure this is also added to node.classes.ts
pub const foo = getter(.foo);
// since we create Histogram objects with bun.new, we need to have it be destroyable
// our node.classes.ts has finalize=true to generate the call to finalize
// it expects this specific function name to destroy itself.
pub fn finalize(self: *Histogram) callconv(.C) void {
bun.destroy(self);
}
};
```
## Upcoming:
- actually adding Histogram as an argument into timerify and making timerify call Histogram methods
- implementing all of Histogram