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 <jasnell@gmail.com> Reviewed-By: Ali Ijaz Sheikh <ofrobots@google.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
5248401174
commit
47bdc716f8
213
node.gyp
213
node.gyp
@ -455,17 +455,24 @@
|
|||||||
'src/inspector_js_api.cc',
|
'src/inspector_js_api.cc',
|
||||||
'src/inspector_socket.cc',
|
'src/inspector_socket.cc',
|
||||||
'src/inspector_socket_server.cc',
|
'src/inspector_socket_server.cc',
|
||||||
|
'src/inspector/tracing_agent.cc',
|
||||||
|
'src/inspector/node_string.cc',
|
||||||
'src/inspector_agent.h',
|
'src/inspector_agent.h',
|
||||||
'src/inspector_io.h',
|
'src/inspector_io.h',
|
||||||
'src/inspector_socket.h',
|
'src/inspector_socket.h',
|
||||||
'src/inspector_socket_server.h',
|
'src/inspector_socket_server.h',
|
||||||
|
'src/inspector/node_string.h',
|
||||||
|
'src/inspector/tracing_agent.h',
|
||||||
|
'<@(node_inspector_generated_sources)'
|
||||||
],
|
],
|
||||||
'dependencies': [
|
'dependencies': [
|
||||||
|
'node_protocol_generated_sources#host',
|
||||||
'v8_inspector_compress_protocol_json#host',
|
'v8_inspector_compress_protocol_json#host',
|
||||||
],
|
],
|
||||||
'include_dirs': [
|
'include_dirs': [
|
||||||
'<(SHARED_INTERMEDIATE_DIR)/include', # for inspector
|
'<(SHARED_INTERMEDIATE_DIR)/include', # for inspector
|
||||||
'<(SHARED_INTERMEDIATE_DIR)',
|
'<(SHARED_INTERMEDIATE_DIR)',
|
||||||
|
'<(SHARED_INTERMEDIATE_DIR)/src', # for inspector
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
'defines': [ 'HAVE_INSPECTOR=0' ]
|
'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',
|
'target_name': 'node_js2c',
|
||||||
'type': 'none',
|
'type': 'none',
|
||||||
@ -1044,5 +1003,163 @@
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}], # end aix section
|
}], # 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
|
], # end conditions block
|
||||||
}
|
}
|
||||||
|
39
src/inspector/node_protocol.pdl
Normal file
39
src/inspector/node_protocol.pdl
Normal file
@ -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
|
27
src/inspector/node_protocol_config.json
Normal file
27
src/inspector/node_protocol_config.json
Normal file
@ -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<StringBuffer>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
92
src/inspector/node_string.cc
Normal file
92
src/inspector/node_string.cc
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
#include "node_string.h"
|
||||||
|
#include "node/inspector/protocol/Protocol.h"
|
||||||
|
|
||||||
|
#include <unicode/unistr.h>
|
||||||
|
|
||||||
|
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<const uint16_t*>(utf16.getBuffer()), utf16.length(),
|
||||||
|
&builder);
|
||||||
|
}
|
||||||
|
builder.put('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Value> 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<const uint16_t*>(utf16.getBuffer()), utf16.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Value> 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<const char*>(view.characters8()),
|
||||||
|
view.length());
|
||||||
|
}
|
||||||
|
const uint16_t* source = view.characters16();
|
||||||
|
const UChar* unicodeSource = reinterpret_cast<const UChar*>(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
|
||||||
|
|
79
src/inspector/node_string.h
Normal file
79
src/inspector/node_string.h
Normal file
@ -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 <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<Value> parseJSON(const String&);
|
||||||
|
std::unique_ptr<Value> 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_
|
106
src/inspector/tracing_agent.cc
Normal file
106
src/inspector/tracing_agent.cc
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#include "tracing_agent.h"
|
||||||
|
|
||||||
|
#include "env-inl.h"
|
||||||
|
#include "v8.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
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<TraceWriter> 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<NodeTracing::Frontend>(dispatcher->channel());
|
||||||
|
NodeTracing::Dispatcher::wire(dispatcher, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchResponse TracingAgent::start(
|
||||||
|
std::unique_ptr<protocol::NodeTracing::TraceConfig> traceConfig) {
|
||||||
|
if (trace_writer_ != nullptr) {
|
||||||
|
return DispatchResponse::Error(
|
||||||
|
"Call NodeTracing::end to stop tracing before updating the config");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> categories_set;
|
||||||
|
protocol::Array<std::string>* 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<InspectorTraceWriter>(frontend_.get()));
|
||||||
|
return DispatchResponse::OK();
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchResponse TracingAgent::stop() {
|
||||||
|
trace_writer_.reset();
|
||||||
|
frontend_->tracingComplete();
|
||||||
|
return DispatchResponse::OK();
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchResponse TracingAgent::getCategories(
|
||||||
|
std::unique_ptr<protocol::Array<String>>* categories) {
|
||||||
|
*categories = Array<String>::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
|
45
src/inspector/tracing_agent.h
Normal file
45
src/inspector/tracing_agent.h
Normal file
@ -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<protocol::NodeTracing::TraceConfig> traceConfig) override;
|
||||||
|
DispatchResponse stop() override;
|
||||||
|
DispatchResponse getCategories(
|
||||||
|
std::unique_ptr<protocol::Array<String>>* categories) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void DisconnectTraceClient();
|
||||||
|
|
||||||
|
Environment* env_;
|
||||||
|
std::unique_ptr<std::pair<tracing::Agent*, int>,
|
||||||
|
void (*)(std::pair<tracing::Agent*, int>*)> trace_writer_;
|
||||||
|
std::unique_ptr<NodeTracing::Frontend> frontend_;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace protocol
|
||||||
|
} // namespace inspector
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#endif // SRC_INSPECTOR_TRACING_AGENT_H_
|
@ -1,6 +1,9 @@
|
|||||||
#include "inspector_agent.h"
|
#include "inspector_agent.h"
|
||||||
|
|
||||||
#include "inspector_io.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 "node_internals.h"
|
||||||
#include "v8-inspector.h"
|
#include "v8-inspector.h"
|
||||||
#include "v8-platform.h"
|
#include "v8-platform.h"
|
||||||
@ -187,18 +190,35 @@ static int StartDebugSignalHandler() {
|
|||||||
const int NANOS_PER_MSEC = 1000000;
|
const int NANOS_PER_MSEC = 1000000;
|
||||||
const int CONTEXT_GROUP_ID = 1;
|
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:
|
public:
|
||||||
explicit ChannelImpl(const std::unique_ptr<V8Inspector>& inspector,
|
explicit ChannelImpl(Environment* env,
|
||||||
|
const std::unique_ptr<V8Inspector>& inspector,
|
||||||
std::unique_ptr<InspectorSessionDelegate> delegate)
|
std::unique_ptr<InspectorSessionDelegate> delegate)
|
||||||
: delegate_(std::move(delegate)) {
|
: delegate_(std::move(delegate)) {
|
||||||
session_ = inspector->connect(1, this, StringView());
|
session_ = inspector->connect(1, this, StringView());
|
||||||
|
node_dispatcher_ = std::make_unique<protocol::UberDispatcher>(this);
|
||||||
|
tracing_agent_ = std::make_unique<protocol::TracingAgent>(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) {
|
void dispatchProtocolMessage(const StringView& message) {
|
||||||
session_->dispatchProtocolMessage(message);
|
std::unique_ptr<protocol::DictionaryValue> 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) {
|
void schedulePauseOnNextStatement(const std::string& reason) {
|
||||||
@ -224,8 +244,25 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel {
|
|||||||
delegate_->SendMessageToFrontend(message);
|
delegate_->SendMessageToFrontend(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sendMessageToFrontend(const std::string& message) {
|
||||||
|
sendMessageToFrontend(Utf8ToStringView(message)->string());
|
||||||
|
}
|
||||||
|
|
||||||
|
using Serializable = protocol::Serializable;
|
||||||
|
|
||||||
|
void sendProtocolResponse(int callId,
|
||||||
|
std::unique_ptr<Serializable> message) override {
|
||||||
|
sendMessageToFrontend(message->serialize());
|
||||||
|
}
|
||||||
|
void sendProtocolNotification(
|
||||||
|
std::unique_ptr<Serializable> message) override {
|
||||||
|
sendMessageToFrontend(message->serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<protocol::TracingAgent> tracing_agent_;
|
||||||
std::unique_ptr<InspectorSessionDelegate> delegate_;
|
std::unique_ptr<InspectorSessionDelegate> delegate_;
|
||||||
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
|
std::unique_ptr<v8_inspector::V8InspectorSession> session_;
|
||||||
|
std::unique_ptr<protocol::UberDispatcher> node_dispatcher_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InspectorTimer {
|
class InspectorTimer {
|
||||||
@ -369,7 +406,8 @@ class NodeInspectorClient : public V8InspectorClient {
|
|||||||
int session_id = next_session_id_++;
|
int session_id = next_session_id_++;
|
||||||
// TODO(addaleax): Revert back to using make_unique once we get issues
|
// TODO(addaleax): Revert back to using make_unique once we get issues
|
||||||
// with CI resolved (i.e. revert the patch that added this comment).
|
// 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;
|
return session_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,7 +676,8 @@ void Agent::DisableAsyncHook() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Agent::ToggleAsyncHook(Isolate* isolate, const Persistent<Function>& fn) {
|
void Agent::ToggleAsyncHook(Isolate* isolate,
|
||||||
|
const node::Persistent<Function>& fn) {
|
||||||
HandleScope handle_scope(isolate);
|
HandleScope handle_scope(isolate);
|
||||||
CHECK(!fn.IsEmpty());
|
CHECK(!fn.IsEmpty());
|
||||||
auto context = parent_env_->context();
|
auto context = parent_env_->context();
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "node_debug_options.h"
|
#include "node_debug_options.h"
|
||||||
#include "node_platform.h"
|
#include "node_persistent.h"
|
||||||
#include "v8.h"
|
#include "v8.h"
|
||||||
|
|
||||||
namespace v8_inspector {
|
namespace v8_inspector {
|
||||||
@ -20,6 +20,7 @@ class StringView;
|
|||||||
namespace node {
|
namespace node {
|
||||||
// Forward declaration to break recursive dependency chain with src/env.h.
|
// Forward declaration to break recursive dependency chain with src/env.h.
|
||||||
class Environment;
|
class Environment;
|
||||||
|
class NodePlatform;
|
||||||
struct ContextInfo;
|
struct ContextInfo;
|
||||||
|
|
||||||
namespace inspector {
|
namespace inspector {
|
||||||
@ -102,7 +103,7 @@ class Agent {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void ToggleAsyncHook(v8::Isolate* isolate,
|
void ToggleAsyncHook(v8::Isolate* isolate,
|
||||||
const Persistent<v8::Function>& fn);
|
const node::Persistent<v8::Function>& fn);
|
||||||
|
|
||||||
node::Environment* parent_env_;
|
node::Environment* parent_env_;
|
||||||
std::shared_ptr<NodeInspectorClient> client_;
|
std::shared_ptr<NodeInspectorClient> client_;
|
||||||
@ -113,8 +114,8 @@ class Agent {
|
|||||||
|
|
||||||
bool pending_enable_async_hook_;
|
bool pending_enable_async_hook_;
|
||||||
bool pending_disable_async_hook_;
|
bool pending_disable_async_hook_;
|
||||||
Persistent<v8::Function> enable_async_hook_function_;
|
node::Persistent<v8::Function> enable_async_hook_function_;
|
||||||
Persistent<v8::Function> disable_async_hook_function_;
|
node::Persistent<v8::Function> disable_async_hook_function_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace inspector
|
} // namespace inspector
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#include "inspector_io.h"
|
#include "inspector_io.h"
|
||||||
|
|
||||||
#include "inspector_socket_server.h"
|
#include "inspector_socket_server.h"
|
||||||
|
#include "inspector/node_string.h"
|
||||||
#include "env-inl.h"
|
#include "env-inl.h"
|
||||||
#include "node.h"
|
#include "node.h"
|
||||||
#include "node_crypto.h"
|
#include "node_crypto.h"
|
||||||
@ -62,31 +63,6 @@ std::string GenerateID() {
|
|||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string StringViewToUtf8(const StringView& view) {
|
|
||||||
if (view.is8Bit()) {
|
|
||||||
return std::string(reinterpret_cast<const char*>(view.characters8()),
|
|
||||||
view.length());
|
|
||||||
}
|
|
||||||
const uint16_t* source = view.characters16();
|
|
||||||
const UChar* unicodeSource = reinterpret_cast<const UChar*>(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) {
|
void HandleSyncCloseCb(uv_handle_t* handle) {
|
||||||
*static_cast<bool*>(handle->data) = true;
|
*static_cast<bool*>(handle->data) = true;
|
||||||
}
|
}
|
||||||
@ -272,7 +248,8 @@ void InspectorIo::IoThreadAsyncCb(uv_async_t* async) {
|
|||||||
break;
|
break;
|
||||||
case TransportAction::kSendMessage:
|
case TransportAction::kSendMessage:
|
||||||
transport->Send(session_id,
|
transport->Send(session_id,
|
||||||
StringViewToUtf8(std::get<2>(outgoing)->string()));
|
protocol::StringUtil::StringViewToUtf8(
|
||||||
|
std::get<2>(outgoing)->string()));
|
||||||
break;
|
break;
|
||||||
case TransportAction::kAcceptSession:
|
case TransportAction::kAcceptSession:
|
||||||
transport->AcceptSession(session_id);
|
transport->AcceptSession(session_id);
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
#include "uv.h"
|
#include "uv.h"
|
||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <map>
|
#include <unordered_map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
@ -8,11 +8,43 @@
|
|||||||
namespace node {
|
namespace node {
|
||||||
namespace tracing {
|
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<std::string> flatten(
|
||||||
|
const std::unordered_map<int, std::set<std::string>>& map) {
|
||||||
|
std::set<std::string> 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::TraceConfig;
|
||||||
|
using v8::platform::tracing::TraceWriter;
|
||||||
using std::string;
|
using std::string;
|
||||||
|
|
||||||
Agent::Agent(const std::string& log_file_pattern)
|
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_ = new TracingController();
|
||||||
tracing_controller_->Initialize(nullptr);
|
tracing_controller_->Initialize(nullptr);
|
||||||
}
|
}
|
||||||
@ -23,11 +55,9 @@ void Agent::Start() {
|
|||||||
|
|
||||||
CHECK_EQ(uv_loop_init(&tracing_loop_), 0);
|
CHECK_EQ(uv_loop_init(&tracing_loop_), 0);
|
||||||
|
|
||||||
NodeTraceWriter* trace_writer =
|
NodeTraceBuffer* trace_buffer_ = new NodeTraceBuffer(
|
||||||
new NodeTraceWriter(log_file_pattern_, &tracing_loop_);
|
NodeTraceBuffer::kBufferChunks, this, &tracing_loop_);
|
||||||
TraceBuffer* trace_buffer = new NodeTraceBuffer(
|
tracing_controller_->Initialize(trace_buffer_);
|
||||||
NodeTraceBuffer::kBufferChunks, trace_writer, &tracing_loop_);
|
|
||||||
tracing_controller_->Initialize(trace_buffer);
|
|
||||||
|
|
||||||
// This thread should be created *after* async handles are created
|
// This thread should be created *after* async handles are created
|
||||||
// (within NodeTraceWriter and NodeTraceBuffer constructors).
|
// (within NodeTraceWriter and NodeTraceBuffer constructors).
|
||||||
@ -36,7 +66,23 @@ void Agent::Start() {
|
|||||||
started_ = true;
|
started_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Agent::ClientHandle Agent::AddClient(const std::set<std::string>& categories,
|
||||||
|
std::unique_ptr<AsyncTraceWriter> 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<Agent*, int>(this, id);
|
||||||
|
return ClientHandle(client_id, &DisconnectClient);
|
||||||
|
}
|
||||||
|
|
||||||
void Agent::Stop() {
|
void Agent::Stop() {
|
||||||
|
file_writer_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Agent::StopTracing() {
|
||||||
if (!started_)
|
if (!started_)
|
||||||
return;
|
return;
|
||||||
// Perform final Flush on TraceBuffer. We don't want the tracing controller
|
// Perform final Flush on TraceBuffer. We don't want the tracing controller
|
||||||
@ -49,6 +95,12 @@ void Agent::Stop() {
|
|||||||
uv_thread_join(&thread_);
|
uv_thread_join(&thread_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Agent::Disconnect(int client) {
|
||||||
|
ScopedSuspendTracing suspend(tracing_controller_, this);
|
||||||
|
writers_.erase(client);
|
||||||
|
categories_.erase(client);
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void Agent::ThreadCb(void* arg) {
|
void Agent::ThreadCb(void* arg) {
|
||||||
Agent* agent = static_cast<Agent*>(arg);
|
Agent* agent = static_cast<Agent*>(arg);
|
||||||
@ -56,72 +108,81 @@ void Agent::ThreadCb(void* arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Agent::Enable(const std::string& categories) {
|
void Agent::Enable(const std::string& categories) {
|
||||||
if (!categories.empty()) {
|
if (categories.empty())
|
||||||
std::stringstream category_list(categories);
|
return;
|
||||||
while (category_list.good()) {
|
std::set<std::string> categories_set;
|
||||||
std::string category;
|
std::stringstream category_list(categories);
|
||||||
getline(category_list, category, ',');
|
while (category_list.good()) {
|
||||||
categories_.insert(category.c_str());
|
std::string category;
|
||||||
}
|
getline(category_list, category, ',');
|
||||||
RestartTracing();
|
categories_set.insert(category);
|
||||||
}
|
}
|
||||||
|
Enable(categories_set);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Agent::Enable(const std::set<std::string>& categories) {
|
void Agent::Enable(const std::set<std::string>& categories) {
|
||||||
if (!categories.empty()) {
|
std::string cats;
|
||||||
categories_.insert(categories.begin(), categories.end());
|
for (const std::string cat : categories)
|
||||||
RestartTracing();
|
cats += cat + ", ";
|
||||||
|
if (categories.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
file_writer_categories_.insert(categories.begin(), categories.end());
|
||||||
|
std::set<std::string> full_list(file_writer_categories_.begin(),
|
||||||
|
file_writer_categories_.end());
|
||||||
|
if (!file_writer_) {
|
||||||
|
// Ensure background thread is running
|
||||||
|
Start();
|
||||||
|
std::unique_ptr<NodeTraceWriter> 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::Disable(const std::set<std::string>& categories) {
|
void Agent::Disable(const std::set<std::string>& categories) {
|
||||||
if (!categories.empty()) {
|
for (auto category : categories) {
|
||||||
for (auto category : categories) {
|
auto it = file_writer_categories_.find(category);
|
||||||
auto pos = categories_.lower_bound(category);
|
if (it != file_writer_categories_.end())
|
||||||
if (pos != categories_.end())
|
file_writer_categories_.erase(it);
|
||||||
categories_.erase(pos);
|
|
||||||
}
|
|
||||||
RestartTracing();
|
|
||||||
}
|
}
|
||||||
}
|
if (!file_writer_)
|
||||||
|
return;
|
||||||
void Agent::RestartTracing() {
|
ScopedSuspendTracing suspend(tracing_controller_, this);
|
||||||
static bool warned;
|
categories_[file_writer_->second] = { file_writer_categories_.begin(),
|
||||||
if (!warned) {
|
file_writer_categories_.end() };
|
||||||
warned = true;
|
|
||||||
fprintf(stderr, "Warning: Trace event is an experimental feature "
|
|
||||||
"and could change at any time.\n");
|
|
||||||
}
|
|
||||||
Start(); // Start the agent if it hasn't already been started
|
|
||||||
tracing_controller_->StopTracing();
|
|
||||||
auto config = CreateTraceConfig();
|
|
||||||
if (config != nullptr)
|
|
||||||
tracing_controller_->StartTracing(config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceConfig* Agent::CreateTraceConfig() {
|
TraceConfig* Agent::CreateTraceConfig() {
|
||||||
if (categories_.empty())
|
if (categories_.empty())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
TraceConfig* trace_config = new TraceConfig();
|
TraceConfig* trace_config = new TraceConfig();
|
||||||
for (auto category = categories_.begin();
|
for (const auto& category : flatten(categories_)) {
|
||||||
category != categories_.end();
|
trace_config->AddIncludedCategory(category.c_str());
|
||||||
category = categories_.upper_bound(*category)) {
|
|
||||||
trace_config->AddIncludedCategory(category->c_str());
|
|
||||||
}
|
}
|
||||||
return trace_config;
|
return trace_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Agent::GetEnabledCategories() {
|
std::string Agent::GetEnabledCategories() {
|
||||||
std::string categories;
|
std::string categories;
|
||||||
for (auto category = categories_.begin();
|
for (const auto& category : flatten(categories_)) {
|
||||||
category != categories_.end();
|
|
||||||
category = categories_.upper_bound(*category)) {
|
|
||||||
if (!categories.empty())
|
if (!categories.empty())
|
||||||
categories += ',';
|
categories += ',';
|
||||||
categories += *category;
|
categories += category;
|
||||||
}
|
}
|
||||||
return categories;
|
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 tracing
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
@ -7,11 +7,20 @@
|
|||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
namespace tracing {
|
namespace tracing {
|
||||||
|
|
||||||
using v8::platform::tracing::TraceConfig;
|
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 {
|
class TracingController : public v8::platform::tracing::TracingController {
|
||||||
public:
|
public:
|
||||||
@ -22,33 +31,58 @@ class TracingController : public v8::platform::tracing::TracingController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class Agent {
|
class Agent {
|
||||||
public:
|
public:
|
||||||
|
// Resetting the pointer disconnects client
|
||||||
|
using ClientHandle = std::unique_ptr<std::pair<Agent*, int>,
|
||||||
|
void (*)(std::pair<Agent*, int>*)>;
|
||||||
|
|
||||||
|
static ClientHandle EmptyClientHandle() {
|
||||||
|
return ClientHandle(nullptr, DisconnectClient);
|
||||||
|
}
|
||||||
explicit Agent(const std::string& log_file_pattern);
|
explicit Agent(const std::string& log_file_pattern);
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
TracingController* GetTracingController() { return tracing_controller_; }
|
TracingController* GetTracingController() { return tracing_controller_; }
|
||||||
|
|
||||||
|
// Destroying the handle disconnects the client
|
||||||
|
ClientHandle AddClient(const std::set<std::string>& categories,
|
||||||
|
std::unique_ptr<AsyncTraceWriter> writer);
|
||||||
|
|
||||||
|
// These 3 methods operate on a "default" client, e.g. the file writer
|
||||||
void Enable(const std::string& categories);
|
void Enable(const std::string& categories);
|
||||||
void Enable(const std::set<std::string>& categories);
|
void Enable(const std::set<std::string>& categories);
|
||||||
void Disable(const std::set<std::string>& categories);
|
void Disable(const std::set<std::string>& categories);
|
||||||
std::string GetEnabledCategories();
|
std::string GetEnabledCategories();
|
||||||
|
|
||||||
private:
|
void AppendTraceEvent(TraceObject* trace_event);
|
||||||
static void ThreadCb(void* arg);
|
void Flush(bool blocking);
|
||||||
|
|
||||||
void Start();
|
|
||||||
void RestartTracing();
|
|
||||||
|
|
||||||
TraceConfig* CreateTraceConfig();
|
TraceConfig* CreateTraceConfig();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void ThreadCb(void* arg);
|
||||||
|
static void DisconnectClient(std::pair<Agent*, int>* id_agent) {
|
||||||
|
id_agent->first->Disconnect(id_agent->second);
|
||||||
|
delete id_agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start();
|
||||||
|
void StopTracing();
|
||||||
|
void Disconnect(int client);
|
||||||
|
|
||||||
const std::string& log_file_pattern_;
|
const std::string& log_file_pattern_;
|
||||||
uv_thread_t thread_;
|
uv_thread_t thread_;
|
||||||
uv_loop_t tracing_loop_;
|
uv_loop_t tracing_loop_;
|
||||||
bool started_ = false;
|
bool started_ = false;
|
||||||
|
|
||||||
std::multiset<std::string> categories_;
|
std::unordered_map<int, std::set<std::string>> categories_;
|
||||||
TracingController* tracing_controller_ = nullptr;
|
TracingController* tracing_controller_ = nullptr;
|
||||||
|
ClientHandle file_writer_;
|
||||||
|
int next_writer_id_ = 1;
|
||||||
|
std::unordered_map<int, std::unique_ptr<AsyncTraceWriter>> writers_;
|
||||||
|
std::multiset<std::string> file_writer_categories_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace tracing
|
} // namespace tracing
|
||||||
|
@ -4,9 +4,9 @@ namespace node {
|
|||||||
namespace tracing {
|
namespace tracing {
|
||||||
|
|
||||||
InternalTraceBuffer::InternalTraceBuffer(size_t max_chunks, uint32_t id,
|
InternalTraceBuffer::InternalTraceBuffer(size_t max_chunks, uint32_t id,
|
||||||
NodeTraceWriter* trace_writer)
|
Agent* agent)
|
||||||
: flushing_(false), max_chunks_(max_chunks),
|
: flushing_(false), max_chunks_(max_chunks),
|
||||||
trace_writer_(trace_writer), id_(id) {
|
agent_(agent), id_(id) {
|
||||||
chunks_.resize(max_chunks);
|
chunks_.resize(max_chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,14 +59,14 @@ void InternalTraceBuffer::Flush(bool blocking) {
|
|||||||
for (size_t i = 0; i < total_chunks_; ++i) {
|
for (size_t i = 0; i < total_chunks_; ++i) {
|
||||||
auto& chunk = chunks_[i];
|
auto& chunk = chunks_[i];
|
||||||
for (size_t j = 0; j < chunk->size(); ++j) {
|
for (size_t j = 0; j < chunk->size(); ++j) {
|
||||||
trace_writer_->AppendTraceEvent(chunk->GetEventAt(j));
|
agent_->AppendTraceEvent(chunk->GetEventAt(j));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
total_chunks_ = 0;
|
total_chunks_ = 0;
|
||||||
flushing_ = false;
|
flushing_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace_writer_->Flush(blocking);
|
agent_->Flush(blocking);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t InternalTraceBuffer::MakeHandle(
|
uint64_t InternalTraceBuffer::MakeHandle(
|
||||||
@ -87,10 +87,10 @@ void InternalTraceBuffer::ExtractHandle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
NodeTraceBuffer::NodeTraceBuffer(size_t max_chunks,
|
NodeTraceBuffer::NodeTraceBuffer(size_t max_chunks,
|
||||||
NodeTraceWriter* trace_writer, uv_loop_t* tracing_loop)
|
Agent* agent, uv_loop_t* tracing_loop)
|
||||||
: tracing_loop_(tracing_loop), trace_writer_(trace_writer),
|
: tracing_loop_(tracing_loop), agent_(agent),
|
||||||
buffer1_(max_chunks, 0, trace_writer),
|
buffer1_(max_chunks, 0, agent),
|
||||||
buffer2_(max_chunks, 1, trace_writer) {
|
buffer2_(max_chunks, 1, agent) {
|
||||||
current_buf_.store(&buffer1_);
|
current_buf_.store(&buffer1_);
|
||||||
|
|
||||||
flush_signal_.data = this;
|
flush_signal_.data = this;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#ifndef SRC_TRACING_NODE_TRACE_BUFFER_H_
|
#ifndef SRC_TRACING_NODE_TRACE_BUFFER_H_
|
||||||
#define SRC_TRACING_NODE_TRACE_BUFFER_H_
|
#define SRC_TRACING_NODE_TRACE_BUFFER_H_
|
||||||
|
|
||||||
|
#include "tracing/agent.h"
|
||||||
#include "node_mutex.h"
|
#include "node_mutex.h"
|
||||||
#include "tracing/node_trace_writer.h"
|
|
||||||
#include "libplatform/v8-tracing.h"
|
#include "libplatform/v8-tracing.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@ -19,8 +19,7 @@ class NodeTraceBuffer;
|
|||||||
|
|
||||||
class InternalTraceBuffer {
|
class InternalTraceBuffer {
|
||||||
public:
|
public:
|
||||||
InternalTraceBuffer(size_t max_chunks, uint32_t id,
|
InternalTraceBuffer(size_t max_chunks, uint32_t id, Agent* agent);
|
||||||
NodeTraceWriter* trace_writer);
|
|
||||||
|
|
||||||
TraceObject* AddTraceEvent(uint64_t* handle);
|
TraceObject* AddTraceEvent(uint64_t* handle);
|
||||||
TraceObject* GetEventByHandle(uint64_t handle);
|
TraceObject* GetEventByHandle(uint64_t handle);
|
||||||
@ -42,7 +41,7 @@ class InternalTraceBuffer {
|
|||||||
Mutex mutex_;
|
Mutex mutex_;
|
||||||
bool flushing_;
|
bool flushing_;
|
||||||
size_t max_chunks_;
|
size_t max_chunks_;
|
||||||
NodeTraceWriter* trace_writer_;
|
Agent* agent_;
|
||||||
std::vector<std::unique_ptr<TraceBufferChunk>> chunks_;
|
std::vector<std::unique_ptr<TraceBufferChunk>> chunks_;
|
||||||
size_t total_chunks_ = 0;
|
size_t total_chunks_ = 0;
|
||||||
uint32_t current_chunk_seq_ = 1;
|
uint32_t current_chunk_seq_ = 1;
|
||||||
@ -51,8 +50,7 @@ class InternalTraceBuffer {
|
|||||||
|
|
||||||
class NodeTraceBuffer : public TraceBuffer {
|
class NodeTraceBuffer : public TraceBuffer {
|
||||||
public:
|
public:
|
||||||
NodeTraceBuffer(size_t max_chunks, NodeTraceWriter* trace_writer,
|
NodeTraceBuffer(size_t max_chunks, Agent* agent, uv_loop_t* tracing_loop);
|
||||||
uv_loop_t* tracing_loop);
|
|
||||||
~NodeTraceBuffer();
|
~NodeTraceBuffer();
|
||||||
|
|
||||||
TraceObject* AddTraceEvent(uint64_t* handle) override;
|
TraceObject* AddTraceEvent(uint64_t* handle) override;
|
||||||
@ -74,7 +72,7 @@ class NodeTraceBuffer : public TraceBuffer {
|
|||||||
Mutex exit_mutex_;
|
Mutex exit_mutex_;
|
||||||
// Used to wait until async handles have been closed.
|
// Used to wait until async handles have been closed.
|
||||||
ConditionVariable exit_cond_;
|
ConditionVariable exit_cond_;
|
||||||
std::unique_ptr<NodeTraceWriter> trace_writer_;
|
Agent* agent_;
|
||||||
std::atomic<InternalTraceBuffer*> current_buf_;
|
std::atomic<InternalTraceBuffer*> current_buf_;
|
||||||
InternalTraceBuffer buffer1_;
|
InternalTraceBuffer buffer1_;
|
||||||
InternalTraceBuffer buffer2_;
|
InternalTraceBuffer buffer2_;
|
||||||
|
@ -126,12 +126,6 @@ void NodeTraceWriter::FlushSignalCb(uv_async_t* signal) {
|
|||||||
trace_writer->FlushPrivate();
|
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) {
|
void NodeTraceWriter::Flush(bool blocking) {
|
||||||
Mutex::ScopedLock scoped_lock(request_mutex_);
|
Mutex::ScopedLock scoped_lock(request_mutex_);
|
||||||
if (!json_trace_writer_) {
|
if (!json_trace_writer_) {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "node_mutex.h"
|
#include "node_mutex.h"
|
||||||
#include "libplatform/v8-tracing.h"
|
#include "libplatform/v8-tracing.h"
|
||||||
|
#include "tracing/agent.h"
|
||||||
#include "uv.h"
|
#include "uv.h"
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
@ -14,15 +15,14 @@ namespace tracing {
|
|||||||
using v8::platform::tracing::TraceObject;
|
using v8::platform::tracing::TraceObject;
|
||||||
using v8::platform::tracing::TraceWriter;
|
using v8::platform::tracing::TraceWriter;
|
||||||
|
|
||||||
class NodeTraceWriter : public TraceWriter {
|
class NodeTraceWriter : public AsyncTraceWriter {
|
||||||
public:
|
public:
|
||||||
explicit NodeTraceWriter(const std::string& log_file_pattern,
|
explicit NodeTraceWriter(const std::string& log_file_pattern,
|
||||||
uv_loop_t* tracing_loop);
|
uv_loop_t* tracing_loop);
|
||||||
~NodeTraceWriter();
|
~NodeTraceWriter();
|
||||||
|
|
||||||
void AppendTraceEvent(TraceObject* trace_event) override;
|
void AppendTraceEvent(TraceObject* trace_event) override;
|
||||||
void Flush() override;
|
void Flush(bool blocking) override;
|
||||||
void Flush(bool blocking);
|
|
||||||
|
|
||||||
static const int kTracesPerFile = 1 << 19;
|
static const int kTracesPerFile = 1 << 19;
|
||||||
|
|
||||||
|
@ -56,4 +56,8 @@ async function test() {
|
|||||||
|
|
||||||
common.crashOnUnhandledRejection();
|
common.crashOnUnhandledRejection();
|
||||||
|
|
||||||
test();
|
const interval = setInterval(() => {}, 1000);
|
||||||
|
test().then(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
console.log('Done!');
|
||||||
|
});
|
||||||
|
70
test/parallel/test-inspector-tracing-domain.js
Normal file
70
test/parallel/test-inspector-tracing-domain.js
Normal file
@ -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();
|
Loading…
x
Reference in New Issue
Block a user