Skip to content

Commit

Permalink
report: add support for Workers
Browse files Browse the repository at this point in the history
Include a report for each sub-Worker of the current Node.js instance.

This adds a feature that is necessary for eventually making the report
feature stable, as was discussed during the last collaborator summit.

Refs: openjs-foundation/summit#240

PR-URL: nodejs#31386
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
addaleax authored and MylesBorins committed Apr 1, 2020
1 parent 4e1a192 commit a9c4fe5
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 4 deletions.
21 changes: 21 additions & 0 deletions doc/api/report.md
Expand Up @@ -297,6 +297,7 @@ is provided below for reference.
"address": "0x000055fc7b2cb180"
}
],
"workers": [],
"environmentVariables": {
"REMOTEHOST": "REMOVED",
"MANPATH": "/opt/rh/devtoolset-3/root/usr/share/man:",
Expand Down Expand Up @@ -578,4 +579,24 @@ NODE_OPTIONS="--experimental-report --report-uncaught-exception \
Specific API documentation can be found under
[`process API documentation`][] section.

## Interaction with Workers
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/31386
description: Workers are now included in the report.
-->

[`Worker`][] threads can create reports in the same way that the main thread
does.

Reports will include information on any Workers that are children of the current
thread as part of the `workers` section, with each Worker generating a report
in the standard report format.

The thread which is generating the report will wait for the reports from Worker
threads to finish. However, the latency for this will usually be low, as both
running JavaScript and the event loop are interrupted to generate the report.

[`process API documentation`]: process.html
[`Worker`]: worker_threads.html
5 changes: 5 additions & 0 deletions src/env-inl.h
Expand Up @@ -893,6 +893,11 @@ inline void Environment::remove_sub_worker_context(worker::Worker* context) {
sub_worker_contexts_.erase(context);
}

template <typename Fn>
inline void Environment::ForEachWorker(Fn&& iterator) {
for (worker::Worker* w : sub_worker_contexts_) iterator(w);
}

inline void Environment::add_refs(int64_t diff) {
task_queues_async_refs_ += diff;
CHECK_GE(task_queues_async_refs_, 0);
Expand Down
2 changes: 2 additions & 0 deletions src/env.h
Expand Up @@ -1057,6 +1057,8 @@ class Environment : public MemoryRetainer {
inline void add_sub_worker_context(worker::Worker* context);
inline void remove_sub_worker_context(worker::Worker* context);
void stop_sub_worker_contexts();
template <typename Fn>
inline void ForEachWorker(Fn&& iterator);
inline bool is_stopping() const;
inline void set_stopping(bool value);
inline std::list<node_module>* extra_linked_bindings();
Expand Down
43 changes: 42 additions & 1 deletion src/node_report.cc
Expand Up @@ -4,6 +4,8 @@
#include "diagnosticfilename-inl.h"
#include "node_internals.h"
#include "node_metadata.h"
#include "node_mutex.h"
#include "node_worker.h"
#include "util.h"

#ifdef _WIN32
Expand All @@ -19,18 +21,20 @@
#include <cwctype>
#include <fstream>

constexpr int NODE_REPORT_VERSION = 1;
constexpr int NODE_REPORT_VERSION = 2;
constexpr int NANOS_PER_SEC = 1000 * 1000 * 1000;
constexpr double SEC_PER_MICROS = 1e-6;

namespace report {
using node::arraysize;
using node::ConditionVariable;
using node::DiagnosticFilename;
using node::Environment;
using node::Mutex;
using node::NativeSymbolDebuggingContext;
using node::PerIsolateOptions;
using node::TIME_TYPE;
using node::worker::Worker;
using v8::HeapSpaceStatistics;
using v8::HeapStatistics;
using v8::Isolate;
Expand Down Expand Up @@ -210,6 +214,10 @@ static void WriteNodeReport(Isolate* isolate,

// Report native process ID
writer.json_keyvalue("processId", pid);
if (env != nullptr)
writer.json_keyvalue("threadId", env->thread_id());
else
writer.json_keyvalue("threadId", JSONWriter::Null{});

{
// Report the process cwd.
Expand Down Expand Up @@ -259,6 +267,39 @@ static void WriteNodeReport(Isolate* isolate,

writer.json_arrayend();

writer.json_arraystart("workers");
if (env != nullptr) {
Mutex workers_mutex;
ConditionVariable notify;
std::vector<std::string> worker_infos;
size_t expected_results = 0;

env->ForEachWorker([&](Worker* w) {
expected_results += w->RequestInterrupt([&](Environment* env) {
std::ostringstream os;

GetNodeReport(env->isolate(),
env,
"Worker thread subreport",
trigger,
Local<String>(),
os);

Mutex::ScopedLock lock(workers_mutex);
worker_infos.emplace_back(os.str());
notify.Signal(lock);
});
});

Mutex::ScopedLock lock(workers_mutex);
worker_infos.reserve(expected_results);
while (worker_infos.size() < expected_results)
notify.Wait(lock);
for (const std::string& worker_info : worker_infos)
writer.json_element(JSONWriter::ForeignJSON { worker_info });
}
writer.json_arrayend();

// Report operating system information
PrintSystemInformation(&writer);

Expand Down
9 changes: 9 additions & 0 deletions src/node_report.h
Expand Up @@ -44,6 +44,7 @@ void GetNodeReport(v8::Isolate* isolate,
// Function declarations - utility functions in src/node_report_utils.cc
void WalkHandle(uv_handle_t* h, void* arg);
std::string EscapeJsonChars(const std::string& str);
std::string Reindent(const std::string& str, int indentation);

template <typename T>
std::string ValueToHexString(T value) {
Expand Down Expand Up @@ -146,6 +147,10 @@ class JSONWriter {

struct Null {}; // Usable as a JSON value.

struct ForeignJSON {
std::string as_string;
};

private:
template <typename T,
typename test_for_number = typename std::
Expand All @@ -161,6 +166,10 @@ class JSONWriter {
inline void write_value(const char* str) { write_string(str); }
inline void write_value(const std::string& str) { write_string(str); }

inline void write_value(const ForeignJSON& json) {
out_ << Reindent(json.as_string, indent_);
}

inline void write_string(const std::string& str) {
out_ << '"' << EscapeJsonChars(str) << '"';
}
Expand Down
24 changes: 24 additions & 0 deletions src/node_report_utils.cc
Expand Up @@ -263,4 +263,28 @@ std::string EscapeJsonChars(const std::string& str) {
return ret;
}

std::string Reindent(const std::string& str, int indent_depth) {
std::string indent;
for (int i = 0; i < indent_depth; i++) indent += ' ';

std::string out;
std::string::size_type pos = 0;
do {
std::string::size_type prev_pos = pos;
pos = str.find('\n', pos);

out.append(indent);

if (pos == std::string::npos) {
out.append(str, prev_pos, std::string::npos);
break;
} else {
pos++;
out.append(str, prev_pos, pos - prev_pos);
}
} while (true);

return out;
}

} // namespace report
11 changes: 8 additions & 3 deletions test/common/report.js
Expand Up @@ -53,7 +53,7 @@ function _validateContent(report) {
// Verify that all sections are present as own properties of the report.
const sections = ['header', 'javascriptStack', 'nativeStack',
'javascriptHeap', 'libuv', 'environmentVariables',
'sharedObjects', 'resourceUsage'];
'sharedObjects', 'resourceUsage', 'workers'];
if (!isWindows)
sections.push('userLimits');

Expand All @@ -74,16 +74,17 @@ function _validateContent(report) {
'componentVersions', 'release', 'osName', 'osRelease',
'osVersion', 'osMachine', 'cpus', 'host',
'glibcVersionRuntime', 'glibcVersionCompiler', 'cwd',
'reportVersion', 'networkInterfaces'];
'reportVersion', 'networkInterfaces', 'threadId'];
checkForUnknownFields(header, headerFields);
assert.strictEqual(header.reportVersion, 1); // Increment as needed.
assert.strictEqual(header.reportVersion, 2); // Increment as needed.
assert.strictEqual(typeof header.event, 'string');
assert.strictEqual(typeof header.trigger, 'string');
assert(typeof header.filename === 'string' || header.filename === null);
assert.notStrictEqual(new Date(header.dumpEventTime).toString(),
'Invalid Date');
assert(String(+header.dumpEventTimeStamp), header.dumpEventTimeStamp);
assert(Number.isSafeInteger(header.processId));
assert(Number.isSafeInteger(header.threadId) || header.threadId === null);
assert.strictEqual(typeof header.cwd, 'string');
assert(Array.isArray(header.commandLine));
header.commandLine.forEach((arg) => {
Expand Down Expand Up @@ -253,6 +254,10 @@ function _validateContent(report) {
report.sharedObjects.forEach((sharedObject) => {
assert.strictEqual(typeof sharedObject, 'string');
});

// Verify the format of the workers section.
assert(Array.isArray(report.workers));
report.workers.forEach(_validateContent);
}

function checkForUnknownFields(actual, expected) {
Expand Down
50 changes: 50 additions & 0 deletions test/report/test-report-worker.js
@@ -0,0 +1,50 @@
// Flags: --experimental-report
'use strict';
const common = require('../common');
common.skipIfReportDisabled();
const assert = require('assert');
const { Worker } = require('worker_threads');
const { once } = require('events');
const helper = require('../common/report');

async function basic() {
// Test that the report includes basic information about Worker threads.

const w = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message', () => {
/* Wait for message to stop the Worker */
});
`, { eval: true });

await once(w, 'online');

const report = process.report.getReport();
helper.validateContent(report);
assert.strictEqual(report.workers.length, 1);
helper.validateContent(report.workers[0]);

w.postMessage({});

await once(w, 'exit');
}

async function interruptingJS() {
// Test that the report also works when Worker threads are busy in JS land.

const w = new Worker('while (true);', { eval: true });

await once(w, 'online');

const report = process.report.getReport();
helper.validateContent(report);
assert.strictEqual(report.workers.length, 1);
helper.validateContent(report.workers[0]);

await w.terminate();
}

(async function() {
await basic();
await interruptingJS();
})().then(common.mustCall());

0 comments on commit a9c4fe5

Please sign in to comment.