V8的HeapProfiler简单解析
v8中的HeapProfiler是用来反映javascript运行时的内存情况,外部接口位于v8-profiler.h文件中,相关的类有HeapGraphEdge,HeapGraphNode,OutputStream,HeapSnapshot,ActivityControl,HeapProfiler。其中HeapProfiler是主要的接口调用类,我们要讲的是这个类里边的TakeHeapSnapshot,StartTrackingHeapObjects,StopTrackingHeapObjects这三个接口。
1. 功能点
首先我们看下关于javascript运行时内存的数据情况,即我们可以用HeapProfiler来做什么,我们可以参照chrome的开发者工具来讨论,chrome浏览器打开开发者工具选择memory,如图所示:
不难看出,memory提供三个功能,我们只讲前两个功能。
1.1 HeapSnapshot
选中第一个,点击“take snapshot”按钮,出现如图界面:
分成两块区域,上边的区域是该时刻存在的对象,按照构造函数/类型分类,例如(array) x2874,array类型的对象有2874个,点开会显示每一个对象,@后边是这个对象的id,之后我们会称这个对象为一个节点(node)。
下边的区域是引用这个node的链,后边节点通过前边的变量引用,下一级引用上一级,我们称前边的变量为边(edge), 例如(GC roots)通过1引用(Stong roots),(Stong roots)通过[16]引用(Stub code),其中distance就是表示从gc root到该node的最短距离。
1.2 Allocation Timeline
选中第二个,并选中复选框allocation stack,点击start按钮,过一段时间点击stop按钮,在这一段时间会如果有内存分配就会在界面上显示,如图所示:
如图上边蓝色柱状就是我们分配内存会显示,右上显示1.7KB,表示此时刻分配内存有1.7KB,我们可以选中该柱状,中间部分显示的是这段时间新分配的对象,下边还是引用,和1.1中类似,比1.1中多了调用栈,如图:
此刻调用栈是说我蓝色选中的那个对象分配内存时的函数调用栈。其中开始的那个复选框allocation stack就是会提供这个功能的。
2. TakeHeapSnapshot,StartTrackingHeapObjects,StopTrackingHeapObjects
1中的功能可以通过这三个接口实现,其中TakeHeapSnapshot可以实现第一个功能,后两个接口用来实现第二个功能。
2.1 TakeHeapSnapshot
我们来看下如何实现第一个功能,这个接口的细则是:
/**
* Takes a heap snapshot and returns it.
*/
const HeapSnapshot* TakeHeapSnapshot(
ActivityControl* control = NULL,
ObjectNameResolver* global_object_name_resolver = NULL);
我们调用时:
class OutputStreamAdapter : public v8::OutputStream
{
public:
void EndOfStream() {
return;
}
int GetChunkSize() {
return 51200;
}
WriteResult WriteAsciiChunk(char* data, int size) {
FILE *file = nullptr;
//fopen_s(&file, "snapshot.heapsnapshot", "a+");
fopen_s(&file, "tracking1.heapsnapshot", "a+");
fwrite(data, 1, size, file);
fclose(file);
return kContinue;
}
WriteResult WriteHeapStatsChunk(v8::HeapStatsUpdate* data, int count) {
return kContinue;
}
};
// call:
OutputStreamAdapter stream;
const HeapSnapshot *shot = isolate->GetHeapProfiler()->TakeHeapSnapshot();
shot->Serialize(&stream);
然后会通过WriteAsciiChunk将数据写入到文件中,导出是json格式的,然后把这个导入到chrome浏览器就可以。
2.2 StartTrackingHeapObjects,StopTrackingHeapObjects
看下代码原行:
/**
* Starts tracking of heap objects population statistics. After calling
* this method, all heap objects relocations done by the garbage collector
* are being registered.
*
* |track_allocations| parameter controls whether stack trace of each
* allocation in the heap will be recorded and reported as part of
* HeapSnapshot.
*/
void StartTrackingHeapObjects(bool track_allocations = false);
/**
* Stops tracking of heap objects population statistics, cleans up all
* collected data. StartHeapObjectsTracking must be called again prior to
* calling GetHeapStats next time.
*/
void StopTrackingHeapObjects();
track_allocations类似上1中叙述的复选框,表示是否采集调用栈信息。
通过如下调用:
isolate->GetHeapProfiler()->StartTrackingHeapObjects(true);
// js代码运行
// ...
const HeapSnapshot *shot = isolate->GetHeapProfiler()->TakeHeapSnapshot();
shot->Serialize(&stream);
isolate->GetHeapProfiler()->StopTrackingHeapObjects();
我们在StopTrackingHeapObjects前调用TakeHeapSnapshot收集这段时间的数据会继续导出成json格式的文件,导入到chrome浏览器中即可显示。如果想要实时显示内存分配情况,类似蓝色柱状,可调用这个函数,然后使用WriteHeapStatsChunk获得数据。
/**
* Adds a new time interval entry to the aggregated statistics array. The
* time interval entry contains information on the current heap objects
* population size. The method also updates aggregated statistics and
* reports updates for all previous time intervals via the OutputStream
* object. Updates on each time interval are provided as a stream of the
* HeapStatsUpdate structure instances.
* If |timestamp_us| is supplied, timestamp of the new entry will be written
* into it. The return value of the function is the last seen heap object Id.
*
* StartTrackingHeapObjects must be called before the first call to this
* method.
*/
SnapshotObjectId GetHeapStats(OutputStream* stream,
int64_t* timestamp_us = NULL);
*3. Json文件解析
如果想要了解json文件里的信息,以上两个功能的json文件格式类似,我只解释第二个功能的,打开这个有好几M的文件,肉眼已经分辨不出来,我们来看下他的大致结构。
我们先展开snapshot看看:
node_count是节点的个数,edge_count是边的个数,trace_function_count是调用栈中总共的函数的个数。
然后我们看meta里,node_fields是一个6个元素的数组,表示一个节点有哪些属性,type类型,name名字,id,self_size,edge_count由该节点出发引用别的对象时边的数量,trace_node_id调用栈的节点id。接下来node_type分别是对上边各个属性的类型的描述.
接下来是edge的属性,type类型,name_or_index名字或者index,to_node表示通过此来引用的节点(通过除以6表示的是下边node数据里的index),然后是edge各个属性的类型。
然后trace_function_info_fields是描述函数调用栈中函数的属性,id,name,script_name所在文件的名字,script_id,line所在文件的行数,column所在函数的列数。
trace_node_fields,在最终表示调用栈时会建立一棵树,其中这个就是一个节点,我觉得也可以表示函数,因为他有引用的函数function_info_index,count表示allocation_count,size表示allocation_size。
samples_fields表示v8对内存采样数据timestamp_us表示时间点,last_assigned_id提供一个数,这个数表示此刻采样的node的最大id(可不等于),即表示的是一个区间,就是此时所采的node的id都小于这个值,同时大于0时刻的值。
meta是对整个文件的描述,接下来我们分析下数据的情况.
snapshot讲完之后是nodes的结构,我们知道表示一个node需要6个元素,所以nodes数组里是6个元素表示一个node,同样edges是3个元素表示一个edge,trace_function_infos是6个元素表示一个function。trace_tree表示的是调用栈形成的一颗树,同样5个元素表示一个trace_node。samples是两个元素表示一次采样。
然后是strings,这个是为了上边的结构提供名字支持的,包括node的name属性的值是strings的index,同样的还有edges的name_or_index,trace_function的name和script_name。
接下来我们通过例子来描述下他们之前形成的关系,弄清楚了这个关系就知道怎样把相应的数据解析了。
javascript例子代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Leak</title>
</head>
<body>
<button id="start">start</button>
<script>
const start = document.getElementById("start");
let b = [];
let c = b;
let d = b;
leak = () => {
for(var i = 0; i < 10; i++) {
var obj = {
a: i
};
window['a'+i] = obj;
b.push(obj);
}
}
start.onclick = () => {
interval = setTimeout(leak, 500);
};
</script>
</body>
</html>
解释下代码,界面有一个按钮,点击start后调用leak函数后,通过key往window里添加10个对象,往b数组中添加10个对象元素。此时window通过a0,a1,a2…a9引用10个obj,b通过数组下标[0],[1],[2]…[9]引用10个obj,同时window分别通过c和d引用b。
3.1 node内存分配时函数调用栈
如标题,一定是node和trace建立关系,我们先看trace相关的,首先trace_node会通过function_info_index和function建立一对一的关系。trace_node里有children成员即是包含子的node,这样就可以形成一个树,我们这里的树是这样的,表明了他们之间的父子关系,我们通过function_info_index从strings数组把它对应的function的name获取到.
"trace_function_infos":[
0,14146,1,0,0,0,
0,14147,1,0,0,0,
46311,4257,1050,17,24,19,
46307,2756,1050,17,15,10,
3629,1074,1,0,0,0
]
"trace_tree": [
1,0,18,652,[ // trace_node1 >> (root)
2,1,365,15224,[], // --trace_node2 >> (V8 API)
3,2,11,1496,[], // --trace_node3 >> start.onclick
4,3,74,16096,[ // --trace_node4 >> leak
5,4,1,76,[] // --trace_node3 >> push
]
]
],
由此也可知道调用顺序是从根到子节点的顺序。
然后我们来看下这个如何和node建立关系,我们注意到node的属性里有一个叫trace_node_id,没错就是它,node通过这个trace_node_id得到trace_node,然后在树中找到位置,就可以知道分配这个node的时候函数调用情况了。
3.2 nodes和edges,引用关系
我们上边已经说了node和edge的关系,这里我们来说下通过数据怎么建立关系。
edges中有一个to_node的属性,表示引用的node,那是谁通过edges来引用的呢?是这样的,node的结构里有一个叫做edge_count的属性,除了表述edge的数量时,同时还能表示他有哪些edge,通过两个数组的顺序来建立关系,比如nodes中第一个node的edges是5,那么edges结构里的前五个就是第一个node的edge,同样第二个按顺序往下,以此类推。
3.3 samples
我们知道samples里两个元素表示以此采样,然后last_assigned_id表示的是一个范围,这样我们就能知道选中一段时间后,这个范围内有哪些节点分配了内存,分配多少内存(self_size)。
4 完
至此我就说完了,感谢看到这里,还有些细节我没有说到,欢迎留言交流,如果有哪位同仁发现不正确的地方,我及时改正,以免误人子弟。