Skip to content

lib: add util.getCallSite() API #54380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions benchmark/util/get-callsite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const common = require('../common');
const { getCallSite } = require('node:util');
const assert = require('node:assert');

const bench = common.createBenchmark(main, {
n: [1e6],
method: ['ErrorCallSite', 'ErrorCallSiteSerialized', 'CPP'],
});

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 = fn();
lastStack = stack;
}
bench.end(n);
// Attempt to avoid dead-code elimination
assert.ok(lastStack);
}
57 changes: 57 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,63 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// when printed to a terminal.
```

## `util.getCallSite(frames)`

> Stability: 1.1 - Active development

<!-- YAML
added: REPLACEME
-->

* `frames` {number} Number of frames returned in the stacktrace.
**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
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
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)`

<!-- YAML
Expand Down
12 changes: 12 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,17 @@ function parseEnv(content) {
return binding.parseEnv(content);
}

/**
* Returns the callSite
* @param {number} frames
* @returns {object}
*/
function getCallSite(frames = 10) {
// Using kDefaultMaxCallStackSizeToCapture as reference
validateNumber(frames, 'frames', 1, 200);
return binding.getCallSite(frames);
};

// Keep the `exports =` so that various functions can still be monkeypatched
module.exports = {
_errnoException,
Expand All @@ -329,6 +340,7 @@ module.exports = {
format,
styleText,
formatWithOptions,
getCallSite,
getSystemErrorMap,
getSystemErrorName,
inherits,
Expand Down
4 changes: 4 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"transferList") \
V(clone_untransferable_str, "Found invalid value in transferList.") \
V(code_string, "code") \
V(column_string, "column") \
V(commonjs_string, "commonjs") \
V(config_string, "config") \
V(constants_string, "constants") \
Expand Down Expand Up @@ -166,6 +167,7 @@
V(fragment_string, "fragment") \
V(frames_received_string, "framesReceived") \
V(frames_sent_string, "framesSent") \
V(function_name_string, "functionName") \
V(function_string, "function") \
V(get_string, "get") \
V(get_data_clone_error_string, "_getDataCloneError") \
Expand Down Expand Up @@ -215,6 +217,7 @@
V(kind_string, "kind") \
V(length_string, "length") \
V(library_string, "library") \
V(line_number_string, "lineNumber") \
V(loop_count, "loopCount") \
V(mac_string, "mac") \
V(max_buffer_string, "maxBuffer") \
Expand Down Expand Up @@ -305,6 +308,7 @@
V(salt_length_string, "saltLength") \
V(scheme_string, "scheme") \
V(scopeid_string, "scopeid") \
V(script_name_string, "scriptName") \
V(serial_number_string, "serialNumber") \
V(serial_string, "serial") \
V(servername_string, "servername") \
Expand Down
50 changes: 50 additions & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,12 +255,60 @@ static void ParseEnv(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(dotenv.ToObject(env));
}

static void GetCallSite(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsNumber());
const uint32_t frames = args[0].As<Uint32>()->Value();
DCHECK(frames >= 1 && frames <= 200);

// +1 for disregarding node:util
Local<StackTrace> stack = StackTrace::CurrentStackTrace(isolate, frames + 1);
const int frame_count = stack->GetFrameCount();
LocalVector<Value> callsite_objects(isolate);

// Frame 0 is node:util. It should be skipped.
for (int i = 1; i < frame_count; ++i) {
Local<Object> obj = Object::New(isolate);
Local<StackFrame> stack_frame = stack->GetFrame(isolate, i);

Utf8Value function_name(isolate, stack_frame->GetFunctionName());
Utf8Value script_name(isolate, stack_frame->GetScriptName());

obj->Set(env->context(),
env->function_name_string(),
String::NewFromUtf8(isolate, *function_name).ToLocalChecked())
.Check();
obj->Set(env->context(),
env->script_name_string(),
String::NewFromUtf8(isolate, *script_name).ToLocalChecked())
.Check();
obj->Set(env->context(),
env->line_number_string(),
Integer::NewFromUnsigned(isolate, stack_frame->GetLineNumber()))
.Check();
obj->Set(env->context(),
env->column_string(),
Integer::NewFromUnsigned(isolate, stack_frame->GetColumn()))
.Check();

callsite_objects.push_back(obj);
}

Local<Array> callsites =
Array::New(isolate, callsite_objects.data(), callsite_objects.size());
args.GetReturnValue().Set(callsites);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetPromiseDetails);
registry->Register(GetProxyDetails);
registry->Register(GetCallerLocation);
registry->Register(IsArrayBufferDetached);
registry->Register(PreviewEntries);
registry->Register(GetCallSite);
registry->Register(GetOwnNonIndexProperties);
registry->Register(GetConstructorName);
registry->Register(GetExternalValue);
Expand Down Expand Up @@ -365,6 +414,7 @@ void Initialize(Local<Object> target,
SetMethodNoSideEffect(
context, target, "getConstructorName", GetConstructorName);
SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue);
SetMethodNoSideEffect(context, target, "getCallSite", GetCallSite);
SetMethod(context, target, "sleep", Sleep);
SetMethod(context, target, "parseEnv", ParseEnv);

Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/get-call-site.js
Original file line number Diff line number Diff line change
@@ -0,0 +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);
106 changes: 106 additions & 0 deletions test/parallel/test-util-getCallSite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict';

const common = require('../common');

const fixtures = require('../common/fixtures');
const file = fixtures.path('get-call-site.js');

const { getCallSite } = require('node:util');
const { spawnSync } = require('node:child_process');
const assert = require('node:assert');

{
const callsite = getCallSite();
assert.ok(callsite.length > 1);
assert.match(
callsite[0].scriptName,
/test-util-getCallSite/,
'node:util should be ignored',
);
}

{
const callsite = getCallSite(3);
assert.strictEqual(callsite.length, 3);
assert.match(
callsite[0].scriptName,
/test-util-getCallSite/,
'node:util should be ignored',
);
}

// Guarantee dot-left numbers are ignored
{
const callsite = getCallSite(3.6);
assert.strictEqual(callsite.length, 3);
}

{
const callsite = getCallSite(3.4);
assert.strictEqual(callsite.length, 3);
}

{
assert.throws(() => {
// Max than kDefaultMaxCallStackSizeToCapture
getCallSite(201);
}, common.expectsError({
code: 'ERR_OUT_OF_RANGE'
}));
assert.throws(() => {
getCallSite(-1);
}, common.expectsError({
code: 'ERR_OUT_OF_RANGE'
}));
assert.throws(() => {
getCallSite({});
}, common.expectsError({
code: 'ERR_INVALID_ARG_TYPE'
}));
}

{
const callsite = getCallSite(1);
assert.strictEqual(callsite.length, 1);
assert.match(
callsite[0].scriptName,
/test-util-getCallSite/,
'node:util should be ignored',
);
}

// Guarantee [eval] will appear on stacktraces when using -e
{
const { status, stderr, stdout } = spawnSync(
process.execPath,
[
'-e',
`const util = require('util');
const assert = require('assert');
assert.ok(util.getCallSite().length > 1);
process.stdout.write(util.getCallSite()[0].scriptName);
`,
],
);
assert.strictEqual(status, 0, stderr.toString());
assert.strictEqual(stdout.toString(), '[eval]');
}

// Guarantee the stacktrace[0] is the filename
{
const { status, stderr, stdout } = spawnSync(
process.execPath,
[file],
);
assert.strictEqual(status, 0, stderr.toString());
assert.strictEqual(stdout.toString(), file);
}

// Error.stackTraceLimit should not influence callsite size
{
const originalStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
const callsite = getCallSite();
assert.notStrictEqual(callsite.length, 0);
Error.stackTraceLimit = originalStackTraceLimit;
}
Loading