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 <riclau@uk.ibm.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
James M Snell 2019-03-07 16:51:36 +00:00
parent 024842f8f4
commit 5f38797ea5
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
14 changed files with 538 additions and 16 deletions

39
LICENSE
View File

@ -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 <info@bnoordhuis.nl>
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.
"""

View File

@ -87,6 +87,24 @@ The value returned is an array of objects containing the following properties:
]
```
## v8.getHeapSnapshot()
<!-- YAML
added: REPLACEME
-->
* 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()
<!-- YAML
added: v1.0.0
@ -159,6 +177,58 @@ v8.setFlagsFromString('--trace_gc');
setTimeout(() => { v8.setFlagsFromString('--notrace_gc'); }, 60e3);
```
## v8.writeHeapSnapshot([filename])
<!-- YAML
added: REPLACEME
-->
* `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

View File

@ -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
};

View File

@ -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
};

View File

@ -41,6 +41,7 @@ namespace node {
V(FSREQPROMISE) \
V(GETADDRINFOREQWRAP) \
V(GETNAMEINFOREQWRAP) \
V(HEAPSNAPSHOT) \
V(HTTP2SESSION) \
V(HTTP2STREAM) \
V(HTTP2PING) \

View File

@ -383,6 +383,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) \

View File

@ -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<JSString> buffer_;
};
void CreateHeapDump(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const HeapSnapshot* snapshot = isolate->GetHeapProfiler()->TakeHeapSnapshot();
BufferOutputStream out;
snapshot->Serialize(&out, HeapSnapshot::kJSON);
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_t>(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<v8::Object> 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<ssize_t>(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<HeapSnapshot*>(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<HeapSnapshot*>(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<Value>& args) {
Isolate* isolate = args.GetIsolate();
BufferOutputStream out;
TakeSnapshot(isolate, &out);
Local<Value> ret;
if (JSON::Parse(isolate->GetCurrentContext(),
out.ToString(isolate)).ToLocal(&ret)) {
@ -244,14 +381,73 @@ void CreateHeapDump(const FunctionCallbackInfo<Value>& args) {
}
}
void CreateHeapSnapshotStream(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HandleScope scope(env->isolate());
const HeapSnapshot* const snapshot =
env->isolate()->GetHeapProfiler()->TakeHeapSnapshot();
CHECK_NOT_NULL(snapshot);
Local<Object> 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
Local<Value> 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<Object> target,
Local<Value> unused,
Local<Context> 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<FunctionTemplate> os = FunctionTemplate::New(env->isolate());
os->Inherit(AsyncWrap::GetConstructorTemplate(env));
Local<ObjectTemplate> 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

View File

@ -299,6 +299,41 @@ v8::MaybeLocal<v8::Value> StartExecution(Environment* env,
namespace profiler {
void 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

View File

@ -27,7 +27,15 @@
#include "string_bytes.h"
#include "uv.h"
#ifdef _WIN32
#include <time.h>
#else
#include <sys/time.h>
#include <sys/types.h>
#endif
#include <cstdio>
#include <iomanip>
#include <sstream>
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

View File

@ -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();
}

View File

@ -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')

View File

@ -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();
}

View File

@ -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);
}));
}

View File

@ -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