介绍
memwatch 是一个 c ++ 扩展,主要用来观察 nodejs 内存泄露问题,基本用法如下:
const memwatch = require(‘@airbnb/memwatch’);
function LeakingClass() {
}
memwatch.gc();
var arr = [];
var hd = new memwatch.HeapDiff();
for (var i = 0; i < 10000; i++) arr.push(new LeakingClass);
var hde = hd.end();
console.log(JSON.stringify(hde, null, 2));
实现分析
分析的版本为 @airbnb/memwatch。首先从 binding.gyp 开始入手:
{
‘targets’: [
{
‘target_name’: ‘memwatch’,
‘include_dirs’: [
“<!(node -e \”require(‘nan’)\”)”
],
‘sources’: [
‘src/heapdiff.cc’,
‘src/init.cc’,
‘src/memwatch.cc’,
‘src/util.cc’
]
}
]
}
这份配置表示其生成的目标是 memwatch.node, 源码是 src 目录下的 heapdiff.cc、init.cc、memwatch.cc、util.cc, 在项目编译的过程中还需要 include 额外的 nan 目录,nan 目录通过执行 node -e “require(‘nan’) 按照 node 模块系统寻找 nan 依赖,<! 表示后面是一条指令。
memwatch 的入口函数在 init.cc 文件中,通过 NODE_MODULE(memwatch, init); 进行声明。当执行 require(‘@airbnb/memwatch’) 的时候会首先调用 init 函数:
void init (v8::Handle<v8::Object> target)
{
Nan::HandleScope scope;
heapdiff::HeapDiff::Initialize(target);
Nan::SetMethod(target, “upon_gc”, memwatch::upon_gc);
Nan::SetMethod(target, “gc”, memwatch::trigger_gc);
Nan::AddGCPrologueCallback(memwatch::before_gc);
Nan::AddGCEpilogueCallback(memwatch::after_gc);
}
init 函数的入口参数 v8:Handle<v8:Object> target 可以类比 nodejs 中的 module.exports 的 exports 对象。函数内部做的实现可以分为三块,初始化 target、给 target 绑定 upon_gc 和 gc 两个函数、在 nodejs 的 gc 前后分别挂上对应的钩子函数。
Initialize 实现
到 heapdiff.cc 文件中来看 heapdiff::HeapDiff::Initialize(target); 的实现。
void heapdiff::HeapDiff::Initialize (v8::Handle<v8::Object> target)
{
Nan::HandleScope scope;
v8::Local<v8::FunctionTemplate> t = Nan::New<v8::FunctionTemplate>(New);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(Nan::New<v8::String>(“HeapDiff”).ToLocalChecked());
Nan::SetPrototypeMethod(t, “end”, End);
target->Set(Nan::New<v8::String>(“HeapDiff”).ToLocalChecked(), t->GetFunction());
}
Initialize 函数中创建一个叫做 HeapDiff 的函数 t,同时在 t 的原型链上绑了 end 方法,使得 js 层面可以执行 vat hp = new memwatch.HeapDiff();hp.end()。
new memwatch.HeapDiff 实现
当 js 执行 new memwatch.HeapDiff(); 的时候,c++ 层面会执行 heapdiff::HeapDiff::New 函数,去掉注释和不必要的宏,New 函数精简如下:
NAN_METHOD(heapdiff::HeapDiff::New)
{
if (!info.IsConstructCall()) {
return Nan::ThrowTypeError(“Use the new operator to create instances of this object.”);
}
Nan::HandleScope scope;
HeapDiff * self = new HeapDiff();
self->Wrap(info.This());
s_inProgress = true;
s_startTime = time(NULL);
self->before = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL);
s_inProgress = false;
info.GetReturnValue().Set(info.This());
}
可以看到用户在 js 层面执行 var hp = new memwatch.HeapDiff(); 的时候,c++ 层面会调用 nodejs 中的 v8 的 api 对对堆上内存打一个 snapshot 保存到 self->before 中,并将当前对象返回出去。
memwatch.HeapDiff.End 实现
当用户执行 hp.end() 的时候,会执行原型链上的 end 方法,也就是 c ++ 的 heapdiff::HeapDiff::End 方法。同样去掉冗余的注释以及宏,End 方法可以精简如下:
NAN_METHOD(heapdiff::HeapDiff::End)
{
Nan::HandleScope scope;
HeapDiff *t = Unwrap<HeapDiff>(info.This() );
if (t->ended) {
return Nan::ThrowError(“attempt to end() a HeapDiff that was already ended”);
}
t->ended = true;
s_inProgress = true;
t->after = v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(NULL);
s_inProgress = false;
v8::Local<Value> comparison = compare(t->before, t->after);
((HeapSnapshot *) t->before)->Delete();
t->before = NULL;
((HeapSnapshot *) t->after)->Delete();
t->after = NULL;
info.GetReturnValue().Set(comparison);
}
在 End 函数中,拿到当前的 HeapDiff 对象之后,再对当前的堆上内存再打一个 snapshot,调用 compare 函数对前后两个 snapshot 对比后得到 comparison 后,将前后两次 snapshot 对象释放掉,并将结果通知给 js。
下面分析下 compare 函数的具体实现:compare 函数内部会递归调用 buildIDSet 函数得到最终堆快照的 diff 结果。
static v8::Local<Value>
compare(const v8::HeapSnapshot * before, const v8::HeapSnapshot * after)
{
Nan::EscapableHandleScope scope;
int s, diffBytes;
Local<Object> o = Nan::New<v8::Object>();
// first let’s append summary information
Local<Object> b = Nan::New<v8::Object>();
b->Set(Nan::New(“nodes”).ToLocalChecked(), Nan::New(before->GetNodesCount()));
//b->Set(Nan::New(“time”), s_startTime);
o->Set(Nan::New(“before”).ToLocalChecked(), b);
Local<Object> a = Nan::New<v8::Object>();
a->Set(Nan::New(“nodes”).ToLocalChecked(), Nan::New(after->GetNodesCount()));
//a->Set(Nan::New(“time”), time(NULL));
o->Set(Nan::New(“after”).ToLocalChecked(), a);
// now let’s get allocations by name
set<uint64_t> beforeIDs, afterIDs;
s = 0;
buildIDSet(&beforeIDs, before->GetRoot(), s);
b->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(s));
b->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked());
diffBytes = s;
s = 0;
buildIDSet(&afterIDs, after->GetRoot(), s);
a->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(s));
a->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(s).c_str()).ToLocalChecked());
diffBytes = s – diffBytes;
Local<Object> c = Nan::New<v8::Object>();
c->Set(Nan::New(“size_bytes”).ToLocalChecked(), Nan::New(diffBytes));
c->Set(Nan::New(“size”).ToLocalChecked(), Nan::New(mw_util::niceSize(diffBytes).c_str()).ToLocalChecked());
o->Set(Nan::New(“change”).ToLocalChecked(), c);
// before – after will reveal nodes released (memory freed)
vector<uint64_t> changedIDs;
setDiff(beforeIDs, afterIDs, changedIDs);
c->Set(Nan::New(“freed_nodes”).ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size()));
// here’s where we’ll collect all the summary information
changeset changes;
// for each of these nodes, let’s aggregate the change information
for (unsigned long i = 0; i < changedIDs.size(); i++) {
const HeapGraphNode * n = before->GetNodeById(changedIDs[i]);
manageChange(changes, n, false);
}
changedIDs.clear();
// after – before will reveal nodes added (memory allocated)
setDiff(afterIDs, beforeIDs, changedIDs);
c->Set(Nan::New(“allocated_nodes”).ToLocalChecked(), Nan::New<v8::Number>(changedIDs.size()));
for (unsigned long i = 0; i < changedIDs.size(); i++) {
const HeapGraphNode * n = after->GetNodeById(changedIDs[i]);
manageChange(changes, n, true);
}
c->Set(Nan::New(“details”).ToLocalChecked(), changesetToObject(changes));
return scope.Escape(o);
}
该函数中构造了两个对象 b(before)、a(after) 用于保存前后两个快照的详细信息。用一个 js 对象描述如下:
// b(before) / a(after)
{
nodes: // heap snapshot 中对象节点个数
size_bytes: // heap snapshot 的对象大小 (bytes)
size: // heap snapshot 的对象大小 (kb、mb)
}
进一步对前后两次的快照进行分析可以得到 o,o 中的 before、after 对象就是前后两次的 snapshot 对象的引用:
// o
{
before: {// before 的堆 snapshot
nodes:
size_bytes:
size:
},
after: {// after 的堆 snapshot
nodes:
size_bytes:
size:
},
change: {
freed_nodes: // gc 掉的节点数量
allocated_nodes: // 新增节点数量
details: [// 按照类型 String、Array 聚合出来的详细信息
{
Array : {
what: // 类型
size_bytes: // 字节数 bytes
size: // kb、mb
+: // 新增数量
-: // gc 数量
}
},
{}
]
}
}
得到两次 snapshot 对比的结果后将 o 返回出去,在 End 函数中通过 info.GetReturnValue().Set(comparison); 将结果传递到 js 层面。
下面来具体说下 compare 函数中的 buildIDSet、setDiff 以及 manageChange 函数的实现。buildIDSet 的用法:buildIDSet(&beforeIDs, before->GetRoot(), s);,该函数会从堆 snapshot 的根节点出发,递归的寻找所有能够访问的子节点,加入到集合 seen 中,做 DFS 统计所有可达节点的同时,也会对所有节点的 shallowSize(对象本身占用的内存,不包括引用的对象所占内存)进行累加,统计当前堆所占用的内存大小。其具体实现如下:
static void buildIDSet(set<uint64_t> * seen, const HeapGraphNode* cur, int & s)
{
Nan::HandleScope scope;
if (seen->find(cur->GetId()) != seen->end()) {
return;
}
if (cur->GetType() == HeapGraphNode::kObject &&
handleToStr(cur->GetName()).compare(“HeapDiff”) == 0)
{
return;
}
s += cur->GetShallowSize();
seen->insert(cur->GetId());
for (int i=0; i < cur->GetChildrenCount(); i++) {
buildIDSet(seen, cur->GetChild(i)->GetToNode(), s);
}
}
setDiff 函数用法:setDiff(beforeIDs, afterIDs, changedIDs); 主要用来计算集合差集用的,具体实现很简单,这里直接贴代码,不再赘述:
typedef set<uint64_t> idset;
// why doesn’t STL work?
// XXX: improve this algorithm
void setDiff(idset a, idset b, vector<uint64_t> &c)
{
for (idset::iterator i = a.begin(); i != a.end(); i++) {
if (b.find(*i) == b.end()) c.push_back(*i);
}
}
manageChange 函数用法:manageChange(changes, n, false);, 其作用在于做数据的聚合。对某个指定的 set,按照 set 中对象的类型,聚合出每种对象创建了多少、销毁了多少,实现如下:
static void manageChange(changeset & changes, const HeapGraphNode * node, bool added)
{
std::string type;
switch(node->GetType()) {
case HeapGraphNode::kArray:
type.append(“Array”);
break;
case HeapGraphNode::kString:
type.append(“String”);
break;
case HeapGraphNode::kObject:
type.append(handleToStr(node->GetName()));
break;
case HeapGraphNode::kCode:
type.append(“Code”);
break;
case HeapGraphNode::kClosure:
type.append(“Closure”);
break;
case HeapGraphNode::kRegExp:
type.append(“RegExp”);
break;
case HeapGraphNode::kHeapNumber:
type.append(“Number”);
break;
case HeapGraphNode::kNative:
type.append(“Native”);
break;
case HeapGraphNode::kHidden:
default:
return;
}
if (changes.find(type) == changes.end()) {
changes[type] = change();
}
changeset::iterator i = changes.find(type);
i->second.size += node->GetShallowSize() * (added ? 1 : -1);
if (added) i->second.added++;
else i->second.released++;
return;
}
upon_gc 和 gc 实现
这两个方法的在 init 函数中声明如下:
Nan::SetMethod(target, “upon_gc”, memwatch::upon_gc);
Nan::SetMethod(target, “gc”, memwatch::trigger_gc);
先看 gc 方法的实现,实际上对应 memwatch::trigger_gc,实现如下:
NAN_METHOD(memwatch::trigger_gc) {
Nan::HandleScope scope;
int deadline_in_ms = 500;
if (info.Length() >= 1 && info[0]->IsNumber()) {
deadline_in_ms = (int)(info[0]->Int32Value());
}
Nan::IdleNotification(deadline_in_ms);
Nan::LowMemoryNotification();
info.GetReturnValue().Set(Nan::Undefined());
}
通过 Nan::IdleNotification 和 Nan::LowMemoryNotification 触发 v8 的 gc 功能。再来看 upon_gc 方法,该方法实际上会绑定一个函数,当执行到 gc 方法时,就会触发该函数:
NAN_METHOD(memwatch::upon_gc) {
Nan::HandleScope scope;
if (info.Length() >= 1 && info[0]->IsFunction()) {
uponGCCallback = new UponGCCallback(info[0].As<v8::Function>());
}
info.GetReturnValue().Set(Nan::Undefined());
}
其中 info[0] 就是用户传入的回调函数。调用 new UponGCCallback 的时候,其对应的构造函数内部会执行:
UponGCCallback(v8::Local<v8::Function> callback_) : Nan::AsyncResource(“memwatch:upon_gc”) {
callback.Reset(callback_);
}
把用户传入的 callback_函数设置到 UponGCCallback 类的成员变量 callback 上。upon_gc 回调的触发与 gc 的钩子有关,详细看下一节分析。
gc 前、后钩子函数的实现
gc 钩子的挂载如下:
Nan::AddGCPrologueCallback(memwatch::before_gc);
Nan::AddGCEpilogueCallback(memwatch::after_gc);
先来看 memwatch::before_gc 函数的实现,内部给 gc 开始记录了时间:
NAN_GC_CALLBACK(memwatch::before_gc) {
currentGCStartTime = uv_hrtime();
}
再来看 memwatch::after_gc 函数的实现,内部会在 gc 后记录 gc 的结果到 GCStats 结构体中:
struct GCStats {
// counts of different types of gc events
size_t gcScavengeCount; // gc 扫描次数
uint64_t gcScavengeTime; // gc 扫描事件
size_t gcMarkSweepCompactCount; // gc 标记清除整理的个数
uint64_t gcMarkSweepCompactTime; // gc 标记清除整理的时间
size_t gcIncrementalMarkingCount; // gc 增量标记的个数
uint64_t gcIncrementalMarkingTime; // gc 增量标记的时间
size_t gcProcessWeakCallbacksCount; // gc 处理 weakcallback 的个数
uint64_t gcProcessWeakCallbacksTime; // gc 处理 weakcallback 的时间
};
对 gc 请求进行统计后,通过 v8 的 api 获取堆的使用情况,最终将结果保存到 barton 中,barton 内部维护了一个 uv_work_t 的变量 req,req 的 data 字段指向 barton 对象本身。
NAN_GC_CALLBACK(memwatch::after_gc) {
if (heapdiff::HeapDiff::InProgress()) return;
uint64_t gcEnd = uv_hrtime();
uint64_t gcTime = gcEnd – currentGCStartTime;
switch(type) {
case kGCTypeScavenge:
s_stats.gcScavengeCount++;
s_stats.gcScavengeTime += gcTime;
return;
case kGCTypeMarkSweepCompact:
case kGCTypeAll:
break;
}
if (type == kGCTypeMarkSweepCompact) {
s_stats.gcMarkSweepCompactCount++;
s_stats.gcMarkSweepCompactTime += gcTime;
Nan::HandleScope scope;
Baton * baton = new Baton;
v8::HeapStatistics hs;
Nan::GetHeapStatistics(&hs);
timeval tv;
gettimeofday(&tv, NULL);
baton->gc_ts = (tv.tv_sec * 1000000) + tv.tv_usec;
baton->total_heap_size = hs.total_heap_size();
baton->total_heap_size_executable = hs.total_heap_size_executable();
baton->req.data = (void *) baton;
uv_queue_work(uv_default_loop(), &(baton->req),
noop_work_func, (uv_after_work_cb)AsyncMemwatchAfter);
}
}
在前面工作完成的基础上,将结果丢到 libuv 的 loop 中,等到合适的实际触发回调函数,在回调函数中可以拿到 req 对象,通过访问 req.data 对其做强制类型装换可以得到 barton 对象,在 loop 的回调函数中,将 barton 中封装的数据依次取出来,保存到 stats 对象中,并调用 uponGCCallback 的 Call 方法,传入字面量 stats 和 stats 对象。
static void AsyncMemwatchAfter(uv_work_t* request) {
Nan::HandleScope scope;
Baton * b = (Baton *) request->data;
// if there are any listeners, it’s time to emit!
if (uponGCCallback) {
Local<Value> argv[2];
Local<Object> stats = Nan::New<v8::Object>();
stats->Set(Nan::New(“gc_ts”).ToLocalChecked(), javascriptNumber(b->gc_ts));
stats->Set(Nan::New(“gcProcessWeakCallbacksCount”).ToLocalChecked(), javascriptNumberSize(b->stats.gcProcessWeakCallbacksCount));
stats->Set(Nan::New(“gcProcessWeakCallbacksTime”).ToLocalChecked(), javascriptNumber(b->stats.gcProcessWeakCallbacksTime));
stats->Set(Nan::New(“peak_malloced_memory”).ToLocalChecked(), javascriptNumberSize(b->peak_malloced_memory));
stats->Set(Nan::New(“gc_time”).ToLocalChecked(), javascriptNumber(b->gc_time));
// the type of event to emit
argv[0] = Nan::New(“stats”).ToLocalChecked();
argv[1] = stats;
uponGCCallback->Call(2, argv);
}
delete b;
}
最后在 Call 函数的内部调用 js 传入的 callback_函数,并将字面量 stats 和 stats 对象传递到 js 层面,供上层用户使用。
void Call(int argc, Local<v8::Value> argv[]) {
v8::Isolate *isolate = v8::Isolate::GetCurrent();
runInAsyncScope(isolate->GetCurrentContext()->Global(), Nan::New(callback), argc, argv);
}