Skip to content

Commit a37d805

Browse files
RafaelGSSaduh95
authored andcommitted
lib: add util.getCallSite() API
PR-URL: #54380 Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Claudio Wunder <cwunder@gnome.org> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 62b0007 commit a37d805

File tree

7 files changed

+298
-0
lines changed

7 files changed

+298
-0
lines changed

benchmark/util/get-callsite.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { getCallSite } = require('node:util');
5+
const assert = require('node:assert');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e6],
9+
method: ['ErrorCallSite', 'ErrorCallSiteSerialized', 'CPP'],
10+
});
11+
12+
function ErrorGetCallSite() {
13+
const originalStackFormatter = Error.prepareStackTrace;
14+
Error.prepareStackTrace = (_err, stack) => {
15+
if (stack && stack.length > 1) {
16+
// Remove node:util
17+
return stack.slice(1);
18+
}
19+
return stack;
20+
};
21+
const err = new Error();
22+
// With the V8 Error API, the stack is not formatted until it is accessed
23+
err.stack; // eslint-disable-line no-unused-expressions
24+
Error.prepareStackTrace = originalStackFormatter;
25+
return err.stack;
26+
}
27+
28+
function ErrorCallSiteSerialized() {
29+
const callsite = ErrorGetCallSite();
30+
const serialized = [];
31+
for (let i = 0; i < callsite.length; ++i) {
32+
serialized.push({
33+
functionName: callsite[i].getFunctionName(),
34+
scriptName: callsite[i].getFileName(),
35+
lineNumber: callsite[i].getLineNumber(),
36+
column: callsite[i].getColumnNumber(),
37+
});
38+
}
39+
return serialized;
40+
}
41+
42+
function main({ n, method }) {
43+
let fn;
44+
switch (method) {
45+
case 'ErrorCallSite':
46+
fn = ErrorGetCallSite;
47+
break;
48+
case 'ErrorCallSiteSerialized':
49+
fn = ErrorCallSiteSerialized;
50+
break;
51+
case 'CPP':
52+
fn = getCallSite;
53+
break;
54+
}
55+
let lastStack = {};
56+
57+
bench.start();
58+
for (let i = 0; i < n; i++) {
59+
const stack = fn();
60+
lastStack = stack;
61+
}
62+
bench.end(n);
63+
// Attempt to avoid dead-code elimination
64+
assert.ok(lastStack);
65+
}

