From 9092e12b8235f7ebbdd3986af77bb3147b004393 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 7 Mar 2019 16:51:36 +0000 Subject: [PATCH] v8: integrate node-heapdump into core Adds `v8.writeHeapSnapshot(filename)` with impl adapted from the `node-heapdump` module. Also, adds a v8.getHeapSnapshot() alternative that returns a Readable Stream PR-URL: https://github.com/nodejs/node/pull/26501 Reviewed-By: Richard Lau Reviewed-By: Ben Noordhuis Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen Reviewed-By: Vse Mozhet Byt Reviewed-By: Sam Roberts Reviewed-By: Joyee Cheung --- LICENSE | 39 +++- doc/api/v8.md | 71 ++++++ lib/internal/test/heap.js | 11 +- lib/v8.js | 60 ++++- src/async_wrap.h | 1 + src/env.h | 1 + src/heap_utils.cc | 208 +++++++++++++++++- src/node_internals.h | 35 +++ src/util.cc | 65 ++++++ test/common/heap.js | 4 +- test/parallel/test-bootstrap-modules.js | 2 +- test/sequential/test-async-wrap-getasyncid.js | 6 + test/sequential/test-heapdump.js | 46 ++++ tools/license-builder.sh | 3 + 14 files changed, 537 insertions(+), 15 deletions(-) create mode 100644 test/sequential/test-heapdump.js diff --git a/LICENSE b/LICENSE index 2831dc3b5ee4f3..59f5c0e6ecd462 100644 --- a/LICENSE +++ b/LICENSE @@ -634,7 +634,7 @@ The externally maintained libraries used by Node.js are: - OpenSSL, located at deps/openssl, is licensed as follows: """ - Copyright (c) 1998-2018 The OpenSSL Project. All rights reserved. + Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -1445,3 +1445,40 @@ The externally maintained libraries used by Node.js are: ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + +- node-heapdump, located at src/heap_utils.cc, is licensed as follows: + """ + ISC License + + Copyright (c) 2012, Ben Noordhuis + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + === src/compat.h src/compat-inl.h === + + ISC License + + Copyright (c) 2014, StrongLoop Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + """ diff --git a/doc/api/v8.md b/doc/api/v8.md index 04866d92e39854..56e89a6cac0f4d 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -87,6 +87,24 @@ The value returned is an array of objects containing the following properties: ] ``` +## v8.getHeapSnapshot() + + +* Returns: {stream.Readable} A Readable Stream containing the V8 heap snapshot + +Generates a snapshot of the current V8 heap and returns a Readable +Stream that may be used to read the JSON serialized representation. +This JSON stream format is intended to be used with tools such as +Chrome DevTools. The JSON schema is undocumented and specific to the +V8 engine, and may change from one version of V8 to the next. + +```js +const stream = v8.getHeapSnapshot(); +stream.pipe(process.stdout); +``` + ## v8.getHeapStatistics() + +* `filename` {string} The file path where the V8 heap snapshot is to be + saved. If not specified, a file name with the pattern + `'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot'` will be + generated, where `{pid}` will be the PID of the Node.js process, + `{thread_id}` will be `0` when `writeHeapSnapshot()` is called from + the main Node.js thread or the id of a worker thread. +* Returns: {string} The filename where the snapshot was saved. + +Generates a snapshot of the current V8 heap and writes it to a JSON +file. This file is intended to be used with tools such as Chrome +DevTools. The JSON schema is undocumented and specific to the V8 +engine, and may change from one version of V8 to the next. + +A heap snapshot is specific to a single V8 isolate. When using +[Worker Threads][], a heap snapshot generated from the main thread will +not contain any information about the workers, and vice versa. + +```js +const { writeHeapSnapshot } = require('v8'); +const { + Worker, + isMainThread, + parentPort +} = require('worker_threads'); + +if (isMainThread) { + const worker = new Worker(__filename); + + worker.once('message', (filename) => { + console.log(`worker heapdump: ${filename}`); + // Now get a heapdump for the main thread. + console.log(`main thread heapdump: ${writeHeapSnapshot()}`); + }); + + // Tell the worker to create a heapdump. + worker.postMessage('heapdump'); +} else { + parentPort.once('message', (message) => { + if (message === 'heapdump') { + // Generate a heapdump for the worker + // and return the filename to the parent. + parentPort.postMessage(writeHeapSnapshot()); + } + }); +} +``` + ## Serialization API > Stability: 1 - Experimental @@ -417,4 +487,5 @@ A subclass of [`Deserializer`][] corresponding to the format written by [`vm.Script`]: vm.html#vm_constructor_new_vm_script_code_options [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm [V8]: https://developers.google.com/v8/ +[Worker Threads]: worker_threads.html [here]: https://github.com/thlorenz/v8-flags/blob/master/flags-0.11.md diff --git a/lib/internal/test/heap.js b/lib/internal/test/heap.js index 52e00b9da5e136..6565e8fbd8ef13 100644 --- a/lib/internal/test/heap.js +++ b/lib/internal/test/heap.js @@ -4,14 +4,17 @@ process.emitWarning( 'These APIs are for internal testing only. Do not use them.', 'internal/test/heap'); -const { createHeapDump, buildEmbedderGraph } = internalBinding('heap_utils'); +const { + createHeapSnapshot, + buildEmbedderGraph +} = internalBinding('heap_utils'); const assert = require('internal/assert'); // This is not suitable for production code. It creates a full V8 heap dump, // parses it as JSON, and then creates complex objects from it, leading // to significantly increased memory usage. -function createJSHeapDump() { - const dump = createHeapDump(); +function createJSHeapSnapshot() { + const dump = createHeapSnapshot(); const meta = dump.snapshot.meta; const nodes = @@ -81,6 +84,6 @@ function readHeapInfo(raw, fields, types, strings) { } module.exports = { - createJSHeapDump, + createJSHeapSnapshot, buildEmbedderGraph }; diff --git a/lib/v8.js b/lib/v8.js index 1cf80adfb78233..eb46cd4cc64592 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -20,9 +20,65 @@ const { Serializer: _Serializer, Deserializer: _Deserializer } = internalBinding('serdes'); +const assert = require('internal/assert'); const { copy } = internalBinding('buffer'); const { objectToString } = require('internal/util'); const { FastBuffer } = require('internal/buffer'); +const { toPathIfFileURL } = require('internal/url'); +const { validatePath } = require('internal/fs/utils'); +const { toNamespacedPath } = require('path'); +const { + createHeapSnapshotStream, + triggerHeapSnapshot +} = internalBinding('heap_utils'); +const { Readable } = require('stream'); +const { owner_symbol } = require('internal/async_hooks').symbols; +const { + kUpdateTimer, + onStreamRead, +} = require('internal/stream_base_commons'); +const kHandle = Symbol('kHandle'); + + +function writeHeapSnapshot(filename) { + if (filename !== undefined) { + filename = toPathIfFileURL(filename); + validatePath(filename); + filename = toNamespacedPath(filename); + } + return triggerHeapSnapshot(filename); +} + +class HeapSnapshotStream extends Readable { + constructor(handle) { + super({ autoDestroy: true }); + this[kHandle] = handle; + handle[owner_symbol] = this; + handle.onread = onStreamRead; + } + + _read() { + if (this[kHandle]) + this[kHandle].readStart(); + } + + _destroy() { + // Release the references on the handle so that + // it can be garbage collected. + this[kHandle][owner_symbol] = undefined; + this[kHandle] = undefined; + } + + [kUpdateTimer]() { + // Does nothing + } +} + +function getHeapSnapshot() { + const handle = createHeapSnapshotStream(); + assert(handle); + return new HeapSnapshotStream(handle); +} // Calling exposed c++ functions directly throws exception as it expected to be // called with new operator and caused an assert to fire. @@ -210,6 +266,7 @@ function deserialize(buffer) { module.exports = { cachedDataVersionTag, + getHeapSnapshot, getHeapStatistics, getHeapSpaceStatistics, setFlagsFromString, @@ -218,5 +275,6 @@ module.exports = { DefaultSerializer, DefaultDeserializer, deserialize, - serialize + serialize, + writeHeapSnapshot }; diff --git a/src/async_wrap.h b/src/async_wrap.h index 09319c11bbac52..1f1d19201bd61e 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -41,6 +41,7 @@ namespace node { V(FSREQPROMISE) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ + V(HEAPSNAPSHOT) \ V(HTTP2SESSION) \ V(HTTP2STREAM) \ V(HTTP2PING) \ diff --git a/src/env.h b/src/env.h index a43ca3034d31b8..9583586aac1163 100644 --- a/src/env.h +++ b/src/env.h @@ -379,6 +379,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(script_data_constructor_function, v8::Function) \ V(secure_context_constructor_template, v8::FunctionTemplate) \ V(shutdown_wrap_template, v8::ObjectTemplate) \ + V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ V(tcp_constructor_template, v8::FunctionTemplate) \ V(tick_callback_function, v8::Function) \ V(timers_callback_function, v8::Function) \ diff --git a/src/heap_utils.cc b/src/heap_utils.cc index e71a65e600631e..c54f0c41d7b09c 100644 --- a/src/heap_utils.cc +++ b/src/heap_utils.cc @@ -1,4 +1,5 @@ #include "env-inl.h" +#include "stream_base-inl.h" using v8::Array; using v8::Boolean; @@ -6,6 +7,7 @@ using v8::Context; using v8::EmbedderGraph; using v8::EscapableHandleScope; using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; using v8::HandleScope; using v8::HeapSnapshot; using v8::Isolate; @@ -14,6 +16,7 @@ using v8::Local; using v8::MaybeLocal; using v8::Number; using v8::Object; +using v8::ObjectTemplate; using v8::String; using v8::Value; @@ -231,12 +234,146 @@ class BufferOutputStream : public v8::OutputStream { std::unique_ptr buffer_; }; -void CreateHeapDump(const FunctionCallbackInfo& args) { +namespace { +class FileOutputStream : public v8::OutputStream { + public: + explicit FileOutputStream(FILE* stream) : stream_(stream) {} + + int GetChunkSize() override { + return 65536; // big chunks == faster + } + + void EndOfStream() override {} + + WriteResult WriteAsciiChunk(char* data, int size) override { + const size_t len = static_cast(size); + size_t off = 0; + + while (off < len && !feof(stream_) && !ferror(stream_)) + off += fwrite(data + off, 1, len - off, stream_); + + return off == len ? kContinue : kAbort; + } + + private: + FILE* stream_; +}; + +class HeapSnapshotStream : public AsyncWrap, + public StreamBase, + public v8::OutputStream { + public: + HeapSnapshotStream( + Environment* env, + const HeapSnapshot* snapshot, + v8::Local obj) : + AsyncWrap(env, obj, AsyncWrap::PROVIDER_HEAPSNAPSHOT), + StreamBase(env), + snapshot_(snapshot) { + MakeWeak(); + StreamBase::AttachToObject(GetObject()); + } + + ~HeapSnapshotStream() override { + Cleanup(); + } + + int GetChunkSize() override { + return 65536; // big chunks == faster + } + + void EndOfStream() override { + EmitRead(UV_EOF); + Cleanup(); + } + + WriteResult WriteAsciiChunk(char* data, int size) override { + int len = size; + while (len != 0) { + uv_buf_t buf = EmitAlloc(size); + ssize_t avail = len; + if (static_cast(buf.len) < avail) + avail = buf.len; + memcpy(buf.base, data, avail); + data += avail; + len -= avail; + EmitRead(size, buf); + } + return kContinue; + } + + int ReadStart() override { + CHECK_NE(snapshot_, nullptr); + snapshot_->Serialize(this, HeapSnapshot::kJSON); + return 0; + } + + int ReadStop() override { + return 0; + } + + int DoShutdown(ShutdownWrap* req_wrap) override { + UNREACHABLE(); + return 0; + } + + int DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) override { + UNREACHABLE(); + return 0; + } + + bool IsAlive() override { return snapshot_ != nullptr; } + bool IsClosing() override { return snapshot_ == nullptr; } + AsyncWrap* GetAsyncWrap() override { return this; } + + void MemoryInfo(MemoryTracker* tracker) const override { + if (snapshot_ != nullptr) { + tracker->TrackFieldWithSize( + "snapshot", sizeof(*snapshot_), "HeapSnapshot"); + } + } + + SET_MEMORY_INFO_NAME(HeapSnapshotStream) + SET_SELF_SIZE(HeapSnapshotStream) + + private: + void Cleanup() { + if (snapshot_ != nullptr) { + const_cast(snapshot_)->Delete(); + snapshot_ = nullptr; + } + } + + + const HeapSnapshot* snapshot_; +}; + +inline void TakeSnapshot(Isolate* isolate, v8::OutputStream* out) { + const HeapSnapshot* const snapshot = + isolate->GetHeapProfiler()->TakeHeapSnapshot(); + snapshot->Serialize(out, HeapSnapshot::kJSON); + const_cast(snapshot)->Delete(); +} + +inline bool WriteSnapshot(Isolate* isolate, const char* filename) { + FILE* fp = fopen(filename, "w"); + if (fp == nullptr) + return false; + FileOutputStream stream(fp); + TakeSnapshot(isolate, &stream); + fclose(fp); + return true; +} + +} // namespace + +void CreateHeapSnapshot(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); - const HeapSnapshot* snapshot = isolate->GetHeapProfiler()->TakeHeapSnapshot(); BufferOutputStream out; - snapshot->Serialize(&out, HeapSnapshot::kJSON); - const_cast(snapshot)->Delete(); + TakeSnapshot(isolate, &out); Local ret; if (JSON::Parse(isolate->GetCurrentContext(), out.ToString(isolate)).ToLocal(&ret)) { @@ -244,14 +381,73 @@ void CreateHeapDump(const FunctionCallbackInfo& args) { } } +void CreateHeapSnapshotStream(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HandleScope scope(env->isolate()); + const HeapSnapshot* const snapshot = + env->isolate()->GetHeapProfiler()->TakeHeapSnapshot(); + CHECK_NOT_NULL(snapshot); + Local obj; + if (!env->streambaseoutputstream_constructor_template() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return; + } + HeapSnapshotStream* out = new HeapSnapshotStream(env, snapshot, obj); + args.GetReturnValue().Set(out->object()); +} + +void TriggerHeapSnapshot(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = args.GetIsolate(); + + Local filename_v = args[0]; + + if (filename_v->IsUndefined()) { + DiagnosticFilename name(env, "Heap", "heapsnapshot"); + if (!WriteSnapshot(isolate, *name)) + return; + if (String::NewFromUtf8(isolate, *name, v8::NewStringType::kNormal) + .ToLocal(&filename_v)) { + args.GetReturnValue().Set(filename_v); + } + return; + } + + BufferValue path(isolate, filename_v); + CHECK_NOT_NULL(*path); + if (!WriteSnapshot(isolate, *path)) + return; + return args.GetReturnValue().Set(filename_v); +} + void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); - env->SetMethodNoSideEffect(target, "buildEmbedderGraph", BuildEmbedderGraph); - env->SetMethodNoSideEffect(target, "createHeapDump", CreateHeapDump); + env->SetMethodNoSideEffect(target, + "buildEmbedderGraph", + BuildEmbedderGraph); + env->SetMethodNoSideEffect(target, + "createHeapSnapshot", + CreateHeapSnapshot); + env->SetMethodNoSideEffect(target, + "triggerHeapSnapshot", + TriggerHeapSnapshot); + env->SetMethodNoSideEffect(target, + "createHeapSnapshotStream", + CreateHeapSnapshotStream); + + // Create FunctionTemplate for HeapSnapshotStream + Local os = FunctionTemplate::New(env->isolate()); + os->Inherit(AsyncWrap::GetConstructorTemplate(env)); + Local ost = os->InstanceTemplate(); + ost->SetInternalFieldCount(StreamBase::kStreamBaseField + 1); + os->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "HeapSnapshotStream")); + StreamBase::AddMethods(env, os); + env->set_streambaseoutputstream_constructor_template(ost); } } // namespace heap diff --git a/src/node_internals.h b/src/node_internals.h index 212da7af28107c..cd9b2590dffd91 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -297,6 +297,41 @@ v8::MaybeLocal StartExecution(Environment* env, namespace coverage { bool StartCoverageCollection(Environment* env); } + +#ifdef _WIN32 +typedef SYSTEMTIME TIME_TYPE; +#else // UNIX, OSX +typedef struct tm TIME_TYPE; +#endif + +class DiagnosticFilename { + public: + static void LocalTime(TIME_TYPE* tm_struct); + + DiagnosticFilename(Environment* env, + const char* prefix, + const char* ext, + int seq = -1) : + filename_(MakeFilename(env->thread_id(), prefix, ext, seq)) {} + + DiagnosticFilename(uint64_t thread_id, + const char* prefix, + const char* ext, + int seq = -1) : + filename_(MakeFilename(thread_id, prefix, ext, seq)) {} + + const char* operator*() const { return filename_.c_str(); } + + private: + static std::string MakeFilename( + uint64_t thread_id, + const char* prefix, + const char* ext, + int seq = -1); + + std::string filename_; +}; + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/util.cc b/src/util.cc index d41163500344ca..099911cf25d38a 100644 --- a/src/util.cc +++ b/src/util.cc @@ -27,7 +27,15 @@ #include "string_bytes.h" #include "uv.h" +#ifdef _WIN32 +#include +#else +#include +#include +#endif + #include +#include #include namespace node { @@ -144,4 +152,61 @@ void ThrowErrStringTooLong(Isolate* isolate) { isolate->ThrowException(ERR_STRING_TOO_LONG(isolate)); } +void DiagnosticFilename::LocalTime(TIME_TYPE* tm_struct) { +#ifdef _WIN32 + GetLocalTime(tm_struct); +#else // UNIX, OSX + struct timeval time_val; + gettimeofday(&time_val, nullptr); + localtime_r(&time_val.tv_sec, tm_struct); +#endif +} + +// Defined in node_internals.h +std::string DiagnosticFilename::MakeFilename( + uint64_t thread_id, + const char* prefix, + const char* ext, + int seq) { + std::ostringstream oss; + TIME_TYPE tm_struct; + LocalTime(&tm_struct); + oss << prefix; +#ifdef _WIN32 + oss << "." << std::setfill('0') << std::setw(4) << tm_struct.wYear; + oss << std::setfill('0') << std::setw(2) << tm_struct.wMonth; + oss << std::setfill('0') << std::setw(2) << tm_struct.wDay; + oss << "." << std::setfill('0') << std::setw(2) << tm_struct.wHour; + oss << std::setfill('0') << std::setw(2) << tm_struct.wMinute; + oss << std::setfill('0') << std::setw(2) << tm_struct.wSecond; +#else // UNIX, OSX + oss << "." + << std::setfill('0') + << std::setw(4) + << tm_struct.tm_year + 1900; + oss << std::setfill('0') + << std::setw(2) + << tm_struct.tm_mon + 1; + oss << std::setfill('0') + << std::setw(2) + << tm_struct.tm_mday; + oss << "." + << std::setfill('0') + << std::setw(2) + << tm_struct.tm_hour; + oss << std::setfill('0') + << std::setw(2) + << tm_struct.tm_min; + oss << std::setfill('0') + << std::setw(2) + << tm_struct.tm_sec; +#endif + oss << "." << uv_os_getpid(); + oss << "." << thread_id; + if (seq >= 0) + oss << "." << std::setfill('0') << std::setw(3) << ++seq; + oss << "." << ext; + return oss.str(); +} + } // namespace node diff --git a/test/common/heap.js b/test/common/heap.js index e23670b64c8a2d..8bda1284bd7e57 100644 --- a/test/common/heap.js +++ b/test/common/heap.js @@ -10,7 +10,7 @@ try { console.log('using `test/common/heap.js` requires `--expose-internals`'); throw e; } -const { createJSHeapDump, buildEmbedderGraph } = internalTestHeap; +const { createJSHeapSnapshot, buildEmbedderGraph } = internalTestHeap; function inspectNode(snapshot) { return util.inspect(snapshot, { depth: 4 }); @@ -33,7 +33,7 @@ function isEdge(edge, { node_name, edge_name }) { class State { constructor() { - this.snapshot = createJSHeapDump(); + this.snapshot = createJSHeapSnapshot(); this.embedderGraph = buildEmbedderGraph(); } diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index c20648f1a43918..5f1d6103bd945e 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -10,7 +10,7 @@ const assert = require('assert'); const isMainThread = common.isMainThread; const kCoverageModuleCount = process.env.NODE_V8_COVERAGE ? 1 : 0; -const kMaxModuleCount = (isMainThread ? 65 : 87) + kCoverageModuleCount; +const kMaxModuleCount = (isMainThread ? 65 : 88) + kCoverageModuleCount; assert(list.length <= kMaxModuleCount, `Total length: ${list.length}\n` + list.join('\n') diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 65f6f8e703131b..0818dec2db9c3f 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -5,6 +5,7 @@ const common = require('../common'); const { internalBinding } = require('internal/test/binding'); const assert = require('assert'); const fs = require('fs'); +const v8 = require('v8'); const fsPromises = fs.promises; const net = require('net'); const providers = Object.assign({}, internalBinding('async_wrap').Providers); @@ -294,3 +295,8 @@ if (process.features.inspector && common.isMainThread) { testInitialized(handle, 'Connection'); handle.disconnect(); } + +// PROVIDER_HEAPDUMP +{ + v8.getHeapSnapshot().destroy(); +} diff --git a/test/sequential/test-heapdump.js b/test/sequential/test-heapdump.js new file mode 100644 index 00000000000000..a65b33c3138a62 --- /dev/null +++ b/test/sequential/test-heapdump.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const { writeHeapSnapshot, getHeapSnapshot } = require('v8'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); +process.chdir(tmpdir.path); + +{ + writeHeapSnapshot('my.heapdump'); + fs.accessSync('my.heapdump'); +} + +{ + const heapdump = writeHeapSnapshot(); + assert.strictEqual(typeof heapdump, 'string'); + fs.accessSync(heapdump); +} + +[1, true, {}, [], null, Infinity, NaN].forEach((i) => { + common.expectsError(() => writeHeapSnapshot(i), { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "path" argument must be one of type string, Buffer, or URL.' + + ` Received type ${typeof i}` + }); +}); + +{ + let data = ''; + const snapshot = getHeapSnapshot(); + snapshot.setEncoding('utf-8'); + snapshot.on('data', common.mustCallAtLeast((chunk) => { + data += chunk.toString(); + })); + snapshot.on('end', common.mustCall(() => { + JSON.parse(data); + })); +} diff --git a/tools/license-builder.sh b/tools/license-builder.sh index 55f630285eaceb..e0f039b45f8c26 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -102,4 +102,7 @@ addlicense "brotli" "deps/brotli" "$(cat ${rootdir}/deps/brotli/LICENSE)" addlicense "HdrHistogram" "deps/histogram" "$(cat ${rootdir}/deps/histogram/LICENSE.txt)" +addlicense "node-heapdump" "src/heap_utils.cc" \ + "$(curl -sL https://raw.githubusercontent.com/bnoordhuis/node-heapdump/0ca52441e46241ffbea56a389e2856ec01c48c97/LICENSE)" + mv $tmplicense $licensefile