Skip to content

Commit a9a7186

Browse files
addaleaxtargos
authored andcommitted
src: make heap snapshot & embedder graph accessible for tests
Add methods that allow inspection of heap snapshots and a JS version of our own embedder graph. These can be used in tests and might also prove useful for ad-hoc debugging. Usage requires `--expose-internals` and prints a warning similar to our other modules whose primary purpose is test support. PR-URL: #21741 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
1 parent 5121278 commit a9a7186

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-0
lines changed

‎lib/internal/test/heap.js‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
process.emitWarning(
4+
'These APIs are exposed only for testing and are not '+
5+
'tracked by any versioning system or deprecation process.',
6+
'internal/test/heap');
7+
8+
const{ internalBinding }=require('internal/bootstrap/loaders');
9+
const{ createHeapDump, buildEmbedderGraph }=internalBinding('heap_utils');
10+
constassert=require('assert');
11+
12+
// This is not suitable for production code. It creates a full V8 heap dump,
13+
// parses it as JSON, and then creates complex objects from it, leading
14+
// to significantly increased memory usage.
15+
functioncreateJSHeapDump(){
16+
constdump=createHeapDump();
17+
constmeta=dump.snapshot.meta;
18+
19+
constnodes=
20+
readHeapInfo(dump.nodes,meta.node_fields,meta.node_types,dump.strings);
21+
constedges=
22+
readHeapInfo(dump.edges,meta.edge_fields,meta.edge_types,dump.strings);
23+
24+
for(constnodeofnodes){
25+
node.incomingEdges=[];
26+
node.outgoingEdges=[];
27+
}
28+
29+
letfromNodeIndex=0;
30+
letedgeIndex=0;
31+
for(const{ type, name_or_index, to_node }ofedges){
32+
while(edgeIndex===nodes[fromNodeIndex].edge_count){
33+
edgeIndex=0;
34+
fromNodeIndex++;
35+
}
36+
consttoNode=nodes[to_node/meta.node_fields.length];
37+
constfromNode=nodes[fromNodeIndex];
38+
constedge={
39+
type,
40+
toNode,
41+
fromNode,
42+
name: typeofname_or_index==='string' ? name_or_index : null
43+
};
44+
toNode.incomingEdges.push(edge);
45+
fromNode.outgoingEdges.push(edge);
46+
edgeIndex++;
47+
}
48+
49+
for(constnodeofnodes)
50+
assert.strictEqual(node.edge_count,node.outgoingEdges.length);
51+
52+
returnnodes;
53+
}
54+
55+
functionreadHeapInfo(raw,fields,types,strings){
56+
constitems=[];
57+
58+
for(vari=0;i<raw.length;i+=fields.length){
59+
constitem={};
60+
for(varj=0;j<fields.length;j++){
61+
constname=fields[j];
62+
lettype=types[j];
63+
if(Array.isArray(type)){
64+
item[name]=type[raw[i+j]];
65+
}elseif(name==='name_or_index'){// type === 'string_or_number'
66+
if(item.type==='element'||item.type==='hidden')
67+
type='number';
68+
else
69+
type='string';
70+
}
71+
72+
if(type==='string'){
73+
item[name]=strings[raw[i+j]];
74+
}elseif(type==='number'||type==='node'){
75+
item[name]=raw[i+j];
76+
}
77+
}
78+
items.push(item);
79+
}
80+
81+
returnitems;
82+
}
83+
84+
module.exports={
85+
createJSHeapDump,
86+
buildEmbedderGraph
87+
};

‎node.gyp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
'lib/internal/repl/await.js',
147147
'lib/internal/socket_list.js',
148148
'lib/internal/test/binding.js',
149+
'lib/internal/test/heap.js',
149150
'lib/internal/test/unicode.js',
150151
'lib/internal/timers.js',
151152
'lib/internal/tls.js',
@@ -328,6 +329,7 @@
328329
'src/exceptions.cc',
329330
'src/fs_event_wrap.cc',
330331
'src/handle_wrap.cc',
332+
'src/heap_utils.cc',
331333
'src/js_stream.cc',
332334
'src/module_wrap.cc',
333335
'src/node.cc',

