v8 Heapsnapshot 文献领会

v8 Heapsnapshot 文献领会

图片根源:debugging-memory-leaks-node-js-applications正文作家:肖思元v8 Heapsnapshot 文献领会在 node 中不妨经过 v8.getHeapSnapshot 来获得运用暂时的堆快速照相消息,该挪用会天生一份 .heapsnapshot 文献,官方并没有对该文献的实质有一个精细的证明,正文将重要对该文献实质举行领会,并演练了一个领会文献实质后不妨做的风趣的工作

v8 Heapsnapshot 文献领会

v8.getHeapSnapshot开始大略回忆下 v8.getHeapSnapshot 是怎样运用的:

v8 Heapsnapshot 文献领会

// test.js

v8 Heapsnapshot 文献领会

const { writeHeapSnapshot } = require("v8");

v8 Heapsnapshot 文献领会

class HugeObj {

v8 Heapsnapshot 文献领会

constructor() {

v8 Heapsnapshot 文献领会

this.hugeData = Buffer.alloc((1 << 20) * 50, 0);

v8 Heapsnapshot 文献领会

}

v8 Heapsnapshot 文献领会

}

v8 Heapsnapshot 文献领会

// 提防底下的用法在本质运用中常常是 anti-pattern,

v8 Heapsnapshot 文献领会

// 这边不过为了简单演练,才将东西挂到 module 上以提防被 GC 开释

v8 Heapsnapshot 文献领会

module.exports.data = new HugeObj();

v8 Heapsnapshot 文献领会

writeHeapSnapshot();

v8 Heapsnapshot 文献领会

将上头的代码生存到 test.js 中,而后运转 node test.js,会天生文献名一致 Heap.20210228.154141.9320.0.001.heapsnapshot 的文献,该文献不妨运用 Chrome Dev Tools 举行察看

v8 Heapsnapshot 文献领会

对于上头的办法咱们也不妨径直 察看视频演练当咱们将 .heapsnapshot 文献导出到 Chrome Dev Tools 之后,咱们会看到一致底下的实质:

v8 Heapsnapshot 文献领会

上海图书馆表格列出了暂时堆中的一切东西,个中列的含意是:

v8 Heapsnapshot 文献领会

Constructor,表白东西是运用该因变量结构而来Constructor 对应的范例的数目,在 Constructor 反面的 x2 中表露Shallow size,东西自己巨细(单元是 Byte),比方上头的 HugeObj,它的范例的 Shallow size 即是自己占用的外存巨细,比方,东西里面为了保护属性和值的对应联系所占用的外存,并不包括持有东西的巨细比方 hugeData 属性援用的 Buffer 东西的巨细,并不管帐算在 HugeObj 范例的 Shallow size 中Retained size,东西自己巨细加上它依附链路上的一切东西的自己巨细(Shallow size)之和Distance,表白从根节点(Roots)达到该东西过程的最短路途的长度heapsnapshot 文献Chrome Dev Tools 不过 .heapsnapshot 文献的一种展示情势,即使咱们蓄意最大水平运用那些消息,则须要进一步领会其文献方法

咱们不妨运用大肆的文本编纂器翻开该文献,不妨创造文献实质本来是 JSON 方法的:

由于暂时没有简直的证明文书档案,反面的实质咱们将贯串源码来领会该文献的实质

文献实质大概浏览在原始输入的文献实质中,不妨创造 snapshot 字段局部是去除空缺的,而 nodes 和 edges 字段的实质都是有换行分割的,完全文献有特殊多的行数

为了简单领会,咱们不妨将节点折叠,如许不妨看出该文献的完全实质:

随后咱们在源码中,以该 v8.getHeapSnapshot 的 binding 发端,定位到该文献实质是本领 HeapSnapshotGenerator::GenerateSnapshot 的运转截止

而且咱们领会东西在外存中的拓扑情势须要运用 Graph 数据构造 来表白,所以输入文献中有 nodes 和 edges 字段辨别用来表白堆中的东西,以及东西间的贯穿联系:

图片援用自 Graphs然而 nodes 和 edges 中并没有径直保存东西的消息,而都是延续串数字,咱们须要进一步领会个中的实质

nodesnodes 中的每一个 Node 的序列化本领是:HeapSnapshotJSONSerializer::SerializeNode

从源码来看,每输入完 node 的一切属性值后,会随着输入 \n\0,这也是输入截止中 nodes 数组是一条龙行数字的因为。然而咱们领会 \n\0 在 JSON 反序列化的功夫由于会由于自己适合空缺的设置而被忽视掉,以是如许的换行不妨领会是为了简单径直察看源文献

