From da4c560668898db047b7a2441377033303978d44 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 13 Aug 2024 21:37:09 -0300 Subject: [PATCH 1/9] lib: add util.getCallSite() API --- doc/api/util.md | 54 +++++++++++++++++++++++++ lib/util.js | 9 +++++ src/env_properties.h | 4 ++ src/node_util.cc | 42 ++++++++++++++++++++ test/fixtures/get-call-site.js | 4 ++ test/parallel/test-util-getCallSite.js | 55 ++++++++++++++++++++++++++ tools/doc/type-parser.mjs | 2 + 7 files changed, 170 insertions(+) create mode 100644 test/fixtures/get-call-site.js create mode 100644 test/parallel/test-util-getCallSite.js diff --git a/doc/api/util.md b/doc/api/util.md index ee6a91af5159d4..618e3d5bf73652 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -364,6 +364,60 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); // when printed to a terminal. ``` +## `util.getCallSite()` + +> Stability: 1.1 - Active development + + + +* Returns: {Object\[]} An array of stacktrace objects + * `functionName` {string} Returns the name of the function associated with this stack frame. + * `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call. + * `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call. + * `column` {number} Returns the 1-based column offset on the line for the associated function call. + +Returns an array of stacktrace objects containing the stack of +the caller function. + +```js +const util = require('node:util'); + +function exampleFunction() { + const callSites = util.getCallSite(); + + console.log('Call Sites:'); + callSites.forEach((callSite, index) => { + console.log(`CallSite ${index + 1}:`); + console.log(`Function Name: ${callSite.functionName}`); + console.log(`Script Name: ${callSite.scriptName}`); + console.log(`Line Number: ${callSite.lineNumer}`); + console.log(`Column Number: ${callSite.column}`); + }); + // CallSite 1: + // Function Name: exampleFunction + // Script Name: /home/example.js + // Line Number: 5 + // Column Number: 26 + + // CallSite 2: + // Function Name: anotherFunction + // Script Name: /home/example.js + // Line Number: 22 + // Column Number: 3 + + // ... +} + +// A function to simulate another stack layer +function anotherFunction() { + exampleFunction(); +} + +anotherFunction(); +``` + ## `util.getSystemErrorName(err)` +* `frames` {number} Number of frames returned in the stacktrace. + **Default:** `10`. + * Returns: {Object\[]} An array of stacktrace objects * `functionName` {string} Returns the name of the function associated with this stack frame. - * `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call. - * `lineNumber` {string} Returns the number, 1-based, of the line for the associate function call. + * `scriptName` {string} Returns the name of the resource that contains the script for the + function for this StackFrame. + * `lineNumber` {number} Returns the number, 1-based, of the line for the associate function call. * `column` {number} Returns the 1-based column offset on the line for the associated function call. Returns an array of stacktrace objects containing the stack of From d3cd8841f09e1656892b34d7fcc922a8d896960a Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 20 Aug 2024 17:05:15 -0300 Subject: [PATCH 5/9] fixup! fixup! fixup! lib: add util.getCallSite() API --- lib/util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/util.js b/lib/util.js index 4cd2d468ca9263..9d80c4ad02e8c3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -317,6 +317,7 @@ function parseEnv(content) { /** * Returns the callSite + * @param {number} frames * @returns {object} */ function getCallSite(frames = 10) { From c73110b59414153fcc06b9f8c0e9b4d0e3da4063 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Wed, 21 Aug 2024 12:44:49 -0300 Subject: [PATCH 6/9] fixup! fixup! fixup! fixup! lib: add util.getCallSite() API --- benchmark/util/get-callsite.js | 52 +++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/benchmark/util/get-callsite.js b/benchmark/util/get-callsite.js index e4e219c2343ecc..9270f841a243d3 100644 --- a/benchmark/util/get-callsite.js +++ b/benchmark/util/get-callsite.js @@ -1,18 +1,62 @@ 'use strict'; const common = require('../common'); -const util = require('node:util'); +const { getCallSite } = require('node:util'); const assert = require('node:assert'); const bench = common.createBenchmark(main, { n: [1e6], + method: ['ErrorCallSite', 'ErrorCallSiteSerialized', 'CPP'], }); -function main({ n }) { - bench.start(); +function ErrorGetCallSite() { + const originalStackFormatter = Error.prepareStackTrace; + Error.prepareStackTrace = (_err, stack) => { + if (stack && stack.length > 1) { + // Remove node:util + return stack.slice(1); + } + return stack; + }; + const err = new Error(); + // With the V8 Error API, the stack is not formatted until it is accessed + err.stack; // eslint-disable-line no-unused-expressions + Error.prepareStackTrace = originalStackFormatter; + return err.stack; +} + +function ErrorCallSiteSerialized() { + const callsite = ErrorGetCallSite(); + const serialized = []; + for (let i = 0; i < callsite.length; ++i) { + serialized.push({ + functionName: callsite[i].getFunctionName(), + scriptName: callsite[i].getFileName(), + lineNumber: callsite[i].getLineNumber(), + column: callsite[i].getColumnNumber(), + }); + } + return serialized; +} + +function main({ n, method }) { + let fn; + switch (method) { + case 'ErrorCallSite': + fn = ErrorGetCallSite; + break; + case 'ErrorCallSiteSerialized': + fn = ErrorCallSiteSerialized; + break; + case 'CPP': + fn = getCallSite; + break; + } let lastStack = {}; + + bench.start(); for (let i = 0; i < n; i++) { - const stack = util.getCallSite(); + const stack = fn(); lastStack = stack; } bench.end(n); From 7d49b664686baca6b29511e45e11995682b794f6 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 26 Aug 2024 11:53:54 -0300 Subject: [PATCH 7/9] fixup! lib: add util.getCallSite() API --- doc/api/util.md | 1 - src/node_util.cc | 16 +++++++++------- test/fixtures/get-call-site.js | 2 +- tools/doc/type-parser.mjs | 2 -- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 363cbaed2bdb37..665ecbabdc742d 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -374,7 +374,6 @@ added: REPLACEME * `frames` {number} Number of frames returned in the stacktrace. **Default:** `10`. - * Returns: {Object\[]} An array of stacktrace objects * `functionName` {string} Returns the name of the function associated with this stack frame. * `scriptName` {string} Returns the name of the resource that contains the script for the diff --git a/src/node_util.cc b/src/node_util.cc index 301c5ec80c2bb6..92a47defb0bc19 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -264,10 +264,11 @@ static void GetCallSite(const FunctionCallbackInfo& args) { // +1 for disregarding node:util Local stack = StackTrace::CurrentStackTrace(isolate, frames + 1); - Local callsites = Array::New(isolate); + const int frame_count = stack->GetFrameCount(); + std::vector> callsite_objects{}; // Frame 0 is node:util. It should be skipped. - for (int i = 1; i < stack->GetFrameCount(); ++i) { + for (int i = 1; i < frame_count; ++i) { Local obj = Object::New(isolate); Local stack_frame = stack->GetFrame(isolate, i); @@ -290,11 +291,12 @@ static void GetCallSite(const FunctionCallbackInfo& args) { env->column_string(), Integer::NewFromUnsigned(isolate, stack_frame->GetColumn())) .Check(); - if (callsites->Set(env->context(), callsites->Length(), obj).IsNothing()) { - args.GetReturnValue().Set(Array::New(isolate)); - return; - } + + callsite_objects.push_back(obj); } + + Local callsites = + Array::New(isolate, callsite_objects.data(), callsite_objects.size()); args.GetReturnValue().Set(callsites); } @@ -410,7 +412,7 @@ void Initialize(Local target, SetMethodNoSideEffect( context, target, "getConstructorName", GetConstructorName); SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue); - SetMethod(context, target, "getCallSite", GetCallSite); + SetMethodNoSideEffect(context, target, "getCallSite", GetCallSite); SetMethod(context, target, "sleep", Sleep); SetMethod(context, target, "parseEnv", ParseEnv); diff --git a/test/fixtures/get-call-site.js b/test/fixtures/get-call-site.js index d01a5283c97df5..bb4e0dce0a0985 100644 --- a/test/fixtures/get-call-site.js +++ b/test/fixtures/get-call-site.js @@ -1,4 +1,4 @@ const util = require('node:util'); const assert = require('node:assert'); assert.ok(util.getCallSite().length > 1); -process.stdout.write(util.getCallSite()[0].scriptName); \ No newline at end of file +process.stdout.write(util.getCallSite()[0].scriptName); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 1c1a0b6eda6176..6b94a94283ccb2 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -49,8 +49,6 @@ const customTypesMap = { 'worker_threads.html#class-broadcastchannel-' + 'extends-eventtarget', - 'CallSite': 'https://v8.dev/docs/stack-trace-api#customizing-stack-traces', - 'Iterable': `${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`, 'Iterator': From 36c2839db8fd711a30c7cf49af3cf29192893d51 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 26 Aug 2024 14:13:52 -0300 Subject: [PATCH 8/9] fixup! fixup! lib: add util.getCallSite() API --- src/node_util.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node_util.cc b/src/node_util.cc index 92a47defb0bc19..57c3026226a9b3 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -22,6 +22,7 @@ using v8::Integer; using v8::Isolate; using v8::KeyCollectionMode; using v8::Local; +using v8::LocalVector; using v8::Object; using v8::ObjectTemplate; using v8::ONLY_CONFIGURABLE; @@ -265,7 +266,7 @@ static void GetCallSite(const FunctionCallbackInfo& args) { // +1 for disregarding node:util Local stack = StackTrace::CurrentStackTrace(isolate, frames + 1); const int frame_count = stack->GetFrameCount(); - std::vector> callsite_objects{}; + LocalVector callsite_objects(isolate); // Frame 0 is node:util. It should be skipped. for (int i = 1; i < frame_count; ++i) { From aa11f566b9693a0c6c33fc907e1d6ab7748d2cdc Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 27 Aug 2024 13:33:14 -0300 Subject: [PATCH 9/9] fixup! apply cr suggestions --- doc/api/util.md | 2 +- src/node_util.cc | 1 + test/parallel/test-util-getCallSite.js | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 665ecbabdc742d..6c922bc0154343 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -373,7 +373,7 @@ added: REPLACEME --> * `frames` {number} Number of frames returned in the stacktrace. - **Default:** `10`. + **Default:** `10`. Allowable range is between 1 and 200. * Returns: {Object\[]} An array of stacktrace objects * `functionName` {string} Returns the name of the function associated with this stack frame. * `scriptName` {string} Returns the name of the resource that contains the script for the diff --git a/src/node_util.cc b/src/node_util.cc index 57c3026226a9b3..680fb10c0ec51d 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -262,6 +262,7 @@ static void GetCallSite(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsNumber()); const uint32_t frames = args[0].As()->Value(); + DCHECK(frames >= 1 && frames <= 200); // +1 for disregarding node:util Local stack = StackTrace::CurrentStackTrace(isolate, frames + 1); diff --git a/test/parallel/test-util-getCallSite.js b/test/parallel/test-util-getCallSite.js index 019165f1d344a9..ae862e2b278401 100644 --- a/test/parallel/test-util-getCallSite.js +++ b/test/parallel/test-util-getCallSite.js @@ -29,6 +29,7 @@ const assert = require('node:assert'); ); } +// Guarantee dot-left numbers are ignored { const callsite = getCallSite(3.6); assert.strictEqual(callsite.length, 3); @@ -68,6 +69,7 @@ const assert = require('node:assert'); ); } +// Guarantee [eval] will appear on stacktraces when using -e { const { status, stderr, stdout } = spawnSync( process.execPath, @@ -84,6 +86,7 @@ const assert = require('node:assert'); assert.strictEqual(stdout.toString(), '[eval]'); } +// Guarantee the stacktrace[0] is the filename { const { status, stderr, stdout } = spawnSync( process.execPath, @@ -93,11 +96,11 @@ const assert = require('node:assert'); assert.strictEqual(stdout.toString(), file); } +// Error.stackTraceLimit should not influence callsite size { const originalStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 0; const callsite = getCallSite(); - // Error.stackTraceLimit should not influence callsite size assert.notStrictEqual(callsite.length, 0); Error.stackTraceLimit = originalStackTraceLimit; }