From 47bdc716f83462b6ab938315d11de6c92be082ac Mon Sep 17 00:00:00 2001 From: Eugene Ostroukhov Date: Fri, 27 Apr 2018 17:20:37 -0700 Subject: [PATCH] inspector: add a "NodeTracing" domain support This change adds a new inspector domain for receiving Node tracing data. 1. Node.js now can extend Inspector protocol with new domains with the API defined in the src/inspector/node_protocol.pdl. 2. Plumbing code will be generated at the build time. /json/protocol HTTP endpoint returns both V8 and Node.js inspector protocol. 3. "NodeTracing" domain was introduced. It is based on the Chrome "Tracing" domain. PR-URL: https://github.com/nodejs/node/pull/20608 Reviewed-By: James M Snell Reviewed-By: Ali Ijaz Sheikh Reviewed-By: Matteo Collina --- node.gyp | 213 ++++++++++++++---- src/inspector/node_protocol.pdl | 39 ++++ src/inspector/node_protocol_config.json | 27 +++ src/inspector/node_string.cc | 92 ++++++++ src/inspector/node_string.h | 79 +++++++ src/inspector/tracing_agent.cc | 106 +++++++++ src/inspector/tracing_agent.h | 45 ++++ src/inspector_agent.cc | 51 ++++- src/inspector_agent.h | 9 +- src/inspector_io.cc | 29 +-- src/inspector_io.h | 2 +- src/tracing/agent.cc | 153 +++++++++---- src/tracing/agent.h | 42 +++- src/tracing/node_trace_buffer.cc | 16 +- src/tracing/node_trace_buffer.h | 12 +- src/tracing/node_trace_writer.cc | 6 - src/tracing/node_trace_writer.h | 6 +- .../test-inspector-multisession-js.js | 6 +- .../parallel/test-inspector-tracing-domain.js | 70 ++++++ 19 files changed, 843 insertions(+), 160 deletions(-) create mode 100644 src/inspector/node_protocol.pdl create mode 100644 src/inspector/node_protocol_config.json create mode 100644 src/inspector/node_string.cc create mode 100644 src/inspector/node_string.h create mode 100644 src/inspector/tracing_agent.cc create mode 100644 src/inspector/tracing_agent.h create mode 100644 test/parallel/test-inspector-tracing-domain.js diff --git a/node.gyp b/node.gyp index 54f22a33da6886..ecdb1479568b93 100644 --- a/node.gyp +++ b/node.gyp @@ -455,17 +455,24 @@ 'src/inspector_js_api.cc', 'src/inspector_socket.cc', 'src/inspector_socket_server.cc', + 'src/inspector/tracing_agent.cc', + 'src/inspector/node_string.cc', 'src/inspector_agent.h', 'src/inspector_io.h', 'src/inspector_socket.h', 'src/inspector_socket_server.h', + 'src/inspector/node_string.h', + 'src/inspector/tracing_agent.h', + '<@(node_inspector_generated_sources)' ], 'dependencies': [ + 'node_protocol_generated_sources#host', 'v8_inspector_compress_protocol_json#host', ], 'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)/include', # for inspector '<(SHARED_INTERMEDIATE_DIR)', + '<(SHARED_INTERMEDIATE_DIR)/src', # for inspector ], }, { 'defines': [ 'HAVE_INSPECTOR=0' ] @@ -677,54 +684,6 @@ } ] ] }, - { - 'target_name': 'v8_inspector_compress_protocol_json', - 'type': 'none', - 'toolsets': ['host'], - 'conditions': [ - [ 'v8_enable_inspector==1', { - 'copies': [ - { - 'destination': '<(SHARED_INTERMEDIATE_DIR)', - 'files': ['deps/v8/src/inspector/js_protocol.pdl'] - } - ], - 'actions': [ - { - 'action_name': 'v8_inspector_convert_protocol_to_json', - 'inputs': [ - '<(SHARED_INTERMEDIATE_DIR)/js_protocol.pdl', - ], - 'outputs': [ - '<(SHARED_INTERMEDIATE_DIR)/js_protocol.json', - ], - 'action': [ - 'python', - 'deps/v8/third_party/inspector_protocol/ConvertProtocolToJSON.py', - '<@(_inputs)', - '<@(_outputs)', - ], - }, - { - 'action_name': 'v8_inspector_compress_protocol_json', - 'process_outputs_as_sources': 1, - 'inputs': [ - '<(SHARED_INTERMEDIATE_DIR)/js_protocol.json', - ], - 'outputs': [ - '<(SHARED_INTERMEDIATE_DIR)/v8_inspector_protocol_json.h', - ], - 'action': [ - 'python', - 'tools/compress_json.py', - '<@(_inputs)', - '<@(_outputs)', - ], - }, - ], - }], - ], - }, { 'target_name': 'node_js2c', 'type': 'none', @@ -1044,5 +1003,163 @@ }, ] }], # end aix section + [ 'v8_enable_inspector==1', { + 'variables': { + 'protocol_path': 'deps/v8/third_party/inspector_protocol', + 'node_inspector_path': 'src/inspector', + 'node_inspector_generated_sources': [ + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Forward.h', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Protocol.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Protocol.h', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.h', + ], + 'node_protocol_files': [ + '<(protocol_path)/lib/Allocator_h.template', + '<(protocol_path)/lib/Array_h.template', + '<(protocol_path)/lib/Collections_h.template', + '<(protocol_path)/lib/DispatcherBase_cpp.template', + '<(protocol_path)/lib/DispatcherBase_h.template', + '<(protocol_path)/lib/ErrorSupport_cpp.template', + '<(protocol_path)/lib/ErrorSupport_h.template', + '<(protocol_path)/lib/Forward_h.template', + '<(protocol_path)/lib/FrontendChannel_h.template', + '<(protocol_path)/lib/Maybe_h.template', + '<(protocol_path)/lib/Object_cpp.template', + '<(protocol_path)/lib/Object_h.template', + '<(protocol_path)/lib/Parser_cpp.template', + '<(protocol_path)/lib/Parser_h.template', + '<(protocol_path)/lib/Protocol_cpp.template', + '<(protocol_path)/lib/ValueConversions_h.template', + '<(protocol_path)/lib/Values_cpp.template', + '<(protocol_path)/lib/Values_h.template', + '<(protocol_path)/templates/Exported_h.template', + '<(protocol_path)/templates/Imported_h.template', + '<(protocol_path)/templates/TypeBuilder_cpp.template', + '<(protocol_path)/templates/TypeBuilder_h.template', + '<(protocol_path)/CodeGenerator.py', + ] + }, + 'targets': [ + { + 'target_name': 'prepare_protocol_json', + 'type': 'none', + 'toolsets': ['host'], + 'copies': [ + { + 'files': [ + '<(node_inspector_path)/node_protocol_config.json', + '<(node_inspector_path)/node_protocol.pdl' + ], + 'destination': '<(SHARED_INTERMEDIATE_DIR)', + } + ], + 'actions': [ + { + 'action_name': 'convert_node_protocol_to_json', + 'inputs': [ + '<(SHARED_INTERMEDIATE_DIR)/node_protocol.pdl', + ], + 'outputs': [ + '<(SHARED_INTERMEDIATE_DIR)/node_protocol.json', + ], + 'action': [ + 'python', + 'deps/v8/third_party/inspector_protocol/ConvertProtocolToJSON.py', + '<@(_inputs)', + '<@(_outputs)', + ], + }, + ] + }, + { + 'target_name': 'node_protocol_generated_sources', + 'type': 'none', + 'toolsets': ['host'], + 'dependencies': ['prepare_protocol_json'], + 'actions': [ + { + 'action_name': 'node_protocol_generated_sources', + 'inputs': [ + '<(SHARED_INTERMEDIATE_DIR)/node_protocol_config.json', + '<(SHARED_INTERMEDIATE_DIR)/node_protocol.json', + '<@(node_protocol_files)', + ], + 'outputs': [ + '<@(node_inspector_generated_sources)', + ], + 'action': [ + 'python', + '<(protocol_path)/CodeGenerator.py', + '--jinja_dir', '<@(protocol_path)/..', + '--output_base', '<(SHARED_INTERMEDIATE_DIR)/src/', + '--config', '<(SHARED_INTERMEDIATE_DIR)/node_protocol_config.json', + ], + 'message': 'Generating node protocol sources from protocol json', + }, + ] + }, + { + 'target_name': 'v8_inspector_compress_protocol_json', + 'type': 'none', + 'toolsets': ['host'], + 'copies': [ + { + 'destination': '<(SHARED_INTERMEDIATE_DIR)', + 'files': ['deps/v8/src/inspector/js_protocol.pdl'] + } + ], + 'actions': [ + { + 'action_name': 'v8_inspector_convert_protocol_to_json', + 'inputs': [ + '<(SHARED_INTERMEDIATE_DIR)/js_protocol.pdl', + ], + 'outputs': [ + '<(SHARED_INTERMEDIATE_DIR)/js_protocol.json', + ], + 'action': [ + 'python', + 'deps/v8/third_party/inspector_protocol/ConvertProtocolToJSON.py', + '<@(_inputs)', + '<@(_outputs)', + ], + }, + { + 'action_name': 'concatenate_protocols', + 'inputs': [ + '<(SHARED_INTERMEDIATE_DIR)/js_protocol.json', + '<(SHARED_INTERMEDIATE_DIR)/node_protocol.json', + ], + 'outputs': [ + '<(SHARED_INTERMEDIATE_DIR)/concatenated_protocol.json', + ], + 'action': [ + 'python', + 'deps/v8/third_party/inspector_protocol/ConcatenateProtocols.py', + '<@(_inputs)', + '<@(_outputs)', + ], + }, + { + 'action_name': 'v8_inspector_compress_protocol_json', + 'process_outputs_as_sources': 1, + 'inputs': [ + '<(SHARED_INTERMEDIATE_DIR)/concatenated_protocol.json', + ], + 'outputs': [ + '<(SHARED_INTERMEDIATE_DIR)/v8_inspector_protocol_json.h', + ], + 'action': [ + 'python', + 'tools/compress_json.py', + '<@(_inputs)', + '<@(_outputs)', + ], + }, + ], + }, + ] + }] ], # end conditions block } diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl new file mode 100644 index 00000000000000..27b3d814c88d0a --- /dev/null +++ b/src/inspector/node_protocol.pdl @@ -0,0 +1,39 @@ +# Please notify @nodejs/v8-inspector and @nodejs/trace-events before modifying this file +version + major 1 + minor 0 + +experimental domain NodeTracing + type TraceConfig extends object + properties + # Controls how the trace buffer stores data. + optional enum recordMode + recordUntilFull + recordContinuously + recordAsMuchAsPossible + # Included category filters. + array of string includedCategories + + # Gets supported tracing categories. + command getCategories + returns + # A list of supported tracing categories. + array of string categories + + # Start trace events collection. + command start + parameters + TraceConfig traceConfig + + # Stop trace events collection. Remaining collected events will be sent as a sequence of + # dataCollected events followed by tracingComplete event. + command stop + + # Contains an bucket of collected trace events. + event dataCollected + parameters + array of object value + + # Signals that tracing is stopped and there is no trace buffers pending flush, all data were + # delivered via dataCollected events. + event tracingComplete diff --git a/src/inspector/node_protocol_config.json b/src/inspector/node_protocol_config.json new file mode 100644 index 00000000000000..7cea20ae937616 --- /dev/null +++ b/src/inspector/node_protocol_config.json @@ -0,0 +1,27 @@ +{ + "protocol": { + "path": "node_protocol.json", + "package": "src/node/inspector/protocol", + "output": "node/inspector/protocol", + "namespace": ["node", "inspector", "protocol"], + "options": [ + { + "domain": "NodeTracing" + } + ] + }, + "exported": { + "package": "include/inspector", + "output": "../../include/inspector", + "string_header": "v8-inspector.h", + "string_in": "StringView", + "string_out": "std::unique_ptr", + "to_string_out": "StringBufferImpl::adopt(%s)", + "export_macro": "V8_EXPORT" + }, + "lib": { + "package": "src/node/inspector/protocol", + "output": "node/inspector/protocol", + "string_header": "inspector/node_string.h" + } +} diff --git a/src/inspector/node_string.cc b/src/inspector/node_string.cc new file mode 100644 index 00000000000000..cb9e90c20e807a --- /dev/null +++ b/src/inspector/node_string.cc @@ -0,0 +1,92 @@ +#include "node_string.h" +#include "node/inspector/protocol/Protocol.h" + +#include + +namespace node { +namespace inspector { +namespace protocol { +namespace StringUtil { + +size_t kNotFound = std::string::npos; + +// NOLINTNEXTLINE(runtime/references) V8 API requirement +void builderAppendQuotedString(StringBuilder& builder, const String& string) { + builder.put('"'); + if (!string.empty()) { + icu::UnicodeString utf16 = icu::UnicodeString::fromUTF8( + icu::StringPiece(string.data(), string.length())); + escapeWideStringForJSON( + reinterpret_cast(utf16.getBuffer()), utf16.length(), + &builder); + } + builder.put('"'); +} + +std::unique_ptr parseJSON(const String& string) { + if (string.empty()) + return nullptr; + + icu::UnicodeString utf16 = + icu::UnicodeString::fromUTF8(icu::StringPiece(string.data(), + string.length())); + return parseJSONCharacters( + reinterpret_cast(utf16.getBuffer()), utf16.length()); +} + +std::unique_ptr parseJSON(v8_inspector::StringView string) { + if (string.length() == 0) + return nullptr; + if (string.is8Bit()) + return parseJSONCharacters(string.characters8(), string.length()); + return parseJSONCharacters(string.characters16(), string.length()); +} + +String StringViewToUtf8(v8_inspector::StringView view) { + if (view.length() == 0) + return ""; + if (view.is8Bit()) { + return std::string(reinterpret_cast(view.characters8()), + view.length()); + } + const uint16_t* source = view.characters16(); + const UChar* unicodeSource = reinterpret_cast(source); + static_assert(sizeof(*source) == sizeof(*unicodeSource), + "sizeof(*source) == sizeof(*unicodeSource)"); + + size_t result_length = view.length() * sizeof(*source); + std::string result(result_length, '\0'); + icu::UnicodeString utf16(unicodeSource, view.length()); + // ICU components for std::string compatibility are not enabled in build... + bool done = false; + while (!done) { + icu::CheckedArrayByteSink sink(&result[0], result_length); + utf16.toUTF8(sink); + result_length = sink.NumberOfBytesAppended(); + result.resize(result_length); + done = !sink.Overflowed(); + } + return result; +} + +String fromDouble(double d) { + std::ostringstream stream; + stream.imbue(std::locale("C")); // Ignore locale + stream << d; + return stream.str(); +} + +double toDouble(const char* buffer, size_t length, bool* ok) { + std::istringstream stream(std::string(buffer, length)); + stream.imbue(std::locale("C")); // Ignore locale + double d; + stream >> d; + *ok = !stream.fail(); + return d; +} + +} // namespace StringUtil +} // namespace protocol +} // namespace inspector +} // namespace node + diff --git a/src/inspector/node_string.h b/src/inspector/node_string.h new file mode 100644 index 00000000000000..468aec96b56e79 --- /dev/null +++ b/src/inspector/node_string.h @@ -0,0 +1,79 @@ +// Bridges V8 Inspector generated code with the std::string used by the Node +// Compare to V8 counterpart - deps/v8/src/inspector/string-util.h +#ifndef SRC_INSPECTOR_NODE_STRING_H_ +#define SRC_INSPECTOR_NODE_STRING_H_ + +#include "util.h" +#include "v8-inspector.h" + +#include +#include +#include + +namespace node { +namespace inspector { +namespace protocol { + +class Value; + +using String = std::string; +using StringBuilder = std::ostringstream; + +namespace StringUtil { +// NOLINTNEXTLINE(runtime/references) This is V8 API... +inline void builderAppend(StringBuilder& builder, char c) { + builder.put(c); +} + +// NOLINTNEXTLINE(runtime/references) +inline void builderAppend(StringBuilder& builder, const char* value, + size_t length) { + builder.write(value, length); +} + +// NOLINTNEXTLINE(runtime/references) +inline void builderAppend(StringBuilder& builder, const char* value) { + builderAppend(builder, value, std::strlen(value)); +} + +// NOLINTNEXTLINE(runtime/references) +inline void builderAppend(StringBuilder& builder, const String& string) { + builder << string; +} + +// NOLINTNEXTLINE(runtime/references) +inline void builderReserve(StringBuilder& builder, size_t) { + // ostringstream does not have a counterpart +} +inline String substring(const String& string, size_t start, size_t count) { + return string.substr(start, count); +} +inline String fromInteger(int n) { + return std::to_string(n); +} +inline String builderToString(const StringBuilder& builder) { + return builder.str(); +} +inline size_t find(const String& string, const char* substring) { + return string.find(substring); +} +String fromDouble(double d); +double toDouble(const char* buffer, size_t length, bool* ok); + +String StringViewToUtf8(v8_inspector::StringView view); + +// NOLINTNEXTLINE(runtime/references) +void builderAppendQuotedString(StringBuilder& builder, const String&); +std::unique_ptr parseJSON(const String&); +std::unique_ptr parseJSON(v8_inspector::StringView view); + +extern size_t kNotFound; +} // namespace StringUtil +} // namespace protocol +} // namespace inspector +} // namespace node + +#define DCHECK CHECK +#define DCHECK_LT CHECK_LT + +#endif // SRC_INSPECTOR_NODE_STRING_H_ diff --git a/src/inspector/tracing_agent.cc b/src/inspector/tracing_agent.cc new file mode 100644 index 00000000000000..2d98fa1ee527cc --- /dev/null +++ b/src/inspector/tracing_agent.cc @@ -0,0 +1,106 @@ +#include "tracing_agent.h" + +#include "env-inl.h" +#include "v8.h" + +#include +#include + +namespace node { +namespace inspector { +namespace protocol { + +namespace { +using v8::platform::tracing::TraceWriter; + +class InspectorTraceWriter : public node::tracing::AsyncTraceWriter { + public: + explicit InspectorTraceWriter(NodeTracing::Frontend* frontend) + : frontend_(frontend) {} + + void AppendTraceEvent( + v8::platform::tracing::TraceObject* trace_event) override { + if (!json_writer_) + json_writer_.reset(TraceWriter::CreateJSONTraceWriter(stream_, "value")); + json_writer_->AppendTraceEvent(trace_event); + } + + void Flush(bool) override { + if (!json_writer_) + return; + json_writer_.reset(); + std::ostringstream result( + "{\"method\":\"NodeTracing.dataCollected\",\"data\":", + std::ostringstream::ate); + result << stream_.str(); + result << "}"; + frontend_->sendRawNotification(result.str()); + stream_.str(""); + } + + private: + std::unique_ptr json_writer_; + std::ostringstream stream_; + NodeTracing::Frontend* frontend_; +}; +} // namespace + +TracingAgent::TracingAgent(Environment* env) + : env_(env), + trace_writer_( + tracing::Agent::EmptyClientHandle()) { +} + +TracingAgent::~TracingAgent() { + trace_writer_.reset(); +} + +void TracingAgent::Wire(UberDispatcher* dispatcher) { + frontend_ = std::make_unique(dispatcher->channel()); + NodeTracing::Dispatcher::wire(dispatcher, this); +} + +DispatchResponse TracingAgent::start( + std::unique_ptr traceConfig) { + if (trace_writer_ != nullptr) { + return DispatchResponse::Error( + "Call NodeTracing::end to stop tracing before updating the config"); + } + + std::set categories_set; + protocol::Array* categories = + traceConfig->getIncludedCategories(); + for (size_t i = 0; i < categories->length(); i++) + categories_set.insert(categories->get(i)); + + if (categories_set.empty()) + return DispatchResponse::Error("At least one category should be enabled"); + + trace_writer_ = env_->tracing_agent()->AddClient( + categories_set, std::make_unique(frontend_.get())); + return DispatchResponse::OK(); +} + +DispatchResponse TracingAgent::stop() { + trace_writer_.reset(); + frontend_->tracingComplete(); + return DispatchResponse::OK(); +} + +DispatchResponse TracingAgent::getCategories( + std::unique_ptr>* categories) { + *categories = Array::create(); + categories->get()->addItem("node"); + categories->get()->addItem("node.async"); + categories->get()->addItem("node.bootstrap"); + categories->get()->addItem("node.fs.sync"); + categories->get()->addItem("node.perf"); + categories->get()->addItem("node.perf.usertiming"); + categories->get()->addItem("node.perf.timerify"); + categories->get()->addItem("v8"); + return DispatchResponse::OK(); +} + +} // namespace protocol +} // namespace inspector +} // namespace node diff --git a/src/inspector/tracing_agent.h b/src/inspector/tracing_agent.h new file mode 100644 index 00000000000000..478107c5acdd64 --- /dev/null +++ b/src/inspector/tracing_agent.h @@ -0,0 +1,45 @@ +#ifndef SRC_INSPECTOR_TRACING_AGENT_H_ +#define SRC_INSPECTOR_TRACING_AGENT_H_ + +#include "node/inspector/protocol/NodeTracing.h" +#include "v8.h" + + +namespace node { +class Environment; + +namespace tracing { +class Agent; +} // namespace tracing + +namespace inspector { +namespace protocol { + +class TracingAgent : public NodeTracing::Backend { + public: + explicit TracingAgent(Environment*); + ~TracingAgent() override; + + void Wire(UberDispatcher* dispatcher); + + DispatchResponse start( + std::unique_ptr traceConfig) override; + DispatchResponse stop() override; + DispatchResponse getCategories( + std::unique_ptr>* categories) override; + + private: + void DisconnectTraceClient(); + + Environment* env_; + std::unique_ptr, + void (*)(std::pair*)> trace_writer_; + std::unique_ptr frontend_; +}; + + +} // namespace protocol +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_TRACING_AGENT_H_ diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index c8e62f6f0a22c0..50aa23b63cd1d0 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -1,6 +1,9 @@ #include "inspector_agent.h" #include "inspector_io.h" +#include "inspector/node_string.h" +#include "inspector/tracing_agent.h" +#include "node/inspector/protocol/Protocol.h" #include "node_internals.h" #include "v8-inspector.h" #include "v8-platform.h" @@ -187,18 +190,35 @@ static int StartDebugSignalHandler() { const int NANOS_PER_MSEC = 1000000; const int CONTEXT_GROUP_ID = 1; -class ChannelImpl final : public v8_inspector::V8Inspector::Channel { +class ChannelImpl final : public v8_inspector::V8Inspector::Channel, + public protocol::FrontendChannel { public: - explicit ChannelImpl(const std::unique_ptr& inspector, + explicit ChannelImpl(Environment* env, + const std::unique_ptr& inspector, std::unique_ptr delegate) : delegate_(std::move(delegate)) { session_ = inspector->connect(1, this, StringView()); + node_dispatcher_ = std::make_unique(this); + tracing_agent_ = std::make_unique(env); + tracing_agent_->Wire(node_dispatcher_.get()); } - virtual ~ChannelImpl() {} + virtual ~ChannelImpl() { + tracing_agent_->disable(); + tracing_agent_.reset(); // Dispose before the dispatchers + } void dispatchProtocolMessage(const StringView& message) { - session_->dispatchProtocolMessage(message); + std::unique_ptr parsed; + std::string method; + node_dispatcher_->getCommandName( + protocol::StringUtil::StringViewToUtf8(message), &method, &parsed); + if (v8_inspector::V8InspectorSession::canDispatchMethod( + Utf8ToStringView(method)->string())) { + session_->dispatchProtocolMessage(message); + } else { + node_dispatcher_->dispatch(std::move(parsed)); + } } void schedulePauseOnNextStatement(const std::string& reason) { @@ -224,8 +244,25 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel { delegate_->SendMessageToFrontend(message); } + void sendMessageToFrontend(const std::string& message) { + sendMessageToFrontend(Utf8ToStringView(message)->string()); + } + + using Serializable = protocol::Serializable; + + void sendProtocolResponse(int callId, + std::unique_ptr message) override { + sendMessageToFrontend(message->serialize()); + } + void sendProtocolNotification( + std::unique_ptr message) override { + sendMessageToFrontend(message->serialize()); + } + + std::unique_ptr tracing_agent_; std::unique_ptr delegate_; std::unique_ptr session_; + std::unique_ptr node_dispatcher_; }; class InspectorTimer { @@ -369,7 +406,8 @@ class NodeInspectorClient : public V8InspectorClient { int session_id = next_session_id_++; // TODO(addaleax): Revert back to using make_unique once we get issues // with CI resolved (i.e. revert the patch that added this comment). - channels_[session_id].reset(new ChannelImpl(client_, std::move(delegate))); + channels_[session_id].reset( + new ChannelImpl(env_, client_, std::move(delegate))); return session_id; } @@ -638,7 +676,8 @@ void Agent::DisableAsyncHook() { } } -void Agent::ToggleAsyncHook(Isolate* isolate, const Persistent& fn) { +void Agent::ToggleAsyncHook(Isolate* isolate, + const node::Persistent& fn) { HandleScope handle_scope(isolate); CHECK(!fn.IsEmpty()); auto context = parent_env_->context(); diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 64e4202ee88e13..21cf4c9f1253da 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -10,7 +10,7 @@ #endif #include "node_debug_options.h" -#include "node_platform.h" +#include "node_persistent.h" #include "v8.h" namespace v8_inspector { @@ -20,6 +20,7 @@ class StringView; namespace node { // Forward declaration to break recursive dependency chain with src/env.h. class Environment; +class NodePlatform; struct ContextInfo; namespace inspector { @@ -102,7 +103,7 @@ class Agent { private: void ToggleAsyncHook(v8::Isolate* isolate, - const Persistent& fn); + const node::Persistent& fn); node::Environment* parent_env_; std::shared_ptr client_; @@ -113,8 +114,8 @@ class Agent { bool pending_enable_async_hook_; bool pending_disable_async_hook_; - Persistent enable_async_hook_function_; - Persistent disable_async_hook_function_; + node::Persistent enable_async_hook_function_; + node::Persistent disable_async_hook_function_; }; } // namespace inspector diff --git a/src/inspector_io.cc b/src/inspector_io.cc index e6ceaf5b736d37..ce18e989737b5a 100644 --- a/src/inspector_io.cc +++ b/src/inspector_io.cc @@ -1,6 +1,7 @@ #include "inspector_io.h" #include "inspector_socket_server.h" +#include "inspector/node_string.h" #include "env-inl.h" #include "node.h" #include "node_crypto.h" @@ -62,31 +63,6 @@ std::string GenerateID() { return uuid; } -std::string StringViewToUtf8(const StringView& view) { - if (view.is8Bit()) { - return std::string(reinterpret_cast(view.characters8()), - view.length()); - } - const uint16_t* source = view.characters16(); - const UChar* unicodeSource = reinterpret_cast(source); - static_assert(sizeof(*source) == sizeof(*unicodeSource), - "sizeof(*source) == sizeof(*unicodeSource)"); - - size_t result_length = view.length() * sizeof(*source); - std::string result(result_length, '\0'); - icu::UnicodeString utf16(unicodeSource, view.length()); - // ICU components for std::string compatibility are not enabled in build... - bool done = false; - while (!done) { - icu::CheckedArrayByteSink sink(&result[0], result_length); - utf16.toUTF8(sink); - result_length = sink.NumberOfBytesAppended(); - result.resize(result_length); - done = !sink.Overflowed(); - } - return result; -} - void HandleSyncCloseCb(uv_handle_t* handle) { *static_cast(handle->data) = true; } @@ -272,7 +248,8 @@ void InspectorIo::IoThreadAsyncCb(uv_async_t* async) { break; case TransportAction::kSendMessage: transport->Send(session_id, - StringViewToUtf8(std::get<2>(outgoing)->string())); + protocol::StringUtil::StringViewToUtf8( + std::get<2>(outgoing)->string())); break; case TransportAction::kAcceptSession: transport->AcceptSession(session_id); diff --git a/src/inspector_io.h b/src/inspector_io.h index 276c78056cb0a1..05de2d17cd3bad 100644 --- a/src/inspector_io.h +++ b/src/inspector_io.h @@ -7,7 +7,7 @@ #include "uv.h" #include -#include +#include #include #include diff --git a/src/tracing/agent.cc b/src/tracing/agent.cc index cd4c3c0df9398e..a3ddfb61a95328 100644 --- a/src/tracing/agent.cc +++ b/src/tracing/agent.cc @@ -8,11 +8,43 @@ namespace node { namespace tracing { +namespace { + +class ScopedSuspendTracing { + public: + ScopedSuspendTracing(TracingController* controller, Agent* agent) + : controller_(controller), agent_(agent) { + controller->StopTracing(); + } + + ~ScopedSuspendTracing() { + TraceConfig* config = agent_->CreateTraceConfig(); + if (config != nullptr) { + controller_->StartTracing(config); + } + } + + private: + TracingController* controller_; + Agent* agent_; +}; + +std::set flatten( + const std::unordered_map>& map) { + std::set result; + for (const auto& id_value : map) + result.insert(id_value.second.begin(), id_value.second.end()); + return result; +} + +} // namespace + using v8::platform::tracing::TraceConfig; +using v8::platform::tracing::TraceWriter; using std::string; Agent::Agent(const std::string& log_file_pattern) - : log_file_pattern_(log_file_pattern) { + : log_file_pattern_(log_file_pattern), file_writer_(EmptyClientHandle()) { tracing_controller_ = new TracingController(); tracing_controller_->Initialize(nullptr); } @@ -23,11 +55,9 @@ void Agent::Start() { CHECK_EQ(uv_loop_init(&tracing_loop_), 0); - NodeTraceWriter* trace_writer = - new NodeTraceWriter(log_file_pattern_, &tracing_loop_); - TraceBuffer* trace_buffer = new NodeTraceBuffer( - NodeTraceBuffer::kBufferChunks, trace_writer, &tracing_loop_); - tracing_controller_->Initialize(trace_buffer); + NodeTraceBuffer* trace_buffer_ = new NodeTraceBuffer( + NodeTraceBuffer::kBufferChunks, this, &tracing_loop_); + tracing_controller_->Initialize(trace_buffer_); // This thread should be created *after* async handles are created // (within NodeTraceWriter and NodeTraceBuffer constructors). @@ -36,7 +66,23 @@ void Agent::Start() { started_ = true; } +Agent::ClientHandle Agent::AddClient(const std::set& categories, + std::unique_ptr writer) { + Start(); + ScopedSuspendTracing suspend(tracing_controller_, this); + int id = next_writer_id_++; + writers_[id] = std::move(writer); + categories_[id] = categories; + + auto client_id = new std::pair(this, id); + return ClientHandle(client_id, &DisconnectClient); +} + void Agent::Stop() { + file_writer_.reset(); +} + +void Agent::StopTracing() { if (!started_) return; // Perform final Flush on TraceBuffer. We don't want the tracing controller @@ -49,6 +95,12 @@ void Agent::Stop() { uv_thread_join(&thread_); } +void Agent::Disconnect(int client) { + ScopedSuspendTracing suspend(tracing_controller_, this); + writers_.erase(client); + categories_.erase(client); +} + // static void Agent::ThreadCb(void* arg) { Agent* agent = static_cast(arg); @@ -56,72 +108,81 @@ void Agent::ThreadCb(void* arg) { } void Agent::Enable(const std::string& categories) { - if (!categories.empty()) { - std::stringstream category_list(categories); - while (category_list.good()) { - std::string category; - getline(category_list, category, ','); - categories_.insert(category.c_str()); - } - RestartTracing(); + if (categories.empty()) + return; + std::set categories_set; + std::stringstream category_list(categories); + while (category_list.good()) { + std::string category; + getline(category_list, category, ','); + categories_set.insert(category); } + Enable(categories_set); } void Agent::Enable(const std::set& categories) { - if (!categories.empty()) { - categories_.insert(categories.begin(), categories.end()); - RestartTracing(); - } -} + std::string cats; + for (const std::string cat : categories) + cats += cat + ", "; + if (categories.empty()) + return; -void Agent::Disable(const std::set& categories) { - if (!categories.empty()) { - for (auto category : categories) { - auto pos = categories_.lower_bound(category); - if (pos != categories_.end()) - categories_.erase(pos); - } - RestartTracing(); + file_writer_categories_.insert(categories.begin(), categories.end()); + std::set full_list(file_writer_categories_.begin(), + file_writer_categories_.end()); + if (!file_writer_) { + // Ensure background thread is running + Start(); + std::unique_ptr writer( + new NodeTraceWriter(log_file_pattern_, &tracing_loop_)); + file_writer_ = AddClient(full_list, std::move(writer)); + } else { + ScopedSuspendTracing suspend(tracing_controller_, this); + categories_[file_writer_->second] = full_list; } } -void Agent::RestartTracing() { - static bool warned; - if (!warned) { - warned = true; - fprintf(stderr, "Warning: Trace event is an experimental feature " - "and could change at any time.\n"); +void Agent::Disable(const std::set& categories) { + for (auto category : categories) { + auto it = file_writer_categories_.find(category); + if (it != file_writer_categories_.end()) + file_writer_categories_.erase(it); } - Start(); // Start the agent if it hasn't already been started - tracing_controller_->StopTracing(); - auto config = CreateTraceConfig(); - if (config != nullptr) - tracing_controller_->StartTracing(config); + if (!file_writer_) + return; + ScopedSuspendTracing suspend(tracing_controller_, this); + categories_[file_writer_->second] = { file_writer_categories_.begin(), + file_writer_categories_.end() }; } TraceConfig* Agent::CreateTraceConfig() { if (categories_.empty()) return nullptr; TraceConfig* trace_config = new TraceConfig(); - for (auto category = categories_.begin(); - category != categories_.end(); - category = categories_.upper_bound(*category)) { - trace_config->AddIncludedCategory(category->c_str()); + for (const auto& category : flatten(categories_)) { + trace_config->AddIncludedCategory(category.c_str()); } return trace_config; } std::string Agent::GetEnabledCategories() { std::string categories; - for (auto category = categories_.begin(); - category != categories_.end(); - category = categories_.upper_bound(*category)) { + for (const auto& category : flatten(categories_)) { if (!categories.empty()) categories += ','; - categories += *category; + categories += category; } return categories; } +void Agent::AppendTraceEvent(TraceObject* trace_event) { + for (const auto& id_writer : writers_) + id_writer.second->AppendTraceEvent(trace_event); +} + +void Agent::Flush(bool blocking) { + for (const auto& id_writer : writers_) + id_writer.second->Flush(blocking); +} } // namespace tracing } // namespace node diff --git a/src/tracing/agent.h b/src/tracing/agent.h index ca5fd35417fd0f..fd7984275969ba 100644 --- a/src/tracing/agent.h +++ b/src/tracing/agent.h @@ -7,11 +7,20 @@ #include #include +#include namespace node { namespace tracing { using v8::platform::tracing::TraceConfig; +using v8::platform::tracing::TraceObject; + +class AsyncTraceWriter { + public: + virtual ~AsyncTraceWriter() {} + virtual void AppendTraceEvent(TraceObject* trace_event) = 0; + virtual void Flush(bool blocking) = 0; +}; class TracingController : public v8::platform::tracing::TracingController { public: @@ -22,33 +31,58 @@ class TracingController : public v8::platform::tracing::TracingController { } }; + class Agent { public: + // Resetting the pointer disconnects client + using ClientHandle = std::unique_ptr, + void (*)(std::pair*)>; + + static ClientHandle EmptyClientHandle() { + return ClientHandle(nullptr, DisconnectClient); + } explicit Agent(const std::string& log_file_pattern); void Stop(); TracingController* GetTracingController() { return tracing_controller_; } + // Destroying the handle disconnects the client + ClientHandle AddClient(const std::set& categories, + std::unique_ptr writer); + + // These 3 methods operate on a "default" client, e.g. the file writer void Enable(const std::string& categories); void Enable(const std::set& categories); void Disable(const std::set& categories); std::string GetEnabledCategories(); + void AppendTraceEvent(TraceObject* trace_event); + void Flush(bool blocking); + + TraceConfig* CreateTraceConfig(); + private: static void ThreadCb(void* arg); + static void DisconnectClient(std::pair* id_agent) { + id_agent->first->Disconnect(id_agent->second); + delete id_agent; + } void Start(); - void RestartTracing(); - - TraceConfig* CreateTraceConfig(); + void StopTracing(); + void Disconnect(int client); const std::string& log_file_pattern_; uv_thread_t thread_; uv_loop_t tracing_loop_; bool started_ = false; - std::multiset categories_; + std::unordered_map> categories_; TracingController* tracing_controller_ = nullptr; + ClientHandle file_writer_; + int next_writer_id_ = 1; + std::unordered_map> writers_; + std::multiset file_writer_categories_; }; } // namespace tracing diff --git a/src/tracing/node_trace_buffer.cc b/src/tracing/node_trace_buffer.cc index 4c9f7c658fd945..1765b88df1ea94 100644 --- a/src/tracing/node_trace_buffer.cc +++ b/src/tracing/node_trace_buffer.cc @@ -4,9 +4,9 @@ namespace node { namespace tracing { InternalTraceBuffer::InternalTraceBuffer(size_t max_chunks, uint32_t id, - NodeTraceWriter* trace_writer) + Agent* agent) : flushing_(false), max_chunks_(max_chunks), - trace_writer_(trace_writer), id_(id) { + agent_(agent), id_(id) { chunks_.resize(max_chunks); } @@ -59,14 +59,14 @@ void InternalTraceBuffer::Flush(bool blocking) { for (size_t i = 0; i < total_chunks_; ++i) { auto& chunk = chunks_[i]; for (size_t j = 0; j < chunk->size(); ++j) { - trace_writer_->AppendTraceEvent(chunk->GetEventAt(j)); + agent_->AppendTraceEvent(chunk->GetEventAt(j)); } } total_chunks_ = 0; flushing_ = false; } } - trace_writer_->Flush(blocking); + agent_->Flush(blocking); } uint64_t InternalTraceBuffer::MakeHandle( @@ -87,10 +87,10 @@ void InternalTraceBuffer::ExtractHandle( } NodeTraceBuffer::NodeTraceBuffer(size_t max_chunks, - NodeTraceWriter* trace_writer, uv_loop_t* tracing_loop) - : tracing_loop_(tracing_loop), trace_writer_(trace_writer), - buffer1_(max_chunks, 0, trace_writer), - buffer2_(max_chunks, 1, trace_writer) { + Agent* agent, uv_loop_t* tracing_loop) + : tracing_loop_(tracing_loop), agent_(agent), + buffer1_(max_chunks, 0, agent), + buffer2_(max_chunks, 1, agent) { current_buf_.store(&buffer1_); flush_signal_.data = this; diff --git a/src/tracing/node_trace_buffer.h b/src/tracing/node_trace_buffer.h index 4d725ce31f2d4d..f08e6a69da2104 100644 --- a/src/tracing/node_trace_buffer.h +++ b/src/tracing/node_trace_buffer.h @@ -1,8 +1,8 @@ #ifndef SRC_TRACING_NODE_TRACE_BUFFER_H_ #define SRC_TRACING_NODE_TRACE_BUFFER_H_ +#include "tracing/agent.h" #include "node_mutex.h" -#include "tracing/node_trace_writer.h" #include "libplatform/v8-tracing.h" #include @@ -19,8 +19,7 @@ class NodeTraceBuffer; class InternalTraceBuffer { public: - InternalTraceBuffer(size_t max_chunks, uint32_t id, - NodeTraceWriter* trace_writer); + InternalTraceBuffer(size_t max_chunks, uint32_t id, Agent* agent); TraceObject* AddTraceEvent(uint64_t* handle); TraceObject* GetEventByHandle(uint64_t handle); @@ -42,7 +41,7 @@ class InternalTraceBuffer { Mutex mutex_; bool flushing_; size_t max_chunks_; - NodeTraceWriter* trace_writer_; + Agent* agent_; std::vector> chunks_; size_t total_chunks_ = 0; uint32_t current_chunk_seq_ = 1; @@ -51,8 +50,7 @@ class InternalTraceBuffer { class NodeTraceBuffer : public TraceBuffer { public: - NodeTraceBuffer(size_t max_chunks, NodeTraceWriter* trace_writer, - uv_loop_t* tracing_loop); + NodeTraceBuffer(size_t max_chunks, Agent* agent, uv_loop_t* tracing_loop); ~NodeTraceBuffer(); TraceObject* AddTraceEvent(uint64_t* handle) override; @@ -74,7 +72,7 @@ class NodeTraceBuffer : public TraceBuffer { Mutex exit_mutex_; // Used to wait until async handles have been closed. ConditionVariable exit_cond_; - std::unique_ptr trace_writer_; + Agent* agent_; std::atomic current_buf_; InternalTraceBuffer buffer1_; InternalTraceBuffer buffer2_; diff --git a/src/tracing/node_trace_writer.cc b/src/tracing/node_trace_writer.cc index 15c59fc98a8747..88ff3e6e7c159d 100644 --- a/src/tracing/node_trace_writer.cc +++ b/src/tracing/node_trace_writer.cc @@ -126,12 +126,6 @@ void NodeTraceWriter::FlushSignalCb(uv_async_t* signal) { trace_writer->FlushPrivate(); } -// TODO(matthewloring): Remove (is it necessary to change the API? -// Since because of WriteSuffix it no longer matters whether it's true or false) -void NodeTraceWriter::Flush() { - Flush(true); -} - void NodeTraceWriter::Flush(bool blocking) { Mutex::ScopedLock scoped_lock(request_mutex_); if (!json_trace_writer_) { diff --git a/src/tracing/node_trace_writer.h b/src/tracing/node_trace_writer.h index 9211790777bddb..b2d5e7912fa01d 100644 --- a/src/tracing/node_trace_writer.h +++ b/src/tracing/node_trace_writer.h @@ -6,6 +6,7 @@ #include "node_mutex.h" #include "libplatform/v8-tracing.h" +#include "tracing/agent.h" #include "uv.h" namespace node { @@ -14,15 +15,14 @@ namespace tracing { using v8::platform::tracing::TraceObject; using v8::platform::tracing::TraceWriter; -class NodeTraceWriter : public TraceWriter { +class NodeTraceWriter : public AsyncTraceWriter { public: explicit NodeTraceWriter(const std::string& log_file_pattern, uv_loop_t* tracing_loop); ~NodeTraceWriter(); void AppendTraceEvent(TraceObject* trace_event) override; - void Flush() override; - void Flush(bool blocking); + void Flush(bool blocking) override; static const int kTracesPerFile = 1 << 19; diff --git a/test/parallel/test-inspector-multisession-js.js b/test/parallel/test-inspector-multisession-js.js index 58533f4cd6241e..c899eeae713908 100644 --- a/test/parallel/test-inspector-multisession-js.js +++ b/test/parallel/test-inspector-multisession-js.js @@ -56,4 +56,8 @@ async function test() { common.crashOnUnhandledRejection(); -test(); +const interval = setInterval(() => {}, 1000); +test().then(() => { + clearInterval(interval); + console.log('Done!'); +}); diff --git a/test/parallel/test-inspector-tracing-domain.js b/test/parallel/test-inspector-tracing-domain.js new file mode 100644 index 00000000000000..61a853a265780c --- /dev/null +++ b/test/parallel/test-inspector-tracing-domain.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { Session } = require('inspector'); + +const session = new Session(); + +function compareIgnoringOrder(array1, array2) { + const set = new Set(array1); + const test = set.size === array2.length && array2.every((el) => set.has(el)); + assert.ok(test, `[${array1}] differs from [${array2}]`); +} + +function post(message, data) { + return new Promise((resolve, reject) => { + session.post(message, data, (err, result) => { + if (err) + reject(new Error(JSON.stringify(err))); + else + resolve(result); + }); + }); +} + +function generateTrace() { + return new Promise((resolve) => setTimeout(() => { + for (let i = 0; i << 1000000; i++) { + 'test' + i; + } + resolve(); + }, 1)); +} + +async function test() { + // This interval ensures Node does not terminate till the test is finished. + // Inspector session does not keep the node process running (e.g. it does not + // have async handles on the main event loop). It is debatable whether this + // should be considered a bug, and there are no plans to fix it atm. + const interval = setInterval(() => {}, 5000); + session.connect(); + let traceNotification = null; + let tracingComplete = false; + session.on('NodeTracing.dataCollected', (n) => traceNotification = n); + session.on('NodeTracing.tracingComplete', () => tracingComplete = true); + const { categories } = await post('NodeTracing.getCategories'); + compareIgnoringOrder(['node', 'node.async', 'node.bootstrap', 'node.fs.sync', + 'node.perf', 'node.perf.usertiming', + 'node.perf.timerify', 'v8'], + categories); + + const traceConfig = { includedCategories: ['node'] }; + await post('NodeTracing.start', { traceConfig }); + + for (let i = 0; i < 5; i++) + await generateTrace(); + JSON.stringify(await post('NodeTracing.stop', { traceConfig })); + session.disconnect(); + assert(traceNotification.data.value.length > 0); + assert(tracingComplete); + clearInterval(interval); + console.log('Success'); +} + +common.crashOnUnhandledRejection(); + +test();