V8的HeapProfiler简单解析

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 完

至此我就说完了,感谢看到这里,还有些细节我没有说到,欢迎留言交流,如果有哪位同仁发现不正确的地方,我及时改正,以免误人子弟。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值