doc/api/util.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,63 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
364364
// when printed to a terminal.
365365
```
366366

367+
## `util.getCallSite(frames)`
368+
369+
> Stability: 1.1 - Active development
370+
371+
<!-- YAML
372+
added: REPLACEME
373+
-->
374+
375+
* `frames` {number} Number of frames returned in the stacktrace.
376+
**Default:** `10`. Allowable range is between 1 and 200.
377+
* Returns: {Object\[]} An array of stacktrace objects
378+
* `functionName` {string} Returns the name of the function associated with this stack frame.
379+
* `scriptName` {string} Returns the name of the resource that contains the script for the
380+
function for this StackFrame.
381+
* `lineNumber` {number} Returns the number, 1-based, of the line for the associate function call.
382+
* `column` {number} Returns the 1-based column offset on the line for the associated function call.
383+
384+
Returns an array of stacktrace objects containing the stack of
385+
the caller function.
386+
387+
```js
388+
const util = require('node:util');
389+
390+
function exampleFunction() {
391+
const callSites = util.getCallSite();
392+
393+
console.log('Call Sites:');
394+
callSites.forEach((callSite, index) => {
395+
console.log(`CallSite ${index + 1}:`);
396+
console.log(`Function Name: ${callSite.functionName}`);
397+
console.log(`Script Name: ${callSite.scriptName}`);
398+
console.log(`Line Number: ${callSite.lineNumer}`);
399+
console.log(`Column Number: ${callSite.column}`);
400+
});
401+
// CallSite 1:
402+
// Function Name: exampleFunction
403+
// Script Name: /home/example.js
404+
// Line Number: 5
405+
// Column Number: 26
406+
407+
// CallSite 2:
408+
// Function Name: anotherFunction
409+
// Script Name: /home/example.js
410+
// Line Number: 22
411+
// Column Number: 3
412+
413+
// ...
414+
}
415+
416+
// A function to simulate another stack layer
417+
function anotherFunction() {
418+
exampleFunction();
419+
}
420+
421+
anotherFunction();
422+
```
423+
367424
## `util.getSystemErrorName(err)`
368425

369426
<!-- YAML

lib/util.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,17 @@ function parseEnv(content) {
461461
return binding.parseEnv(content);
462462
}
463463

464+
/**
465+
* Returns the callSite
466+
* @param {number} frames
467+
* @returns {object}
468+
*/
469+
function getCallSite(frames = 10) {
470+
// Using kDefaultMaxCallStackSizeToCapture as reference
471+
validateNumber(frames, 'frames', 1, 200);
472+
return binding.getCallSite(frames);
473+
};
474+
464475
// Keep the `exports =` so that various functions can still be monkeypatched
465476
module.exports = {
466477
_errnoException,
@@ -475,6 +486,7 @@ module.exports = {
475486
format,
476487
styleText,
477488
formatWithOptions,
489+
getCallSite,
478490
getSystemErrorMap,
479491
getSystemErrorName,
480492
inherits,

src/env_properties.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"transferList") \
9999
V(clone_untransferable_str, "Found invalid value in transferList.") \
100100
V(code_string, "code") \
101+
V(column_string, "column") \
101102
V(commonjs_string, "commonjs") \
102103
V(config_string, "config") \
103104
V(constants_string, "constants") \
@@ -166,6 +167,7 @@
166167
V(fragment_string, "fragment") \
167168
V(frames_received_string, "framesReceived") \
168169
V(frames_sent_string, "framesSent") \
170+
V(function_name_string, "functionName") \
169171
V(function_string, "function") \
170172
V(get_string, "get") \
171173
V(get_data_clone_error_string, "_getDataCloneError") \
@@ -215,6 +217,7 @@
215217
V(kind_string, "kind") \
216218
V(length_string, "length") \
217219
V(library_string, "library") \
220+
V(line_number_string, "lineNumber") \
218221
V(loop_count, "loopCount") \
219222
V(mac_string, "mac") \
220223
V(max_buffer_string, "maxBuffer") \
@@ -305,6 +308,7 @@
305308
V(salt_length_string, "saltLength") \
306309
V(scheme_string, "scheme") \
307310
V(scopeid_string, "scopeid") \
311+
V(script_name_string, "scriptName") \
308312
V(serial_number_string, "serialNumber") \
309313
V(serial_string, "serial") \
310314
V(servername_string, "servername") \

src/node_util.cc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ using v8::Integer;
2222
using v8::Isolate;
2323
using v8::KeyCollectionMode;
2424
using v8::Local;
25+
using v8::LocalVector;
2526
using v8::Object;
2627
using v8::ObjectTemplate;
2728
using v8::ONLY_CONFIGURABLE;
@@ -254,12 +255,60 @@ static void ParseEnv(const FunctionCallbackInfo<Value>& args) {
254255
args.GetReturnValue().Set(dotenv.ToObject(env));
255256
}
256257

258+
static void GetCallSite(const FunctionCallbackInfo<Value>& args) {
259+
Environment* env = Environment::GetCurrent(args);
260+
Isolate* isolate = env->isolate();
261+
262+
CHECK_EQ(args.Length(), 1);
263+
CHECK(args[0]->IsNumber());
264+
const uint32_t frames = args[0].As<Uint32>()->Value();
265+
DCHECK(frames >= 1 && frames <= 200);
266+
267+
// +1 for disregarding node:util
268+
Local<StackTrace> stack = StackTrace::CurrentStackTrace(isolate, frames + 1);
269+
const int frame_count = stack->GetFrameCount();
270+
LocalVector<Value> callsite_objects(isolate);
271+
272+
// Frame 0 is node:util. It should be skipped.
273+
for (int i = 1; i < frame_count; ++i) {
274+
Local<Object> obj = Object::New(isolate);
275+
Local<StackFrame> stack_frame = stack->GetFrame(isolate, i);
276+
277+
Utf8Value function_name(isolate, stack_frame->GetFunctionName());
278+
Utf8Value script_name(isolate, stack_frame->GetScriptName());
279+
280+
obj->Set(env->context(),
281+
env->function_name_string(),
282+
String::NewFromUtf8(isolate, *function_name).ToLocalChecked())
283+
.Check();
284+
obj->Set(env->context(),
285+
env->script_name_string(),
286+
String::NewFromUtf8(isolate, *script_name).ToLocalChecked())
287+
.Check();
288+
obj->Set(env->context(),
289+
env->line_number_string(),
290+
Integer::NewFromUnsigned(isolate, stack_frame->GetLineNumber()))
291+
.Check();
292+
obj->Set(env->context(),
293+
env->column_string(),
294+
Integer::NewFromUnsigned(isolate, stack_frame->GetColumn()))
295+
.Check();
296+
297+
callsite_objects.push_back(obj);
298+
}
299+
300+
Local<Array> callsites =
301+
Array::New(isolate, callsite_objects.data(), callsite_objects.size());
302+
args.GetReturnValue().Set(callsites);
303+
}
304+
257305
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
258306
registry->Register(GetPromiseDetails);
259307
registry->Register(GetProxyDetails);
260308
registry->Register(GetCallerLocation);
261309
registry->Register(IsArrayBufferDetached);
262310
registry->Register(PreviewEntries);
311+
registry->Register(GetCallSite);
263312
registry->Register(GetOwnNonIndexProperties);
264313
registry->Register(GetConstructorName);
265314
registry->Register(GetExternalValue);
@@ -365,6 +414,7 @@ void Initialize(Local<Object> target,
365414
SetMethodNoSideEffect(
366415
context, target, "getConstructorName", GetConstructorName);
367416
SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue);
417+
SetMethodNoSideEffect(context, target, "getCallSite", GetCallSite);
368418
SetMethod(context, target, "sleep", Sleep);
369419
SetMethod(context, target, "parseEnv", ParseEnv);
370420

test/fixtures/get-call-site.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const util = require('node:util');
2+
const assert = require('node:assert');
3+
assert.ok(util.getCallSite().length > 1);
4+
process.stdout.write(util.getCallSite()[0].scriptName);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const fixtures = require('../common/fixtures');
6+
const file = fixtures.path('get-call-site.js');
7+
8+
const { getCallSite } = require('node:util');
9+
const { spawnSync } = require('node:child_process');
10+
const assert = require('node:assert');
11+
12+
{
13+
const callsite = getCallSite();
14+
assert.ok(callsite.length > 1);
15+
assert.match(
16+
callsite[0].scriptName,
17+
/test-util-getCallSite/,
18+
'node:util should be ignored',
19+
);
20+
}
21+
22+
{
23+
const callsite = getCallSite(3);
24+
assert.strictEqual(callsite.length, 3);
25+
assert.match(
26+
callsite[0].scriptName,
27+
/test-util-getCallSite/,
28+
'node:util should be ignored',
29+
);
30+
}
31+
32+
// Guarantee dot-left numbers are ignored
33+
{
34+
const callsite = getCallSite(3.6);
35+
assert.strictEqual(callsite.length, 3);
36+
}
37+
38+
{
39+
const callsite = getCallSite(3.4);
40+
assert.strictEqual(callsite.length, 3);
41+
}
42+
43+
{
44+
assert.throws(() => {
45+
// Max than kDefaultMaxCallStackSizeToCapture
46+
getCallSite(201);
47+
}, common.expectsError({
48+
code: 'ERR_OUT_OF_RANGE'
49+
}));
50+
assert.throws(() => {
51+
getCallSite(-1);
52+
}, common.expectsError({
53+
code: 'ERR_OUT_OF_RANGE'
54+
}));
55+
assert.throws(() => {
56+
getCallSite({});
57+
}, common.expectsError({
58+
code: 'ERR_INVALID_ARG_TYPE'
59+
}));
60+
}
61+
62+
{
63+
const callsite = getCallSite(1);
64+
assert.strictEqual(callsite.length, 1);
65+
assert.match(
66+
callsite[0].scriptName,
67+
/test-util-getCallSite/,
68+
'node:util should be ignored',
69+
);
70+
}
71+
72+
// Guarantee [eval] will appear on stacktraces when using -e
73+
{
74+
const { status, stderr, stdout } = spawnSync(
75+
process.execPath,
76+
[
77+
'-e',
78+
`const util = require('util');
79+
const assert = require('assert');
80+
assert.ok(util.getCallSite().length > 1);
81+
process.stdout.write(util.getCallSite()[0].scriptName);
82+
`,
83+
],
84+
);
85+
assert.strictEqual(status, 0, stderr.toString());
86+
assert.strictEqual(stdout.toString(), '[eval]');
87+
}
88+
89+
// Guarantee the stacktrace[0] is the filename
90+
{
91+
const { status, stderr, stdout } = spawnSync(
92+
process.execPath,
93+
[file],
94+
);
95+
assert.strictEqual(status, 0, stderr.toString());
96+
assert.strictEqual(stdout.toString(), file);
97+
}
98+
99+
// Error.stackTraceLimit should not influence callsite size
100+
{
101+
const originalStackTraceLimit = Error.stackTraceLimit;
102+
Error.stackTraceLimit = 0;
103+
const callsite = getCallSite();
104+
assert.notStrictEqual(callsite.length, 0);
105+
Error.stackTraceLimit = originalStackTraceLimit;
106+
}

0 commit comments

Comments
 (0)