-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add object_async example #52
Conversation
Codecov Report
@@ Coverage Diff @@
## master #52 +/- ##
==========================================
+ Coverage 92.85% 93.23% +0.37%
==========================================
Files 5 8 +3
Lines 70 133 +63
==========================================
+ Hits 65 124 +59
- Misses 5 9 +4
Continue to review full report at Codecov.
|
src/object_async/hello_async.cpp
Outdated
v8::Local<v8::Value> argv[1] = { Nan::Error(message.c_str()) }; | ||
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), callback, 1, argv); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this same function getting repeated in multiple .cpp
files. How about we refactor the code to move it to a new header file called module_utils.hpp
? Then define it there, include module_utils.hpp
in each of the cpp
files that uses it, and that will reduce the code duplication.
Note: for fun context because of the inline
keyword is used the compiler is doing away with this function in the binary code. When something is "inlined" it is re-arranged and optimized. So the v8::Local<v8::Value>...
and Nan::MakeCallback...
lines are being moved to where the code is calling CallbackError
. This means that moving the source code into a common header will only impact how the source code looks. The final binary will probably not change at all because of how the compiler works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@springmeyer so inline
is great for reusable methods within a file? And a util header is one step further than that, great for reusable methods within an application/directory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@springmeyer so inline is great for reusable methods within a file?
- inlining, when in a cpp file, may help performance.
- inlining, when in an hpp file, may help performance and may also be absolutely necessary to allow the header to be included from multiple cpp files when the code in the header includes both the definition and the implementation (which a "header-only" library does). And example of this being fixed is Non-inline function definitions in header file vector-tile#19.
So, inlining is important to understand because it impacts performance, code size, and is critical to header-only library design.
And a util header is one step further than that, great for reusable methods within an application/directory
Exactly. If you want to share code in multiple .cpp files then you need to put at least definitions for it in an .hpp file (and, in the case of header-only design, also the implementation)
Ticketed adding this to the C++ glossary at mapbox/cpp#28
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finished at mapbox/cpp#29
src/object_async/hello_async.cpp
Outdated
// Mention anything about "Unwrap"? | ||
HelloObjectAsync* h = Nan::ObjectWrap::Unwrap<HelloObjectAsync>(info.Holder()); | ||
|
||
std::string name = h->name_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor issue: this line is resulting in an uncessary copy. We are copying the h->name_
to a new variable named name
. Instead, down below you could do:
new AsyncHelloWorker{louder, h->name_, new Nan::Callback{callback}};
That would avoid the copy. Or alternatively you could do:
std::string const& name = h->name_;
That would also avoid the copy by making the name
variable a reference to the h->name_
.
Why does this matter?
In short, performance. If this code needed to be as fast as possible we want to avoid all possible copies. Because copies require allocation (or re-allocation of memory) for the new variables space. And std::string
allocates memory on the heap (internally) (https://github.com/mapbox/cpp/blob/master/glossary.md#heap-allocation) when newly created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're pointing out string copies here - not sure if it makes sense to use move-semantics throughout this example (e.g. to move the string into the async worker). Maybe it just makes it more complicated than it has to be..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 great suggestion @daniel-j-h - I agree that move semantics here would likely make the most sense given that the goal of node-cpp-skel is to set developers up for writing high performance code.
src/object_async/hello_async.cpp
Outdated
|
||
// Expensive allocation of std::map, querying, and string comparison, | ||
// therefore threads are busy | ||
std::string do_expensive_work(bool louder, std::string name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid a copy here pass by reference like:
std::string do_expensive_work(bool louder, std::string const& name) {
src/object_async/hello_async.cpp
Outdated
|
||
using Base = Nan::AsyncWorker; | ||
|
||
AsyncHelloWorker(bool louder, std::string name, Nan::Callback* callback) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another spot where we can avoid a copy:
Best to pass by reference like:
AsyncHelloWorker(bool louder, std::string const& name, Nan::Callback* callback)
src/object_async/hello_async.cpp
Outdated
else { | ||
auto *const self = new HelloObjectAsync(); | ||
self->Wrap(info.This()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a more realistic thing would be to throw an error if no argument is passed. Currently we default to world
below. But I think instead we should throw a JS error here if no name
arg is passed. What do you think?
Yes, perfectly. We are passing in a custom class that holds a custom data member. That member is safe to pass into the threadpool because it is not a V8 object, but rather a
You mean the custom constructor that accepts the
It is. Well done!
We spoke about this voice. The short version is that
|
{ | ||
return Nan::ThrowTypeError( | ||
"Cannot call constructor as function, you need to use 'new' keyword"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could handle both new T()
as well as T()
by adding this to the function's beginning:
if (!info.IsConstructCall()) {
auto init = Nan::New(create_once());
info.GetReturnValue().Set(init->NewInstance());
return;
}
/*continue with New function impl*/
Not sure if we want to do that, though.
If not we could flatten the control flow here by exiting early on !IsConstructorCall()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@daniel-j-h - really great catch 🙇 . I agree that handling the case where a user forgets to use new
in Javascript is important. I'm aware that some JS developers like to hide the complexity and allow object creation to work without new
. But my preference would be to throw since this is a consistent pattern that has worked in other node C++ modules I have written. And I've never seen a complaint about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to lean towards being more explicit here and requiring new
src/object_async/hello_async.cpp
Outdated
// Mention anything about "Unwrap"? | ||
HelloObjectAsync* h = Nan::ObjectWrap::Unwrap<HelloObjectAsync>(info.Holder()); | ||
|
||
std::string name = h->name_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're pointing out string copies here - not sure if it makes sense to use move-semantics throughout this example (e.g. to move the string into the async worker). Maybe it just makes it more complicated than it has to be..
Thank you so much for your input @daniel-j-h @springmeyer 🙌 I've updated the PR with the optimization suggestions and comments/links for context. Once tests are 🍏 , I will merge! Feel free to add any other comments if I missed anything. |
This object-async example builds off the async standalone example and the object-sync example
Next Actions
node::ObjectWrap
and how it relates toUnwrap
Questions
node::ObjectWrap
and how it relates toUnwrap
Any other thoughts/comments/edits?
cc @springmeyer @daniel-j-h @flippmoke