BSON to v8::Object performance issue

79 views
Skip to first unread message

Jean-Marc Le Roux

unread,
Nov 14, 2017, 2:46:20 PM11/14/17
to v8-users
Hello,

I'm trying to create NodeJS bindings for libbson.
One of the things I need is to be able to transform a bson_t document into a v8::Object.
Loading, iterating on bson_t documents and matching them with MongoDB-style queries it very fast: 50ms to load from my HDD, iterate and filter 26000 bson_t documents.
On the same machine, converting the same BSON documents into v8::Object is painfully slow.
So I suspect I missed something.

Here is my code:

Local<Value>
iterator_to_value(Isolate* isolate, const bson_iter_t* iter)
{
    switch (bson_iter_type(iter))
    {
    case BSON_TYPE_INT32:
        return Number::New(isolate, bson_iter_int32(iter));
        break;
    case BSON_TYPE_INT64:
        return Number::New(isolate, bson_iter_int64(iter));
        break;
    case BSON_TYPE_DOUBLE:
        return Number::New(isolate, bson_iter_double(iter));
        break;
    case BSON_TYPE_DOCUMENT:
        {
            bson_iter_t sub_iter;
            bson_iter_recurse(iter, &sub_iter);

            Local<Object> obj = Object::New(isolate);
            while (bson_iter_next(&sub_iter))
            {
                const char* key = bson_iter_key(&sub_iter);

                obj->Set(
                    String::NewFromUtf8(isolate, key),
                    iterator_to_value(isolate, &sub_iter)
                );
            }

            return obj;
        }
        break;
    case BSON_TYPE_ARRAY:
        {
            bson_iter_t sub_iter;
            uint32_t length;
            const uint8_t* array_data;

            bson_iter_array(iter, &length, &array_data);
            bson_iter_recurse(iter, &sub_iter);

            Local<Array> array = Array::New(isolate);
            int i = 0;

            while (bson_iter_next(&sub_iter))
            {
                array->Set(i++, iterator_to_value(isolate, &sub_iter));
            }

            return array;
        }
        break;
    case BSON_TYPE_OID:
        {
            const bson_oid_t* oid = bson_iter_oid(iter);
            char oid_buffer[25];

            bson_oid_to_string(oid, oid_buffer);

            return String::NewFromOneByte(isolate, (uint8_t*)oid_buffer);
        }
        break;
    case BSON_TYPE_UTF8:
        {
            uint32_t length;
            return String::NewFromUtf8(isolate, bson_iter_utf8(iter, &length));
        }
        break;
    }

    return Null(isolate);    
}

Local<Value>
fill_object(Isolate* isolate, Local<Object>& obj, const bson_t* document)
{
    bson_iter_t iter;
    if (bson_iter_init(&iter, document))
    {
        while (bson_iter_next(&iter))
        {
            const char* key = bson_iter_key(&iter);

            obj->Set(
                String::NewFromUtf8(isolate, key),
                iterator_to_value(isolate, &iter)
            );
        }
    }

    return obj;
}

As you can see, it's very straight forward.
Transforming 26400 bson_t into v_::Object takes 1.23s.

I tried doing what I believe to be the same code in pure JS with a representative sample Object :

var array = [];
for (var i = 0; i < 25000; ++i)
{
    array.push({
        "_id": {
          "$oid": "5a00bad8f759511811e030ba"
        },
        "attributes": [
          {
            "key": "smartshape.scene.node.path|default",
            "value": "/scene/Lot444.scene",
            "type": "imported"
          },
          {
            "key": "max x",
            "value": 196.5773162841797,
            "type": "computed"
          },
          {
            "key": "max y",
            "value": 18.55002021789551,
            "type": "computed"
          },
          {
            "key": "max z",
            "value": 22.87815856933594,
            "type": "computed"
          },
          {
            "key": "min x",
            "value": 149.9346771240234,
            "type": "computed"
          },
          {
            "key": "min y",
            "value": 18.54999732971191,
            "type": "computed"
          },
          {
            "key": "min z",
            "value": -23.35353088378906,
            "type": "computed"
          },
          {
            "key": "box radius",
            "value": 23.32131958007814,
            "type": "computed"
          },
          {
            "key": "center x",
            "value": 173.25599670410156,
            "type": "computed"
          },
          {
            "key": "center y",
            "value": 18.55000877380371,
            "type": "computed"
          },
          {
            "key": "center z",
            "value": -0.23768615722655895,
            "type": "computed"
          },
          {
            "key": "width",
            "value": 46.64263916015628,
            "type": "computed"
          },
          {
            "key": "height",
            "value": 2.2888183600855427e-05,
            "type": "computed"
          },
          {
            "key": "depth",
            "value": 46.231689453125,
            "type": "computed"
          },
          {
            "key": "box volume",
            "value": 0.04935534689932106,
            "type": "computed"
          },
          {
            "key": "box surface",
            "value": 4312.740269302394,
            "type": "computed"
          }
        ],
        "name": "default1161",
        "uuid": "70bf7d72-1fa9-5c8f-21ff-03ef209d4404",
        "surfaces": [
          "6f5201a2-31a7-14d0-6b2a-5130007d2a51"
        ],
        "aabb": [
          196.5773162841797,
          18.55002021789551,
          22.87815856933594,
          149.9346771240234,
          18.54999732971191,
          -23.35353088378906
        ],
        "position": null,
        "scale": null,
        "rotation": null,
        "parents": [
          "18497b66-3f32-6e98-1899-2998203e6397",
          null
        ],
        "file": "5a00b9a4aa5d2517d32d03da",
        "numChildren": 0,
        "sceneTreeIndices": [
          0
        ]
      });
}

