Skip to content

Commit 8eabf52

Browse files
committed
src,lib: support path and encoding in v8.writeHeapSnapshot output
Fixes: #58857 Signed-off-by: Juan José Arboleda <[email protected]>
1 parent 607a741 commit 8eabf52

File tree

6 files changed

+99
-27
lines changed

6 files changed

+99
-27
lines changed

doc/api/v8.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ disk unless [`v8.stopCoverage()`][] is invoked before the process exits.
511511
<!-- YAML
512512
added: v11.13.0
513513
changes:
514+
- version: REPLACEME
515+
pr-url: https://github.com/nodejs/node/pull/58959
516+
description: Support path and encoding options.
514517
- version: v19.1.0
515518
pr-url: https://github.com/nodejs/node/pull/44989
516519
description: Support options to configure the heap snapshot.
@@ -533,6 +536,9 @@ changes:
533536
**Default:** `false`.
534537
* `exposeNumericValues` {boolean} If true, expose numeric values in
535538
artificial fields. **Default:** `false`.
539+
* `path` {string|Buffer|URL} If provided, specifies the file system location
540+
where the snapshot will be written. **Default:** `undefined`
541+
* `encoding` {string|null} **Default:** `'utf8'`
536542
* Returns: {string} The filename where the snapshot was saved.
537543

538544
Generates a snapshot of the current V8 heap and writes it to a JSON

lib/v8.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,23 @@ const { getOptionValue } = require('internal/options');
7878
* @param {string} [filename]
7979
* @param {{
8080
* exposeInternals?: boolean,
81-
* exposeNumericValues?: boolean
81+
* exposeNumericValues?: boolean,
82+
* path?: string,
83+
* encoding?: string
8284
* }} [options]
8385
* @returns {string}
8486
*/
8587
function writeHeapSnapshot(filename, options) {
8688
if (filename !== undefined) {
8789
filename = getValidatedPath(filename);
8890
}
91+
92+
if (options?.path != undefined) {
93+
options.path = getValidatedPath(options.path);
94+
}
95+
8996
const optionArray = getHeapSnapshotOptions(options);
90-
return triggerHeapSnapshot(filename, optionArray);
97+
return triggerHeapSnapshot(filename, optionArray, options?.path, options?.encoding);
9198
}
9299