‎src/heap_utils.cc‎

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#include"node_internals.h"
2+
#include"env.h"
3+
4+
using v8::Array;
5+
using v8::Boolean;
6+
using v8::Context;
7+
using v8::EmbedderGraph;
8+
using v8::EscapableHandleScope;
9+
using v8::FunctionCallbackInfo;
10+
using v8::HandleScope;
11+
using v8::HeapSnapshot;
12+
using v8::Isolate;
13+
using v8::JSON;
14+
using v8::Local;
15+
using v8::MaybeLocal;
16+
using v8::Number;
17+
using v8::Object;
18+
using v8::String;
19+
using v8::Value;
20+
21+
namespacenode{
22+
namespaceheap{
23+
24+
classJSGraphJSNode : publicEmbedderGraph::Node{
25+
public:
26+
constchar* Name() override{return"<JS Node>"}
27+
size_tSizeInBytes() override{return0}
28+
boolIsEmbedderNode() override{returnfalse}
29+
Local<Value> JSValue(){returnStrongPersistentToLocal(persistent_)}
30+
31+
intIdentityHash(){
32+
Local<Value> v = JSValue();
33+
if (v->IsObject()) return v.As<Object>()->GetIdentityHash();
34+
if (v->IsName()) return v.As<v8::Name>()->GetIdentityHash();
35+
if (v->IsInt32()) return v.As<v8::Int32>()->Value();
36+
return0;
37+
}
38+
39+
JSGraphJSNode(Isolate* isolate, Local<Value> val)
40+
: persistent_(isolate, val){
41+
CHECK(!val.IsEmpty());
42+
}
43+
44+
structHash{
45+
inlinesize_toperator()(JSGraphJSNode* n) const{
46+
return n->IdentityHash();
47+
}
48+
};
49+
50+
structEqual{
51+
inlinebooloperator()(JSGraphJSNode* a, JSGraphJSNode* b) const{
52+
return a->JSValue()->SameValue(b->JSValue());
53+
}
54+
};
55+
56+
private:
57+
Persistent<Value> persistent_;
58+
};
59+
60+
classJSGraph : publicEmbedderGraph{
61+
public:
62+
explicitJSGraph(Isolate* isolate) : isolate_(isolate){}
63+
64+
Node* V8Node(const Local<Value>& value) override{
65+
std::unique_ptr<JSGraphJSNode> n{newJSGraphJSNode(isolate_, value) };
66+
auto it = engine_nodes_.find(n.get());
67+
if (it != engine_nodes_.end())
68+
return *it;
69+
engine_nodes_.insert(n.get());
70+
returnAddNode(std::unique_ptr<Node>(n.release()));
71+
}
72+
73+
Node* AddNode(std::unique_ptr<Node> node) override{
74+
Node* n = node.get();
75+
nodes_.emplace(std::move(node));
76+
return n;
77+
}
78+
79+
voidAddEdge(Node* from, Node* to) override{
80+
edges_[from].insert(to);
81+
}
82+
83+
MaybeLocal<Array> CreateObject() const{
84+
EscapableHandleScope handle_scope(isolate_);
85+
Local<Context> context = isolate_->GetCurrentContext();
86+
87+
std::unordered_map<Node*, Local<Object>> info_objects;
88+
Local<Array> nodes = Array::New(isolate_, nodes_.size());
89+
Local<String> edges_string = FIXED_ONE_BYTE_STRING(isolate_, "edges");
90+
Local<String> is_root_string = FIXED_ONE_BYTE_STRING(isolate_, "isRoot");
91+
Local<String> name_string = FIXED_ONE_BYTE_STRING(isolate_, "name");
92+
Local<String> size_string = FIXED_ONE_BYTE_STRING(isolate_, "size");
93+
Local<String> value_string = FIXED_ONE_BYTE_STRING(isolate_, "value");
94+
Local<String> wraps_string = FIXED_ONE_BYTE_STRING(isolate_, "wraps");
95+
96+
for (const std::unique_ptr<Node>& n : nodes_)
97+
info_objects[n.get()] = Object::New(isolate_);
98+
99+
{
100+
HandleScope handle_scope(isolate_);
101+
size_t i = 0;
102+
for (const std::unique_ptr<Node>& n : nodes_){
103+
Local<Object> obj = info_objects[n.get()];
104+
Local<Value> value;
105+
if (!String::NewFromUtf8(isolate_, n->Name(),
106+
v8::NewStringType::kNormal).ToLocal(&value) ||
107+
obj->Set(context, name_string, value).IsNothing() ||
108+
obj->Set(context, is_root_string,
109+
Boolean::New(isolate_, n->IsRootNode())).IsNothing() ||
110+
obj->Set(context, size_string,
111+
Number::New(isolate_, n->SizeInBytes())).IsNothing() ||
112+
obj->Set(context, edges_string,
113+
Array::New(isolate_)).IsNothing()){
114+
return MaybeLocal<Array>();
115+
}
116+
if (nodes->Set(context, i++, obj).IsNothing())
117+
return MaybeLocal<Array>();
118+
if (!n->IsEmbedderNode()){
119+
value = static_cast<JSGraphJSNode*>(n.get())->JSValue();
120+
if (obj->Set(context, value_string, value).IsNothing())
121+
return MaybeLocal<Array>();
122+
}
123+
}
124+
}
125+
126+
for (const std::unique_ptr<Node>& n : nodes_){
127+
Node* wraps = n->WrapperNode();
128+
if (wraps == nullptr) continue;
129+
Local<Object> from = info_objects[n.get()];
130+
Local<Object> to = info_objects[wraps];
131+
if (from->Set(context, wraps_string, to).IsNothing())
132+
return MaybeLocal<Array>();
133+
}
134+
135+
for (constauto& edge_info : edges_){
136+
Node* source = edge_info.first;
137+
Local<Value> edges;
138+
if (!info_objects[source]->Get(context, edges_string).ToLocal(&edges) ||
139+
!edges->IsArray()){
140+
return MaybeLocal<Array>();
141+
}
142+
143+
size_t i = 0;
144+
for (Node* target : edge_info.second){
145+
if (edges.As<Array>()->Set(context,
146+
i++,
147+
info_objects[target]).IsNothing()){
148+
return MaybeLocal<Array>();
149+
}
150+
}
151+
}
152+
153+
return handle_scope.Escape(nodes);
154+
}
155+
156+
private:
157+
Isolate* isolate_;
158+
std::unordered_set<std::unique_ptr<Node>> nodes_;
159+
std::unordered_set<JSGraphJSNode*, JSGraphJSNode::Hash, JSGraphJSNode::Equal>
160+
engine_nodes_;
161+
std::unordered_map<Node*, std::unordered_set<Node*>> edges_;
162+
};
163+
164+
voidBuildEmbedderGraph(const FunctionCallbackInfo<Value>& args){
165+
Environment* env = Environment::GetCurrent(args);
166+
JSGraph graph(env->isolate());
167+
Environment::BuildEmbedderGraph(env->isolate(), &graph, env);
168+
Local<Array> ret;
169+
if (graph.CreateObject().ToLocal(&ret))
170+
args.GetReturnValue().Set(ret);
171+
}
172+
173+
174+
classBufferOutputStream : publicv8::OutputStream{
175+
public:
176+
BufferOutputStream() : buffer_(new JSString()){}
177+
178+
voidEndOfStream() override{}
179+
intGetChunkSize() override{return1024 * 1024}
180+
WriteResult WriteAsciiChunk(char* data, int size) override{
181+
buffer_->Append(data, size);
182+
returnkContinue;
183+
}
184+
185+
Local<String> ToString(Isolate* isolate){
186+
returnString::NewExternalOneByte(isolate,
187+
buffer_.release()).ToLocalChecked();
188+
}
189+
190+
private:
191+
classJSString : publicString::ExternalOneByteStringResource{
192+
public:
193+
voidAppend(char* data, size_t count){
194+
store_.append(data, count);
195+
}
196+
197+
constchar* data() constoverride{return store_.data()}
198+
size_tlength() constoverride{return store_.size()}
199+
200+
private:
201+
std::string store_;
202+
};
203+
204+
std::unique_ptr<JSString> buffer_;
205+
};
206+
207+
voidCreateHeapDump(const FunctionCallbackInfo<Value>& args){
208+
Isolate* isolate = args.GetIsolate();
209+
const HeapSnapshot* snapshot = isolate->GetHeapProfiler()->TakeHeapSnapshot();
210+
BufferOutputStream out;
211+
snapshot->Serialize(&out, HeapSnapshot::kJSON);
212+
const_cast<HeapSnapshot*>(snapshot)->Delete();
213+
Local<Value> ret;
214+
if (JSON::Parse(isolate->GetCurrentContext(),
215+
out.ToString(isolate)).ToLocal(&ret)){
216+
args.GetReturnValue().Set(ret);
217+
}
218+
}
219+
220+
voidInitialize(Local<Object> target,
221+
Local<Value> unused,
222+
Local<Context> context){
223+
Environment* env = Environment::GetCurrent(context);
224+
225+
env->SetMethodNoSideEffect(target, "buildEmbedderGraph", BuildEmbedderGraph);
226+
env->SetMethodNoSideEffect(target, "createHeapDump", CreateHeapDump);
227+
}
228+
229+
} // namespace heap
230+
} // namespace node
231+
232+
NODE_MODULE_CONTEXT_AWARE_INTERNAL(heap_utils, node::heap::Initialize)

‎src/node_internals.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ struct sockaddr;
111111
V(domain) \
112112
V(fs) \
113113
V(fs_event_wrap) \
114+
V(heap_utils) \
114115
V(http2) \
115116
V(http_parser) \
116117
V(inspector) \

0 commit comments

Comments
(0)