This pure JS version only takes 0.14s.

Why is my C++ code 10x slower than the equivalent JS code?
I suspect it has something to do with memory management/GC.

Thanks!


Jakob Kummerow

unread,
Nov 14, 2017, 3:25:34 PM11/14/17
to v8-users
There are a bunch of optimizations going into object literal handling, so the two versions are not at all equivalent: the JS version is expected to be much faster. To make them more comparable, you could model the same control flow in JS, roughly:

var input = {
    "_id": {
        "$oid": "5a00bad8f759511811e030ba"
    },
    "attributes": ...
};
function process(o) {
    var result = {};
    for (var key of Object.keys(o)) {
        var value = o[key];
        if (typeof value === "Number") {
            ...
        } else if ... {
            ....
        } else if (typeof value === "object") {
            result[key] = process(value);
        }
    return result;
}
var output;
for (var i = 0; i < 26400; i++) {
    output = process(input);
}

I don't see anything obviously wrong with your code.

However, if there are any assumptions you can make, maybe you can optimize it. For example: do all your objects have the same shape? If so, using a constructor function and just filling in the values should be faster than assembling them one property at a time.


--
--
v8-users mailing list
v8-u...@googlegroups.com
http://groups.google.com/group/v8-users
---
You received this message because you are subscribed to the Google Groups "v8-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to v8-users+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jean-Marc Le Roux

unread,
Nov 15, 2017, 4:27:26 AM11/15/17
to v8-users
Thanks for the quick feedback!

I rewrote the JS version to be closer to the native one:

var array = [];
var input = {
  "_id": {
    "$oid": "5a00bad8f759511811e030ba"
  },
};

function process(value) {
  if (typeof value === "number") {
    return value;
  } else if (typeof value === "string") {
    return value;
  } else if (Array.isArray(value)) {
    var array = [];

    for (var item of value) {
      array.push(process(item));
    }

    return array;
  } else if (typeof value === "object") {

    if (!value) {
      return value;
    }

    var obj = {};
    for (var key of Object.keys(value)) {
      obj[key] = process(value[key]);
    }

    return obj;
  } else {
    return value;
  }

  return value;
}

for (var i = 0; i < 26000; ++i) {
  var obj = process(input);

  array.push(obj);
}



Now it takes 30ms. That's still 5x faster than the native code.

I'm suspecting my Local<Object> are copied/GCed multiple time to build the final object.
Isn't that the case?
Is there a better way to do it?

using a constructor function and just filling in the values should be faster than assembling them one property at a time.

The root object is expected to be the same 99% of the time.
Each of his field too.
So I could definitely have a map<key, function>.
How can I implement such a constructor function in v8 ?

Thanks !

Jean-Marc Le Roux

unread,
Nov 15, 2017, 4:44:39 AM11/15/17
to v8-u...@googlegroups.com
Now it takes 30ms. That's still 5x faster than the native code.

300ms. (not 30)

You received this message because you are subscribed to a topic in the Google Groups "v8-users" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/v8-users/aVeevQcHJ2c/unsubscribe.
To unsubscribe from this group and all its topics, send an email to v8-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Jean-Marc Le Roux


Founder and CEO of Aerys (http://aerys.in)

Jean-Marc Le Roux

unread,
Nov 15, 2017, 11:37:09 AM11/15/17
to v8-users
So I've found a solution to make things lazy: I set an accessor instead of decoding/setting the actual value.

void
getter(Local<Name> property, const PropertyCallbackInfo<Value>& info)
{
    Isolate* isolate = info.GetIsolate();

      const bson_t* document = reinterpret_cast<bson_t*>(
        info.This()
            ->Get(String::NewFromUtf8(isolate, "__bson_document_ptr"))
            ->ToInt32()->Value()
    );

    char key[255];
    property->ToString(isolate)->WriteUtf8(key, 255);

    bson_iter_t iter;
    bson_iter_init(&iter, document);
    // FIXME: index the property so we don't have to find it
    bson_iter_find(&iter, key);

    // FIXME: replace the accessor with the deserialized value

    info.GetReturnValue().Set(iterator_to_value(isolate, &iter));
}

Local<Value>
fill_object(Isolate* isolate, Local<Object>& obj, const bson_t* document)
{
    obj->Set(
        String::NewFromUtf8(isolate, "__bson_document_ptr"),
        Int32::New(isolate, reinterpret_cast<intptr_t>(document))
    );

    bson_iter_t iter;
    if (bson_iter_init(&iter, document))
    {
        while (bson_iter_next(&iter))
        {
            const char* key = bson_iter_key(&iter);

                        if (!obj->Has(String::NewFromUtf8(isolate, key))) 
            {
                obj->SetAccessor(
                    isolate->GetCurrentContext(),
                    String::NewFromUtf8(isolate, key),
                    &getter
                );
            }
                }
    }
}

The secret sauce is to :
  • keep the original data (the bson_t) allocated
  • store the corresponding pointer in a v8::Object
But now I have a memory leak because those pointers will never be freed.
How can I know when the Object will be disposed by the GC?

Thanks,

J Decker

unread,
Nov 15, 2017, 1:50:52 PM11/15/17
to v8-u...@googlegroups.com
I assume you're storing references to the objects as Persistent?  All you need to do then is call SetWeak() https://v8docs.nodesource.com/node-0.10/d2/d78/classv8_1_1_persistent.html
void MakeWeak (void *parameters, WeakReferenceCallback callback)

the callback is called when the object is GC'd.


--
You received this message because you are subscribed to the Google Groups "v8-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to v8-users+unsubscribe@googlegroups.com.

Jean-Marc Le Roux

unread,
Nov 16, 2017, 4:24:56 AM11/16/17
to v8-u...@googlegroups.com
I assume you're storing references to the objects as Persistent?

Nope. You've got the whole code up there.
So if I understand correctly:
  • the objects I need to keep track of the lifecycle should be made Persistent ;
  • all the other values can be set as Local ;
  • I should set the weak callback using MakeWeak ;
  • callback will be called when such object is GCed and I can free my C/C++ memory then.
Is that correct ?

You received this message because you are subscribed to a topic in the Google Groups "v8-users" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/v8-users/aVeevQcHJ2c/unsubscribe.
To unsubscribe from this group and all its topics, send an email to v8-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Jean-Marc Le Roux


Founder and CEO of Aerys (http://aerys.in)

J Decker

unread,
Nov 16, 2017, 12:36:43 PM11/16/17
to v8-u...@googlegroups.com
On Thu, Nov 16, 2017 at 1:24 AM, Jean-Marc Le Roux <jeanmar...@aerys.in> wrote:
I assume you're storing references to the objects as Persistent?

Nope. You've got the whole code up there.
So if I understand correctly:
  • the objects I need to keep track of the lifecycle should be made Persistent ;
  • all the other values can be set as Local ;
  • I should set the weak callback using MakeWeak ;
  • callback will be called when such object is GCed and I can free my C/C++ memory then.
Is that correct ?

Yes.  I sthis perhaps related to Node?
Node offers a C++ Clss extention ObjectWrap 


which simplifies that whole process... when the object is GC'd the class destructor will get called so you can release values there.

Jean-Marc Le Roux

unread,
Nov 16, 2017, 12:57:04 PM11/16/17
to v8-u...@googlegroups.com
Node offers a C++ Clss extention ObjectWrap 

Thanks !
It is related to node. Yet I'm not sure how I'm supposed to use ObjectWrap.

I understand I have to create my own class that extends ObjectWrap and implement a proper destructor.
Then I'll instanciate that class to wrap my Local<Object>. But how do I manage such instance?
If I don't keep it somewhere, it will be destructed.
If I keep it around, then I'm keeping references to potentially deallocated memory.

It feels like I end up with the same problem...

J Decker

unread,
Nov 16, 2017, 4:01:31 PM11/16/17
to v8-u...@googlegroups.com
You don't manage the instance, the v8 engine does.  When it's no longer referenced in the engine, it's garbage collected which triggers the callback set when the perisstent object is made weak.

You don't need to keep it anywhere, the object returned into the engine has Internal Fields ( SetInternalFieldCount(1) ) which is used to point to your native class instance.   So when the callback is called, it knows what to call to destruct it.

You have to 'keep it' in the javascript scrpit if you want to keep it, it's not 'kept' in the native code...


--- This is some code I've done that uses setweak directly; but it's fairly scattered....


You can create an instance of your wrapped object with

Local<Function> cons = Local<Function>::New( isolate, constructor );
    cons->NewInstance( isolate->GetCurrentContext(), argc, argv      ).ToLocalChecked() ;

Hmm that's not the best example source... 

This creates a buffer, and makes it weak... 

Local<Object> arrayBuffer = ArrayBuffer::New( isolate, buf, len );
PARRAY_BUFFER_HOLDER holder = GetHolder();
holder->o.Reset( isolate, arrayBuffer );
holder->o.SetWeak<ARRAY_BUFFER_HOLDER>( holder, releaseBuffer, WeakCallbackType::kParameter );
holder->buffer = buf;

PARRAY_BUFFER_HOLDER is just a type that holds the thing to be free (or object containing things to be released), and the persistent reference that has been made weak...

struct arrayBufferHolder {
void *buffer;
Persistent<Object> o;
};


Jean-Marc Le Roux

unread,
Nov 17, 2017, 4:27:22 AM11/17/17
to v8-u...@googlegroups.com
Thanks for the great help!
I think it makes a lot of sense now.

So I've extended ObjectWrap like so:

class BSONObject : public node::ObjectWrap
{
private:
    bson_t* _bson;
public:
    BSONObject(bson_t* bson, Local<ObjectTemplate> tpl) :
        _bson(bson)
    {
        Local<Object> obj = tpl->NewInstance();
        initialize(v8::Isolate::GetCurrent(), obj, bson);
        Wrap(obj);
    }
 
 ~BSONObject()
    {
        std::cout << "~BSONObject()" << std::endl;
        bson_destroy(_bson);
    }
 
    static
    Local<ObjectTemplate>
    create_template(Isolate* isolate, const bson_t* document)
    {
        Local<ObjectTemplate> tpl = ObjectTemplate::New(isolate);
        tpl->SetInternalFieldCount(1); 

And the "insert" function that I expose in JS and that instanciates the BSONObject is written like this:

void
insert(const FunctionCallbackInfo<Value>& args)
{
    Isolate* isolate = args.GetIsolate();
    
    db->insert(
        json_stringify(isolate, args[0]),
        [&args, isolate](const bson_t* document)
        {
            if (!args[1]->IsUndefined())
            {
                Local<Function> callback = Local<Function>::Cast(args[1]);

                Local<Object> obj = Object::New(isolate);
                Local<Value> argv[] = { (new BSONObject(bson_copy(document)))->handle(isolate) };
                callback->Call(isolate->GetCurrentContext()->Global(), 1, argv);
            }
        }
    );
}

It runs fine.
But the destructor is never called (I never see "~BSONObject()" on stdout).

Did I miss something?

Thanks !

Jean-Marc Le Roux

unread,
Nov 17, 2017, 10:25:00 AM11/17/17
to v8-users
My assumption is that my test app terminates before the GC has a chance to run.
To test this:
  • I run node with the --expose-gc option
  • I call global.gc() at the end of my test app
In this case, the destructor of my ObjectWrap is indeed called.

So it seams the GC doesn't simply "collect everything" when the app terminates.
Why is that?

It sounds like a pretty bad things to do: some behaviors might expect the destructor to be called in order to close a socket, etc...

J Decker

unread,
Nov 17, 2017, 12:21:07 PM11/17/17
to v8-u...@googlegroups.com
On Fri, Nov 17, 2017 at 7:25 AM, Jean-Marc Le Roux <jeanmar...@aerys.in> wrote:
My assumption is that my test app terminates before the GC has a chance to run.
To test this:
  • I run node with the --expose-gc option
  • I call global.gc() at the end of my test app
In this case, the destructor of my ObjectWrap is indeed called.

So it seams the GC doesn't simply "collect everything" when the app terminates.
Why is that?

It sounds like a pretty bad things to do: some behaviors might expect the destructor to be called in order to close a socket, etc...

because all memory is released. when the app exits.  Why do extra work of minor side effects?
but ya, if tf you were returning like a file to a handle and expected the close in order to flush all data out to a device it's bad... 

You can attach to Node::AtExit( ... ) 

which should be called so you an finalize such things.

but sockets get closed by the system when a application exits also.  and files are closed....

Jean-Marc Le Roux

unread,
Nov 17, 2017, 12:22:40 PM11/17/17
to v8-u...@googlegroups.com
Thank you!

Great help here. Amazing folks!
Reply all
Reply to author
Forward
0 new messages