Files
2026-10-14 04:20:24 +02:00

78 lines
3.7 KiB
Diff

QuickJS: sample the running stack from an interrupt handler.
Modified from QuickJS-ng (MIT, Copyright Fabrice Bellard, Charlie Gordon, Ben
Noordhuis and the QuickJS-ng contributors) at the revision pinned in
dependencies.lock.json.
Changed files: quickjs.c and quickjs.h.
Not proposed upstream: PS5 React maintains this change here.
PS5 React's JavaScript profiler (native/shared/js_profiler.cpp) samples the
stack about once a millisecond from the interrupt handler. JS_SampleStack walks
the runtime's stack frames and reports each function's name and the line its
definition starts on, reading names from the bytecode or, for a native
function, its own `name` property, without allocating or running JavaScript.
diff --git a/quickjs.c b/quickjs.c
index 38a724f..3fedc2d 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -7742,6 +7742,41 @@ static const char *get_func_name(JSContext *ctx, JSValueConst func)
return JS_ToCString(ctx, val);
}
+int JS_SampleStack(JSRuntime *rt, JSStackSampleFunc *visit, void *opaque, int max_frames)
+{
+ char buf[ATOM_GET_STR_BUF_SIZE];
+ int count = 0;
+ for (JSStackFrame *sf = rt->current_stack_frame; sf && count < max_frames; sf = sf->prev_frame, count++) {
+ const char *name = "";
+ int line = -1;
+ if (JS_VALUE_GET_TAG(sf->cur_func) == JS_TAG_OBJECT) {
+ JSObject *p = JS_VALUE_GET_OBJ(sf->cur_func);
+ if (js_class_has_bytecode(p->class_id)) {
+ JSFunctionBytecode *b = p->u.func.function_bytecode;
+ if (b->func_name != JS_ATOM_NULL)
+ name = JS_AtomGetStrRT(rt, buf, sizeof buf, b->func_name);
+ line = b->line_num;
+ } else {
+ /* A native function's name is its own `name` property, read without a getter. */
+ JSProperty *pr;
+ JSShapeProperty *prs = find_own_property(&pr, p, JS_ATOM_name);
+ if (prs && (prs->flags & JS_PROP_TMASK) == JS_PROP_NORMAL
+ && JS_VALUE_GET_TAG(pr->u.value) == JS_TAG_STRING) {
+ JSString *str = JS_VALUE_GET_STRING(pr->u.value);
+ if (!str->is_wide_char) {
+ size_t len = min_uint32(str->len, sizeof buf - 1);
+ memcpy(buf, strv(str), len);
+ buf[len] = '\0';
+ name = buf;
+ }
+ }
+ }
+ }
+ visit(opaque, name, line);
+ }
+ return count;
+}
+
/* Note: it is important that no exception is returned by this function */
static bool can_add_backtrace(JSValueConst obj)
{
diff --git a/quickjs.h b/quickjs.h
index b9ed275..4ba4efe 100644
--- a/quickjs.h
+++ b/quickjs.h
@@ -1139,6 +1139,12 @@ JS_EXTERN void JS_SetHostPromiseRejectionTracker(JSRuntime *rt, JSHostPromiseRej
/* return != 0 if the JS code needs to be interrupted */
typedef int JSInterruptHandler(JSRuntime *rt, void *opaque);
JS_EXTERN void JS_SetInterruptHandler(JSRuntime *rt, JSInterruptHandler *cb, void *opaque);
+/* Calls `visit` for each frame of the running stack, innermost first, at most `max_frames`: the
+ function's name ("" when it has none; valid only during the call) and the line its definition
+ starts on (-1 for native functions). Allocates nothing and runs no JavaScript, so an interrupt
+ handler may call it to sample the stack. Returns the frames visited. */
+typedef void JSStackSampleFunc(void *opaque, const char *name, int line);
+JS_EXTERN int JS_SampleStack(JSRuntime *rt, JSStackSampleFunc *visit, void *opaque, int max_frames);
/* if can_block is true, Atomics.wait() can be used */
JS_EXTERN void JS_SetCanBlock(JSRuntime *rt, bool can_block);
/* set the [IsHTMLDDA] internal slot */