Skip to content
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 node.js bindings #67

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Add node.js bindings #67

wants to merge 7 commits into from

Conversation

beila
Copy link

@beila beila commented Apr 6, 2020

Description of changes:
Node.js apps can access HawkTracer data through 'hawk-tracer-client' package.

Still need to do:

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Still need to do:
- buffering events between reader thread and js thread
- clean up buffered events
- figure out deployment
Copy link
Member

@loganek loganek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beila Thanks a lot for the contribution! Unfortunately I didn't have chance to review the whole code yet, but I'm posting some comments already so you can have a look and respond.

package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
bindings/nodejs/CMakeLists.txt Show resolved Hide resolved
bindings/nodejs/CMakeLists.txt Outdated Show resolved Hide resolved
hawktracer_client_nodejs.hpp
client_context.cpp
client_context.hpp)
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawk_tracer_client SUFFIX .node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawk_tracer_client SUFFIX .node)
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawktracer_client SUFFIX .node)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project name is HawkTracer in README.md. Isn't it two words?

bindings/nodejs/client_context.hpp Outdated Show resolved Hide resolved
bindings/nodejs/hawktracer_client_nodejs.hpp Outdated Show resolved Hide resolved
bindings/nodejs/hawktracer_client_nodejs.hpp Outdated Show resolved Hide resolved
bindings/nodejs/hawktracer_client_nodejs.hpp Outdated Show resolved Hide resolved
bindings/nodejs/hawktracer_client_nodejs.hpp Outdated Show resolved Hide resolved
beila and others added 5 commits April 15, 2020 12:17
- Invert ENABLE_CLIENT condition
- Prefix private member functions with underscore
- Use make_unique
- Remove empty initializer list
- Fix space around asterisk and ampersand
- Use just vector for _buffer
"main": "index.js",
"license": "MIT",
"dependencies": {
"@hawktracer/client": "https://github.com/amzn/hawktracer.git"
Copy link
Author

@beila beila Apr 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until this PR is merged, this can be tested with "https://github.com/beila/hawktracer.git#node_js_support3".

"@types/bindings": "^1.3.0",
"bindings": "^1.3.0",
"cmake-js": "^6.1.0",
"node-addon-api": "https://github.com/nodejs/node-addon-api.git#7f56a78"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this commit because it fixes nodejs/node-addon-api#660 but it's not included in any released version yet.

Copy link
Member

@loganek loganek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think overall we should be focusing on 2 things that are separate:

  • provide low-level node.js bindings which expose raw events as they are and expose API for querying the registry
  • provide high-level helper classes that help users e.g. converting integer labels into strings etc.

At the moment those two functionalities are tightly coupled and the code assumes that user always want to use our helper methods. I think it'd be valuable to separate those two, so the user can access low-level API from nodejs if needed.

Having said that, I think the contribution so far is enormous and extremely valuable. Having current changes we can already build simple clients with nodejs. I think we could work a bit more on having the API more flexible (i.e. exposing low-level APIs) so even more users could benefit from that.


add_executable(hawktracer-converter main.cpp)

target_link_libraries(hawktracer_client hawktracer_parser hawktracer_client_utils)
target_include_directories(hawktracer_client PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)
target_include_directories(hawktracer_client INTERFACE .)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Having target_include_directories(hawktracer_client PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) should be enough.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I only needed when I accessed this from another library, which was reverted before pushing. I'll remove it.

@@ -0,0 +1,67 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this file be placed in bindings/nodejs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be. I'll try.

@@ -0,0 +1,28 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this file be in bindings/nodejs?

"install": "cmake-js compile --target nodejs_bindings_client --CDENABLE_CLIENT=YES --CDENABLE_NODEJS_BINDINGS=YES --CDBUILD_STATIC_LIB=YES"
},
"dependencies": {
"@types/bindings": "^1.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it dev dependency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, the typescript files are built at the time of installation, not deployment. So this will have to be installed for the dependent packages too.

"dependencies": {
"@types/bindings": "^1.3.0",
"bindings": "^1.3.0",
"cmake-js": "^6.1.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it dev dependency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, the c++ files are built at the time of installation, not deployment. So this will have to be installed for the dependent packages too.


function isEventKlassInfoEvent(event: BareEvent): event is EventKlassInfoEvent {
const e = event as EventKlassInfoEvent;
return e.info_klass_id !== undefined && e.event_klass_name !== undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comparison is strong enough. Somebody might define a custom event with those two fields, which is not eventklassinfo event. We should be rather using klass_id for comparison to make sure this is the right class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to avoid depending on global state, since it's a special type of functions called type guards.
While it may limit the usability a bit, I'll change it to check for klass_id and make it normal function.


export function isCallstackEvent(event: CallstackEvent): event is CallstackEvent {
const e = event as CallstackEvent;
return e.duration !== undefined && e.thread_id !== undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be rather looking at klass_id (or klass_name) to make sure that the event is callstackevent

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do the same as isEventKlassInfoEvent.

const events = bareEvents as Event[];
events.forEach(e => {
// @ts-ignore
e.klass_name = this._klass_names[e.klass_id];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of keeping duplicated registry in javascript, could we copy rely on eventklassregistry from native code?

o.Set(it.first, _convert_field_value(env, it.second));
}
if (!event.get_label_string().empty()) {
o.Set("label", String::New(env, event.get_label_string()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that's the right approach; the label might be an actual field in the event, and we probably shouldn't be replacing it. It's done in the native library (client_utils) because client_utils was supposed to be used only for the end-user applications. Since we're building a nodejs bindings (but not end-user application), I'm not sure we should be manipulating fields, as this is something the customer might not want.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label is defined in core_events.h, not by the user.
How about doing this only for HT_CallstackIntEvent where it's predefined?
When the map files are provided, the user clearly wants to see the string labels, not the int labels.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole mapping thing is more like a client feature, not a parser feature. I'd recommend to provide bindings only for parser first, and we might extend it to client if needed.

const events = bareEvents as Event[];
events.forEach(e => {
// @ts-ignore
e.klass_name = this._klass_names[e.klass_id];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if event already has klass_name field? Will that be overwritten?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be overwritten.
I think it's a trade-off between data-safety and usability.
As I mentioned in another comment, for javascript/typescript users, it feels unnatural and uncomfortable to get the type information from somewhere else other than the object itself.
I considered having a generic function that returns the event only when the klass matches, but it would take some time to write, at least for me.
Plus, the term "klass" is being used several times in predefined events in hawktracer. I don't see it being used frequently here.
So my decision came from that it'd be really useful, and that in only rare cases user data would be overwritten.
How about checking whether the event already has "klass_name" field and avoid overwriting it, and just log a warning in such a case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might consider making the C++ layer as thin as possible, expose C++ interfaces almost 'as they are' to JS, and then on top of the low-level JS binding layer, build more user-friendly JS layer, so in case somebody needs access to low-level bindings, can have it.

@beila
Copy link
Author

beila commented May 11, 2020

I think overall we should be focusing on 2 things that are separate:

  • provide low-level node.js bindings which expose raw events as they are and expose API for querying the registry
  • provide high-level helper classes that help users e.g. converting integer labels into strings etc.

At the moment those two functionalities are tightly coupled and the code assumes that user always want to use our helper methods. I think it'd be valuable to separate those two, so the user can access low-level API from nodejs if needed.

Having said that, I think the contribution so far is enormous and extremely valuable. Having current changes we can already build simple clients with nodejs. I think we could work a bit more on having the API more flexible (i.e. exposing low-level APIs) so even more users could benefit from that.

I don't think having raw events APIs would actually help users. JavaScript/TypeScript users are already accustomed to checking the existence of properties. Checking if a particular property exists is much more familiar and convenient than getting the type information from somewhere else than the object itself.
Having said that, even though we decide to provide the raw events APIs, I think it'd better be done in another PR.

@beila
Copy link
Author

beila commented May 30, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants