GeezerVison is showing github.com — lib: add util.getCallSite() API · nodejs/node@a37d805
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +65Lines changed: 65 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
Collapse file Copy file name to clipboardExpand all lines: doc/api/util.md +57Lines changed: 57 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -364,6 +364,63 @@ util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 }); | 364 | 364 | // when printed to a terminal. | 365 | 365 | ``` | 366 | 366 |
| | 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 | + | 367 | 424 | ## `util.getSystemErrorName(err)` | 368 | 425 |
| 369 | 426 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: lib/util.js +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -461,6 +461,17 @@ function parseEnv(content) { | 461 | 461 | return binding.parseEnv(content); | 462 | 462 | } | 463 | 463 |
| | 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 | + | 464 | 475 | // Keep the `exports =` so that various functions can still be monkeypatched | 465 | 476 | module.exports = { | 466 | 477 | _errnoException, | @@ -475,6 +486,7 @@ module.exports = { | 475 | 486 | format, | 476 | 487 | styleText, | 477 | 488 | formatWithOptions, | | 489 | + getCallSite, | 478 | 490 | getSystemErrorMap, | 479 | 491 | getSystemErrorName, | 480 | 492 | inherits, | |
Collapse file Copy file name to clipboardExpand all lines: src/env_properties.h +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 98 | 98 | "transferList") \ | 99 | 99 | V(clone_untransferable_str, "Found invalid value in transferList.") \ | 100 | 100 | V(code_string, "code") \ | | 101 | + V(column_string, "column") \ | 101 | 102 | V(commonjs_string, "commonjs") \ | 102 | 103 | V(config_string, "config") \ | 103 | 104 | V(constants_string, "constants") \ | | 166 | 167 | V(fragment_string, "fragment") \ | 167 | 168 | V(frames_received_string, "framesReceived") \ | 168 | 169 | V(frames_sent_string, "framesSent") \ | | 170 | + V(function_name_string, "functionName") \ | 169 | 171 | V(function_string, "function") \ | 170 | 172 | V(get_string, "get") \ | 171 | 173 | V(get_data_clone_error_string, "_getDataCloneError") \ | | 215 | 217 | V(kind_string, "kind") \ | 216 | 218 | V(length_string, "length") \ | 217 | 219 | V(library_string, "library") \ | | 220 | + V(line_number_string, "lineNumber") \ | 218 | 221 | V(loop_count, "loopCount") \ | 219 | 222 | V(mac_string, "mac") \ | 220 | 223 | V(max_buffer_string, "maxBuffer") \ | | 305 | 308 | V(salt_length_string, "saltLength") \ | 306 | 309 | V(scheme_string, "scheme") \ | 307 | 310 | V(scopeid_string, "scopeid") \ | | 311 | + V(script_name_string, "scriptName") \ | 308 | 312 | V(serial_number_string, "serialNumber") \ | 309 | 313 | V(serial_string, "serial") \ | 310 | 314 | V(servername_string, "servername") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/node_util.cc +50Lines changed: 50 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -22,6 +22,7 @@ using v8::Integer; | 22 | 22 | using v8::Isolate; | 23 | 23 | using v8::KeyCollectionMode; | 24 | 24 | using v8::Local; | | 25 | +using v8::LocalVector; | 25 | 26 | using v8::Object; | 26 | 27 | using v8::ObjectTemplate; | 27 | 28 | using v8::ONLY_CONFIGURABLE; | @@ -254,12 +255,60 @@ static void ParseEnv(const FunctionCallbackInfo<Value>& args) { | 254 | 255 | args.GetReturnValue().Set(dotenv.ToObject(env)); | 255 | 256 | } | 256 | 257 |
| | 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 | + | 257 | 305 | void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 258 | 306 | registry->Register(GetPromiseDetails); | 259 | 307 | registry->Register(GetProxyDetails); | 260 | 308 | registry->Register(GetCallerLocation); | 261 | 309 | registry->Register(IsArrayBufferDetached); | 262 | 310 | registry->Register(PreviewEntries); | | 311 | + registry->Register(GetCallSite); | 263 | 312 | registry->Register(GetOwnNonIndexProperties); | 264 | 313 | registry->Register(GetConstructorName); | 265 | 314 | registry->Register(GetExternalValue); | @@ -365,6 +414,7 @@ void Initialize(Local<Object> target, | 365 | 414 | SetMethodNoSideEffect( | 366 | 415 | context, target, "getConstructorName", GetConstructorName); | 367 | 416 | SetMethodNoSideEffect(context, target, "getExternalValue", GetExternalValue); | | 417 | + SetMethodNoSideEffect(context, target, "getCallSite", GetCallSite); | 368 | 418 | SetMethod(context, target, "sleep", Sleep); | 369 | 419 | SetMethod(context, target, "parseEnv", ParseEnv); | 370 | 420 |
| |
Collapse file Copy file name to clipboard +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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); |
Collapse file Copy file name to clipboard +106Lines changed: 106 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|