node扩展 memwatch分析

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

内容简介: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);
}
复制代码

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

查看所有标签

猜你喜欢:

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

Coding the Matrix

Coding the Matrix

Philip N. Klein / Newtonian Press / 2013-7-26 / $35.00

An engaging introduction to vectors and matrices and the algorithms that operate on them, intended for the student who knows how to program. Mathematical concepts and computational problems are motiva......一起来看看 《Coding the Matrix》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具