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
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.idea/
build
.vscode
node_modules
dist
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ option(ENABLE_EXAMPLES "Enable examples" OFF)
option(ENABLE_DOC "Build documentation" ON)
option(ENABLE_CLIENT "Enable client application for parsing HawkTracer stream" ON)
option(ENABLE_PYTHON_BINDINGS "Enable Python bindings (requires ENABLE_CLIENT=ON)" OFF)
option(ENABLE_NODEJS_BINDINGS "Enable Node.js bindings (requires ENABLE_CLIENT=ON)" OFF)
option(ENABLE_TCP_LISTENER "Enable TCP listener in a core library" ON)

option(ENABLE_POSITION_INDEPENDENT_CODE "Enable position independent code of
Expand Down
4 changes: 4 additions & 0 deletions bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
if(ENABLE_PYTHON_BINDINGS)
add_subdirectory(python3)
endif()

if(ENABLE_NODEJS_BINDINGS)
add_subdirectory(nodejs)
endif()
46 changes: 46 additions & 0 deletions bindings/nodejs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
if (NOT ENABLE_CLIENT)
# Currently, we only support client bindings.
return()
endif()

# https://www.npmjs.com/package/cmake-js#general
if (CMAKE_JS_SRC)
beila marked this conversation as resolved.
Show resolved Hide resolved
add_library(cmake_js STATIC ${CMAKE_JS_SRC})
else()
add_library(cmake_js INTERFACE)
endif()
target_include_directories(cmake_js INTERFACE SYSTEM ${CMAKE_JS_INC})
target_link_libraries(cmake_js ${CMAKE_JS_LIB})

# https://www.npmjs.com/package/cmake-js#n-api-and-node-addon-api
add_library(node_addon_api INTERFACE)
if (NOT NODE_RUNTIME)
find_program(NODE_RUNTIME NAMES node HINTS /usr)
endif()
execute_process(COMMAND ${NODE_RUNTIME} -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
if (NOT NODE_ADDON_API_DIR)
message(FATAL_ERROR "Failed to find node-addon-api headers with node command \"${NODE_RUNTIME}\"")
endif()
string(REPLACE "\n" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
string(REPLACE "\"" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
target_include_directories(node_addon_api INTERFACE SYSTEM ${NODE_ADDON_API_DIR})
target_link_libraries(node_addon_api INTERFACE cmake_js)

# Supports Node version >= 12.11.0 https://nodejs.org/api/n-api.html#n_api_n_api_version_matrix
target_compile_definitions(node_addon_api INTERFACE NAPI_VERSION=5)

add_library(nodejs_bindings_client SHARED
hawktracer_client_nodejs.cpp
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.

could we name it hawktracer_client to follow other namings?

Copy link
Author

Choose a reason for hiding this comment

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

I asked this before (#67 (comment)).
Is "HawkTracer" one word, or two words?

target_link_libraries(nodejs_bindings_client PUBLIC node_addon_api hawktracer_parser hawktracer_client_utils)

# cmake-js specifies CMAKE_LIBRARY_OUTPUT_DIRECTORY.
add_custom_target(make_directory_for_nodejs_bindings_client
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
add_dependencies(nodejs_bindings_client make_directory_for_nodejs_bindings_client)
73 changes: 73 additions & 0 deletions bindings/nodejs/client_context.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include "client_context.hpp"

#include "hawktracer/client_utils/stream_factory.hpp"
#include "hawktracer/parser/make_unique.hpp"
#include <iostream>
#include <utility>

namespace HawkTracer
{
namespace Nodejs
{

std::unique_ptr<ClientContext>
ClientContext::create(const std::string& source, const std::string& map_files, EventCallback event_callback)
{
std::unique_ptr<parser::Stream> stream = ClientUtils::make_stream_from_string(source);
if (!stream) {
return nullptr;
beila marked this conversation as resolved.
Show resolved Hide resolved
}

auto klass_register = parser::make_unique<parser::KlassRegister>();
auto reader = parser::make_unique<parser::ProtocolReader>(klass_register.get(), std::move(stream), true);
auto converter = parser::make_unique<ClientUtils::Converter>();
converter->set_tracepoint_map(map_files);

std::unique_ptr<ClientContext> context{new ClientContext(std::move(reader),
std::move(klass_register),
std::move(converter),
std::move(event_callback))};
return context;
}

ClientContext::ClientContext(std::unique_ptr<parser::ProtocolReader> reader,
std::unique_ptr<parser::KlassRegister> klass_register,
std::unique_ptr<ClientUtils::Converter> converter,
EventCallback event_callback)
: _klass_register(std::move(klass_register)), _reader(std::move(reader)), _label_mapper(std::move(converter)),
_event_callback(std::move(event_callback))
{
_reader->register_events_listener(
[this](const parser::Event& event)
{
{
std::lock_guard<std::mutex> lock{_buffer_mutex};
_buffer.emplace_back(event, _label_mapper->get_label(event)); // Event is copied once.
}
_event_callback();
});

_reader->start();
}

ClientContext::~ClientContext()
{
_reader->stop();

if (!_buffer.empty()) {
std::cerr << _buffer.size() << " events were not processed." << std::endl;
}
}

// This method can be called from any thread, while the event listener is called from reader thread.
std::vector<LabeledEvent> ClientContext::take_events()
{
std::lock_guard<std::mutex> lock{_buffer_mutex};
std::vector<LabeledEvent> new_buffer;
std::swap(new_buffer, _buffer);
return new_buffer;
}

} // namespace Nodejs
} // namespace HawkTracer

56 changes: 56 additions & 0 deletions bindings/nodejs/client_context.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#ifndef CLIENT_CONTEXT_HPP
#define CLIENT_CONTEXT_HPP

#include <string>

#include "hawktracer/client_utils/converter.hpp"
#include "hawktracer/parser/event.hpp"
#include "hawktracer/parser/protocol_reader.hpp"
#include "hawktracer/parser/klass_register.hpp"

namespace HawkTracer
{
namespace Nodejs
{

class LabeledEvent: public parser::Event
{
public:
LabeledEvent(const Event& event, std::string label_string)
: Event(event), _label_string(std::move(label_string))
{}
std::string get_label_string() const { return _label_string; }
private:
std::string _label_string;
};

class ClientContext
{
public:
using EventCallback = std::function<void()>;
static std::unique_ptr<ClientContext>
create(const std::string& source, const std::string& map_files, EventCallback event_callback);

~ClientContext();

std::vector<LabeledEvent> take_events();

private:
ClientContext(std::unique_ptr<parser::ProtocolReader> reader,
std::unique_ptr<parser::KlassRegister> klass_register,
std::unique_ptr<ClientUtils::Converter> converter,
EventCallback event_callback);

const std::unique_ptr<const parser::KlassRegister> _klass_register; // needs to be destructed after _reader
const std::unique_ptr<parser::ProtocolReader> _reader;
const std::unique_ptr<ClientUtils::Converter> _label_mapper;
const EventCallback _event_callback;

std::vector<LabeledEvent> _buffer;
std::mutex _buffer_mutex;
};

} // namespace Nodejs
} // namespace HawkTracer

#endif //CLIENT_CONTEXT_HPP
153 changes: 153 additions & 0 deletions bindings/nodejs/hawktracer_client_nodejs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include "hawktracer_client_nodejs.hpp"

#include <iostream>

namespace HawkTracer
{
namespace Nodejs
{

Object Client::init_bindings(const class Env& env, Object exports)
{
HandleScope scope(env);

Function constructor = DefineClass(
env,
"HawkTracerClient",
{
InstanceMethod("start", &Client::start),
InstanceMethod("onEvents", &Client::set_on_events),
InstanceMethod("stop", &Client::stop)
});
Persistent(constructor).SuppressDestruct();
exports.Set("HawkTracerClient", constructor);
return exports;
}

Client::Client(const CallbackInfo& info)
: ObjectWrap<Client>(info)
{
_source = info[0].As<String>();
_maps = info.Length() >= 2 && !info[1].IsUndefined() ? info[1].As<String>() : std::string{};
}

Value Client::start(const CallbackInfo& info)
{
_state.start(
ClientContext::create(
_source,
_maps,
[this]()
{
_notify_new_event();
}));
if (!_state.is_started()) {
throw Error::New(info.Env(), "Failed to start");
}
return Boolean::New(info.Env(), _state.is_started());
}

void Client::stop(const CallbackInfo&)
{
_state.stop();
Reset();
}

void Client::set_on_events(const CallbackInfo& info)
{
// maxQueueSize is set to 2 so that even though the first callback is already running there's room for a new callback.
// If 2 slots are already filled up, the second callback will pick up the new events with take_events(),
// in which case we can ignore napi_queue_full.
_state.set_function(ThreadSafeFunction::New(info.Env(),
info[0].As<Napi::Function>(),
"HawkTracerClientOnEvent",
2,
1));
}

// This method is called from reader thread, while all other methods are called from js main thread.
void Client::_notify_new_event()
{
// prevents this from garbage-collected before the callback is finished
Ref();

auto status = _state.use_function(
[this](ThreadSafeFunction f)
{
return f.NonBlockingCall(this, &Client::_convert_and_callback);
});

if (status != napi_ok) {
// Callback was not added in the queue, hence no need to increase the reference count.
Unref();
}

if (status != napi_ok && status != napi_queue_full) {
std::cerr << "Request for callback failed with error code: " << status << std::endl;
}
}

Value Client::_convert_field_value(const class Env& env, const parser::Event::Value& value)
{
switch (value.field->get_type_id()) {
case parser::FieldTypeId::UINT8:
return Number::New(env, value.value.f_UINT8);
case parser::FieldTypeId::INT8:
return Number::New(env, value.value.f_INT8);
case parser::FieldTypeId::UINT16:
return Number::New(env, value.value.f_UINT16);
case parser::FieldTypeId::INT16:
return Number::New(env, value.value.f_INT16);
case parser::FieldTypeId::UINT32:
return Number::New(env, value.value.f_UINT32);
case parser::FieldTypeId::INT32:
return Number::New(env, value.value.f_INT32);
case parser::FieldTypeId::UINT64:
return Number::New(env, value.value.f_UINT64);
case parser::FieldTypeId::INT64:
return Number::New(env, value.value.f_INT64);
case parser::FieldTypeId::STRING:
return String::New(env, value.value.f_STRING);
case parser::FieldTypeId::POINTER:
return String::New(env, "(pointer)");
default:
assert(0);
}
}

Object Client::_convert_event(const class Env& env, const LabeledEvent& event)
{
auto o = Object::New(env);
for (const auto& it: event.get_values()) {
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.

}
return o;
}

void Client::_convert_and_callback(const class Env& env, Function real_callback, Client* calling_object)
{
std::vector<LabeledEvent> events = calling_object->_state.take_events();

if (!events.empty()) {
Array array = Array::New(env);
int i = 0;
std::for_each(events.cbegin(),
events.cend(),
[env, &array, &i](const LabeledEvent& e)
{
array[i++] = _convert_event(env, e);
});
real_callback.Call({array});
}

// Now calling_object can be garbage-collected, however stop() could have been already called from inside real_callback.
if (!calling_object->IsEmpty()) {
calling_object->Unref();
}
}

} // namespace Nodejs
} // namespace HawkTracer
Loading