咱们来看一个例子,比方:

{

"nodes":[9,1,1,0,10,0 // 第一条龙

,9,2,3,0,23,0 // 第二行

}

上头的实质,每行辨别表白一个 node,每一条龙都是东西的属性的 value(咱们先不必商量干什么 value 都是数值)。而属性的 name 咱们经过源码中输入的程序不妨整治出来:

0. type

1. name

2. id

3. self_size

4. edge_count

5. trace_node_id

由于 value 的输入程序和上头的 name 是对应的,以是咱们不妨按照属性 name 的程序动作索引,去关系其 value 的值

然而本质上并不许简略属性称呼列表的输入,由于属性的实质是大概在后续的 node 本子中变革的(主假如伴随 v8 的变革),为了和对应的数据耗费端解耦,文献中会将属性 name 列出输入,生存在 snapshot.meta.node_fields 中

Field Type接下来咱们来看干什么 nodes 数组生存的属性 value 都是数值

仍旧上头的例子,由于咱们仍旧领会了,属性称呼和属性值是按索引程序对应上的,那么对于上头第一个 node 的 propertyName(propertyValue) 列表不妨表白为:

0. type(9)

1. name(1)

2. id(1)

3. self_size(0)

4. edge_count(10)

5. trace_node_id(0)

比方第 1 号属性 name,它即是东西的称呼,然而按照东西的典型各别,该值也会有各别的取值办法。比方对于普遍东西而言,它的实质即是其结构因变量的称呼,对于 Regexp 东西而言,它的值即是 pattern 字符串,更多得不妨参考 V8HeapExplorer::AddEntry

假设咱们径直生存属性的值,那么即使堆中有 1000 个由 HugeObj 结构的东西,HugeObj 字符串就要生存 1000 个正片

由于 heapdump 望文生义,输入巨细简直就和暂时 Node 运用所占外存巨细普遍(并不实足普遍,这边 heapdump 只包括受 GC 处置的实质),为了让输入的截止尽大概的紧凑,v8 在输入属性值的功夫,按确定的准则举行了收缩,收缩的诀窍是:

减少一条记载 snapshot.meta.node_types,来寄存属性的典型,和 snapshot.meta.node_fields 一致,它们和属性值之间也是经过索引(程序)关系的nodes 中只寄存属性值,咱们须要计划一下偏移量(底下会讲到),来决定属性的典型:即使是数值典型,那么该值即是自己的实质即使是数组,则 value 对应数组中的索引即使是字符串,则 value 对应 strings 数组的实质咱们不妨用底下的图来表白三者之间的联系:

咱们经过一个例子来串联上头的实质。比方咱们要看索引为 1000 的东西(提防辨别 id 属性)的 name 属性的值,运用底下的办法:

取 name 属性在 snapshot.meta.node_fields 中的索引为 1取 snapshot.meta.node_fields 数组的长度为 6则索引为 1000 的东西的开始索引为:1000 * 6(由于东西属性的数目是恒定的)加上 name 属性的偏移量 1,则 name 在 nodes 数组中的索引为 6001 = 1000 * 6 + 1取 name 属性在 snapshot.meta.node_types 中的典型,即 snapshot.meta.node_types[1],在这个例子中是 string则 strings[6001] 的实质即是 name 属性值的最后实质其他少许字段的含意是:

id,东西的 id,v8 会保证该东西在此次运用人命周期中的屡次的 dump 下中维持沟通的 idself_size,也即是下文提到的 shallow sizeedge_count,即是从该东西出去的边的条数,也即是子东西的数目trace_node_id,不妨姑且不去商量,惟有在同声运用 node --track-heap-objects 启用运用的情景下,该实质才不会为 0。它不妨贯串 trace_tree 和 trace_function_infos 一道领会东西是在什么挪用栈下被创造的,换句话说即是领会过程一系列什么挪用创了该东西。文本不会计划这局部实质,大概会在此后的章节中打开edgesedges 中的 Edge 的序列化办法是:HeapSnapshotJSONSerializer::SerializeEdge

字段实质辨别是:

0. type

1. edge_name_or_index(idx or stringId)

2. to

和上头的 nodes 数组一致,edges 数组也是都存的属性的值,所以在取最后值的功夫,须要贯串 snapshot.meta.edge_fields snapshot.meta.edge_types 来操纵

独一的题目在乎,咱们领会 Edge 表白的东西之间的联系,并且这边是有向图,那么确定有 From 和 To 两个字段,而上头的字段实质惟有 To,那么 nodes 和 edges 是怎样对应的呢?

Node 和 Edge 的对应联系从新以 HeapSnapshotGenerator::GenerateSnapshot 本领发端领会,看看 nodes 和 edges 是怎样爆发的,底下是该本领中的关系重要实质:

bool HeapSnapshotGenerator::GenerateSnapshot() {

// ...

// 介入 Root 节点,动作震动东西的开始

snapshot_->AddSyntheticRootEntries();

// 即 HeapSnapshotGenerator::FillReferences 本领,nodes 和 edges

// 都是由该本领建立的,这边的 nodes 和 edges 指的是 HeapSnapshot 的

// 数据分子 `entries_` 和 `edges_`

if (!FillReferences()) return false;

// 输入文献中的 edges 本质是经过 `FillChildren` 从新构造程序的,

// 从新构造后的实质生存在 HeapSnapshot 的数据分子 children_ 中

snapshot_->FillChildren();

snapshot_->RememberLastJSObjectId();

progress_counter_ = progress_total_;

if (!ProgressReport(true)) return false;

// ...

}

不妨姑且不去深刻领会 Node 和 Edge 是怎样天生的,看一下 HeapSnapshot::FillChildren 本领是怎样从新构造输入的 edges 实质的:

void HeapSnapshot::FillChildren() {

// ...

int children_index = 0;

for (HeapEntry& entry : entries()) {

children_index = entry.set_children_index(children_index);

}

// ...

children().resize(edges().size());

for (HeapGraphEdge& edge : edges()) {

edge.from()->add_child(&edge);

}

}

个中 entry.set_children_index 和 edge.from()->add_child 本领实质辨别是:

int HeapEntry::set_children_index(int index) {

// Note: children_count_ and children_end_index_ are parts of a union.

int next_index = index + children_count_;

children_end_index_ = index;

return next_index;

}

void HeapEntry::add_child(HeapGraphEdge* edge) {

snapshot_->children()[children_end_index_++] = edge;

}

以是对于每个 entry(即 node)都有一个属性 children_index,它表白 entry 的 children 在 children_ 数组中的开始索引(上头解释中仍旧提到,heapsnapshot 文献中的 edges 数组的实质即是按照 children_ 数组输入的)

归纳来看,edges 数组的实质和 nodes 之间的对应联系大概是:

比方上头 edge0 的 From 即是 nodes[0 + 2],个中:

nodes 表白 nodes 数组0 的场所表白该 node 在 nodes 数组中的索引,这边也即是第一个元素2 表白 id 属性在 snapshot.meta.node_fields 数组中的偏移量node0 的 edge_count 不妨表白成 nodes[0 + 4]:

个中 4 表白 edge_count 属性在 snapshot.meta.node_fields 数组中的偏移量其余局部同上以是 edges 数组中,从 0 发端的 node0.edge_count 个 edge 的 From 都是 node0.id

由于 node[n].edge_count 是变量,以是咱们没辙赶快按照索引定位到某个 edge 的 From,咱们必需从索引 0 发端,而后步进 node[n].edge_count 次(n 从 0 发端),步进度数内的 edge 的 From 都为 node[n].id,步进中断后对 n = n + 1 ,从而鄙人一次迭代中关系下一个 node 的 edges

heapquery咱们发端说领会文献实质不妨做少许风趣的工作,接下来咱们将演练一个小步调 heapquery(Rust 劝入版),它不妨将 .heapsnapshot 文献的实质导出到 sqlite 中,而后咱们就不妨经过 SQL 来查问本人感爱好的实质了(固然远没有 osquery 高档,然而径直经过 SQL 来查问堆上的实质,想想城市很风趣吧)

除此除外,它还不妨:

考证下文对 heapsnapshot 文献方法的领会对下文的笔墨刻画供给一个可运转的代码的弥补证明由于 heapquery 的步调实质特殊大略(只是是领会方法并导出罢了),以是就不赘述了。只大略看一下波及的表构造,由于只是是演练用,到结果本来惟有两张表:

Node 表

CREATE TABLE IF NOT EXISTS node (

id INTEGER PRIMARY KEY, /* 东西 id */

name VARCHAR(50), /* 东西分属典型称呼 */

type VARCHAR(50), /* 东西分属典型列举,取自 `snapshot.meta.node_types` */

self_size INTEGER, /* 东西自己巨细 */

edge_count INTEGER, /* 东西持有的子东西数目 */

trace_node_id INTEGER

);

Edge 表

CREATE TABLE IF NOT EXISTS edge (

from_node INTEGER, /* 父东西 id */

to_node INTEGER, /* 子东西 id */

type VARCHAR(50), /* 联系典型,取自 `snapshot.meta.edge_types` */

name_or_index VARCHAR(50) /* 联系称呼,属性称呼大概索引 */

);

小演示在正文发端的场所,咱们设置了一个 HugeObj 类,在范例化该类的功夫,会创造一个巨细为 50M 的 Buffer 东西,并关系到其属性 hugeData 上

接下来咱们将举行一个小演示,假如咱们事前并不领会 HugeObj,咱们怎样经过大概的外存特殊局面反推定位到它

开始咱们须要将 .heapsnapshot 导出到 sqlite 中:

npx heapquery path_to_your_heapdump.heapsnapshot

吩咐运转实行后,会在暂时目次下天生 path_to_your_heapdump.db 文献,咱们不妨采用本人爱好的 sqlite browser 翻开它,比方这边运用的 DB Browser for SQLite

而后咱们实行一条 SQL 语句,将 node 按 self_size 倒序陈设后输入:

SELECT * FROM node ORDER By self_size DESC

咱们会获得一致底下的截止:

咱们接着从巨细疑惑的东西动手,固然这边即是先看截图中 id 为 51389 的这条数据了

接下来咱们再实行一条 SQL 语句,看看是哪个东西持有了东西 51389

SELECT from_node, B.name AS from_node_name

FROM edge AS A

JOIN node AS B ON A.from_node = B.id

WHERE A.to_node = 51389

咱们会获得一致底下的输入:

上头的输入中,咱们领会持有 51389 的东西是 51387,而且该东西的典型是 ArrayBuffer

由于 ArrayBuffer 是情况内置的类,咱们并不许看出什么题目,所以须要运用上头的 SQL,连接察看 51387 是被哪个东西持有的:

和上头的输入一致,这次的 Buffer 仍旧是内置东西,以是咱们连接反复上头的办法:

这次咱们获得了一个交易东西 HugeObj,咱们看看它是在何处设置的。东西的设置即是它的结构因变量,所以咱们须要找到它的 constructor,为此咱们先列出东西的一切属性:

SELECT * FROM edge WHERE from_node = 46141 AND `type` = "property"

接着咱们在原形中连接搜索:

SELECT * FROM edge WHERE from_node = 4575 AND `type` = "property"

咱们找到了 constructor 东西 4577,接着咱们来找到它的 shared 里面属性:

SELECT * FROM edge WHERE from_node = 4577 AND name_or_index = "shared"

咱们大略证明一下 shared 属性的效率是什么。开始,常常因变量包括的消息有:

设置地方的源文献场所原始代码(在具备 JIT 的运转时顶用于 Deoptimize)一组在交易上可复用的训令(Opcode or JITed)PC 存放器消息,表白固然实行到里面哪一个训令,并在将来回复时不妨连接实行BP 存放器消息,表白暂时挪用栈帧在栈上的开始地方因变量东西创造时对应的闭包援用个中「设置地方的源文献场所」、「原始代码」、「一组在交易上可复用的训令(Opcode or JITed)」是没有需要创造出多份正片的,所以一致如许的实质,在 v8 中就会放到 shared 东西中

接下来咱们不妨输入 shared 东西 43271 的属性:

SELECT * FROM edge WHERE from_node = 43271

咱们连接输入 script_or_debug_info 属性持有的东西 8463:

SELECT * FROM edge WHERE from_node = 8463

结果咱们输入 name 属性持有的东西 4587:

如许咱们就找到了东西设置的文献,而后就不妨在该文献中连接决定交易代码能否生存揭发的大概

大概有人会对上头的办法感触烦琐,本来不用担忧,咱们不妨贯串本人本质的查问需要,将常用的查问功效编写成子步调,如许此后只有给一个输出,就能扶助咱们领会出想要的截止了

总结正文以领会 .heapsnapshot 文献的方法为突破点,贯串 node 的源码,证明了 .heapsnapshot 文献方法和其天生的办法,并供给了个 heapquery 的小步调,演练了领会其构造不妨扶助咱们赢得不控制于现有东西的消息。结果祝大师上分欣喜!

正文颁布自 网易云音乐大前者共青团和少先队,作品一经受权遏止任何情势的连载。咱们长年招收前者、iOS、Android,即使你筹备换处事,又凑巧爱好云音乐,那就介入咱们 grp.music-fe(at)corp.netease.com!

分享到 :
相关推荐

Leave a Reply

Your email address will not be published. Required fields are marked *