93100
/**

src/heap_utils.cc

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "path.h"
66
#include "permission/permission.h"
77
#include "stream_base-inl.h"
8+
#include "string_bytes.h"
89
#include "util-inl.h"
910

1011
// Copied from https://github.com/nodejs/node/blob/b07dc4d19fdbc15b4f76557dc45b3ce3a43ad0c3/src/util.cc#L36-L41.
@@ -448,30 +449,60 @@ void CreateHeapSnapshotStream(const FunctionCallbackInfo<Value>& args) {
448449
void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
449450
Environment* env = Environment::GetCurrent(args);
450451
Isolate* isolate = args.GetIsolate();
451-
CHECK_EQ(args.Length(), 2);
452+
CHECK_EQ(args.Length(), 4);
452453
Local<Value> filename_v = args[0];
453454
auto options = GetHeapSnapshotOptions(args[1]);
455+
bool has_path = !args[2]->IsUndefined() && !args[2]->IsNull();
456+
std::string final_filename;
457+
BufferValue path_v(isolate, args[2]);
458+
ToNamespacedPath(env, &path_v);
459+
460+
const enum encoding encoding = ParseEncoding(isolate, args[3], UTF8);
454461

455462
if (filename_v->IsUndefined()) {
456463
DiagnosticFilename name(env, "Heap", "heapsnapshot");
457-
THROW_IF_INSUFFICIENT_PERMISSIONS(
458-
env,
459-
permission::PermissionScope::kFileSystemWrite,
460-
Environment::GetCwd(env->exec_path()));
461-
if (WriteSnapshot(env, *name, options).IsNothing()) return;
462-
if (String::NewFromUtf8(isolate, *name).ToLocal(&filename_v)) {
463-
args.GetReturnValue().Set(filename_v);
464+
if (!has_path) {
465+
THROW_IF_INSUFFICIENT_PERMISSIONS(
466+
env,
467+
permission::PermissionScope::kFileSystemWrite,
468+
Environment::GetCwd(env->exec_path()));
469+
final_filename = *name;
470+
} else {
471+
THROW_IF_INSUFFICIENT_PERMISSIONS(
472+
env,
473+
permission::PermissionScope::kFileSystemWrite,
474+
path_v.ToStringView());
475+
final_filename = path_v.ToString() + "/" +
476+
*name; // NOLINT(readability/pointer_notation)
477+
}
478+
} else {
479+
BufferValue filename(isolate, filename_v);
480+
CHECK_NOT_NULL(*filename);
481+
482+
if (has_path) {
483+
final_filename = path_v.ToString() + "/" + filename.ToString();
484+
THROW_IF_INSUFFICIENT_PERMISSIONS(
485+
env, permission::PermissionScope::kFileSystemWrite, final_filename);
486+
} else {
487+
ToNamespacedPath(env, &filename);
488+
THROW_IF_INSUFFICIENT_PERMISSIONS(
489+
env,
490+
permission::PermissionScope::kFileSystemWrite,
491+
filename.ToStringView());
492+
final_filename = filename.ToString();
464493
}
494+
}
495+
496+
// If encoding fails, snapshot is not triggered.
497+
Local<Value> ret;
498+
if (!StringBytes::Encode(isolate, final_filename.c_str(), encoding)
499+
.ToLocal(&ret)) {
465500
return;
466501
}
467502

468-
BufferValue path(isolate, filename_v);
469-
CHECK_NOT_NULL(*path);
470-
ToNamespacedPath(env, &path);
471-
THROW_IF_INSUFFICIENT_PERMISSIONS(
472-
env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
473-
if (WriteSnapshot(env, *path, options).IsNothing()) return;
474-
return args.GetReturnValue().Set(filename_v);
503+
if (WriteSnapshot(env, final_filename.c_str(), options).IsNothing()) return;
504+
505+
args.GetReturnValue().Set(ret);
475506
}
476507

477508
void Initialize(Local<Object> target,

src/node_internals.h

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -447,16 +447,17 @@ namespace heap {
447447
v8::Maybe<void> WriteSnapshot(Environment* env,
448448
const char* filename,
449449
v8::HeapProfiler::HeapSnapshotOptions options);
450-
}
451-
452-
namespace heap {
453450

454451
void DeleteHeapSnapshot(const v8::HeapSnapshot* snapshot);
455452
using HeapSnapshotPointer =
456453
DeleteFnPtr<const v8::HeapSnapshot, DeleteHeapSnapshot>;
457454

458455
BaseObjectPtr<AsyncWrap> CreateHeapSnapshotStream(
459456
Environment* env, HeapSnapshotPointer&& snapshot);
457+
458+
v8::HeapProfiler::HeapSnapshotOptions GetHeapSnapshotOptions(
459+
v8::Local<v8::Value> options);
460+
460461
} // namespace heap
461462

462463
node_module napi_module_to_node_module(const napi_module* mod);
@@ -485,11 +486,6 @@ std::ostream& operator<<(std::ostream& output,
485486

486487
bool linux_at_secure();
487488

488-
namespace heap {
489-
v8::HeapProfiler::HeapSnapshotOptions GetHeapSnapshotOptions(
490-
v8::Local<v8::Value> options);
491-
} // namespace heap
492-
493489
enum encoding ParseEncoding(v8::Isolate* isolate,
494490
v8::Local<v8::Value> encoding_v,
495491
v8::Local<v8::Value> encoding_id,

test/common/heap.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,18 @@ function getHeapSnapshotOptionTests() {
316316
],
317317
}],
318318
},
319+
// This is the same as the previous case, but with a path option.
320+
// The path option will be mutated by the test
321+
// test-write-heapsnapshot-options.js
322+
// Refs: https://github.com/nodejs/node/issues/58857
323+
{
324+
options: { exposeNumericValues: true, path: 'TBD' },
325+
expected: [{
326+
children: [
327+
{ edge_name: 'numeric', node_name: 'smi number' },
328+
],
329+
}],
330+
},
319331
];
320332
return {
321333
fixtures: fixtures.path('klass-with-fields.js'),

test/sequential/test-write-heapsnapshot-options.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
require('../common');
66

7+
const assert = require('assert');
78
const fs = require('fs');
9+
const tmpdir = require('../common/tmpdir');
810
const { getHeapSnapshotOptionTests, recordState } = require('../common/heap');
911

1012
class ReadStream {
@@ -20,7 +22,12 @@ if (process.argv[2] === 'child') {
2022
const { writeHeapSnapshot } = require('v8');
2123
require(tests.fixtures);
2224
const { options, expected } = tests.cases[parseInt(process.argv[3])];
25+
const { path } = options;
26+
// If path is set, writeHeapSnapshot will write to the tmpdir.
27+
if (path) options.path = tmpdir.path;
2328
const filename = writeHeapSnapshot(undefined, options);
29+
// If path is set, the filename will be the provided path.
30+
if (path) assert(filename.includes(tmpdir.path));
2431
const snapshot = recordState(new ReadStream(filename));
2532
console.log('Snapshot nodes', snapshot.snapshot.length);
2633
console.log('Searching for', expected[0].children);
@@ -30,8 +37,6 @@ if (process.argv[2] === 'child') {
3037
}
3138

3239
const { spawnSync } = require('child_process');
33-
const assert = require('assert');
34-
const tmpdir = require('../common/tmpdir');
3540
tmpdir.refresh();
3641

3742
// Start child processes to prevent the heap from growing too big.
@@ -48,3 +53,18 @@ for (let i = 0; i < tests.cases.length; ++i) {
4853
console.log('[STDOUT]', stdout);
4954
assert.strictEqual(child.status, 0);
5055
}
56+
57+
{
58+
// Don't need to spawn child process for this test, it will fail inmediately.
59+
// Refs: https://github.com/nodejs/node/issues/58857
60+
for (const pathName of [null, true, false, {}, [], () => {}, 1, 0, NaN]) {
61+
assert.throws(() => {
62+
require('v8').writeHeapSnapshot(undefined, {
63+
path: pathName,
64+
});
65+
}, {
66+
code: 'ERR_INVALID_ARG_TYPE',
67+
name: 'TypeError',
68+
});
69+
}
70+
}

0 commit comments

Comments
 (0)