node扩展 memwatch分析

栏目: C++ · 发布时间: 5年前

内容简介:memwatch是一个c++扩展,主要用来观察nodejs内存泄露问题,基本用法如下:分析的版本为这份配置表示其生成的目标是

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 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_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 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);
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从零开始做产品经理

从零开始做产品经理

萧七公子 / 中国华侨出版社 / 2016-12-1 / 27.9

《从零开始做产品经理:产品经理的第一本书》根据产品经理的能力需求与成长体系,共分为八章内容,从了解产品开始,到挖掘用户需求、进行产品设计、管理团队、进行项目管理、产品运营、把握产品的生命周期,以及产品经理的成长路径,全面阐释了产品经理的修炼之道。《从零开始做产品经理:产品经理的第一本书》书中信息量大,图文并茂,论点与论据相得益彰,并且融合了丰富的案例与故事,使得整个阅读过程妙趣横生而且迅速开“悟道......一起来看看 《从零开始做产品经理》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具