介绍

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.ccinit.ccmemwatch.ccutil.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.exportsexports对象。函数内部做的实现可以分为三块,初始化target、给target绑定upon_gcgc两个函数、在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 informationLocal<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 nameset<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 informationchangeset changes;// for each of these nodes, let's aggregate the change informationfor (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的堆snapshotnodes:size_bytes:size: },after: { // after的堆snapshotnodes:size_bytes:size: },change: {freed_nodes: // gc掉的节点数量allocated_nodes: // 新增节点数量details: [ // 按照类型String、Array聚合出来的详细信息{Array : {what: // 类型size_bytes: // 字节数bytessize: // 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_gcgc实现

这两个方法的在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::IdleNotificationNan::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 eventssize_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 emitargv[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);
}
复制代码

node扩展 memwatch分析相关推荐

  1. Sitecore 8.2 扩展体验分析报告

    本文简要介绍了如何为Experience Analytics创建自定义报告.在Sitecore术语中,我会说:创建新的报表维度和适当的报表以显示它们. 我们做的任务是:实现新的报告,显示不同网络浏览器 ...

  2. node-gyp编译c++编写的node扩展

    node有一个模块addon,翻译过来,是插件,但是有的地方也叫扩展,这部分是用c++来编写的,最后可以通过node-gyp来针对各个平台编译适合自己平台的扩展,做到了跨平台.而编译后的这部分,nod ...

  3. memwatch分析

    介绍 memwatch是一个c++扩展,主要用来观察nodejs内存泄露问题,基本用法如下: const memwatch = require('@airbnb/memwatch'); functio ...

  4. 记录node内存瓶颈分析

    注:仅仅是一篇纪实性文章! 概述:不知道因为什么原因,在某个项目新申请的两台服务器上将node版本从6.10.0升级到了6.10.1,发现在这两台机器上一个node进程占用内存一直再涨,启动10h左右 ...

  5. node 常用指令 node 扩展链接

    node -v       node 版本 npm -v     npm版本号,npm是在安装nodejs时一同安装的nodejs包管理器  (注册.安装模块,和小乌龟有点像) npm list  当 ...

  6. 使用V8和node轻松profile分析nodejs应用程序

    文章目录 简介 使用V8的内置profiler工具 使用gm来build V8 手动build V8 生成profile文件 分析生成的文件 生成时间线图 使用nodejs的profile工具 简介 ...

  7. 思科VPP 20.05 dpdk node源码分析

    目录 - 基本概念 - 核心函数 VPP使用者几乎都会使用dpdk node作为收包驱动,本文将分析其源码. - 基本概念 vlib_buffer_t dpdk收到的数据包用rte_mbuf结构描述. ...

  8. Spring Boot 集成SnakerFlow流程引擎,简介、功能列表、详细解读、扩展点分析

    文章目录 简介 功能列表 流程定义 任务参与者 参与者设置 动态添加.删除参与者 组支持 详细解读 Spring Boot集成 表定义 表详细说明: 字段详细说明: 常见操作 常规API 综合查询 模 ...

  9. 综合FuSa和SOTIF的无人驾驶扩展HARA分析方法

    基于场景的危害分析和风险评估(HARA)是一种有效且现实的自动驾驶能力识别方法(例如SAE/NHTSA自动驾驶分级).对于自动驾驶,当系统通过传感器(融合)和执行器与环境发生深度交互时,必须将危险因素 ...

最新文章

  1. Exchange 2010安装前的准备工作
  2. vim编辑器全部删除文件内容
  3. python 实现81个人脸关键点实时检测
  4. 2011.8.2号面试
  5. 机器学习系列-随机过程
  6. org.hibernate.transientobjectexception:The given object has a null identifier: com.gxuwz.check.entit
  7. C++ map的简单实现
  8. 语音识别技术是什么_语音识别技术应用领域介绍
  9. 数学归纳法证明求和公式
  10. 论文阅读-多任务(2021)-YOLOP:用于自动驾驶目标检测与语义分割的实时多任务模型
  11. jquery+baidu map api 仿安居客地图找房源(基于百度地图)
  12. MTK6580适应小分辨率
  13. VerilogHDL正弦信号发生器
  14. 你为你的BLOG找了经纪人了吗?
  15. 试读2-《白话C++ 练功篇》目录
  16. Python+OpenCV实用案例应用教程:基于OpenCV的图像处理
  17. 硬币面值组合(上台阶)
  18. 计算机科学导论在线作业,南开21春学期《计算机科学导论》在线作业
  19. 集群(Cluster)
  20. matlab符号表达式vpa,对MATLAB中符号和数值型数据以及sym(),sym(''),sym(,'d'),vpa()的理解【更新版】...

热门文章

  1. 中考计算机考试评分标准,2021北京中考英语听说机考题型分值及满分技巧
  2. java中字符串判断相等能用不等号吗
  3. Springboot注解@Target用法
  4. target和currentTarget的区别
  5. win10 下tomcat 启动startup.bat闪退解决方法
  6. 中医理论质疑文章集锦
  7. 保研计算机英语词汇,简单的英语自我介绍保研面试
  8. php中引号,PHP中引号的用法
  9. WPF WrapPanel:自动折行面板
  10. 去掉服务器硬盘告警声音,某局点服务器硬盘误拔出导致硬盘告警