Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf_hooks: emit user timing marks, measures and timerify to trace events #18789

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ added: v8.5.0

* {string}

The type of the performance entry. Current it may be one of: `'node'`, `'mark'`,
`'measure'`, `'gc'`, or `'function'`.
The type of the performance entry. Currently it may be one of: `'node'`,
`'mark'`, `'measure'`, `'gc'`, or `'function'`.

### performanceEntry.kind
<!-- YAML
Expand Down
18 changes: 16 additions & 2 deletions doc/api/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,20 @@ starting a Node.js application.

The set of categories for which traces are recorded can be specified using the
`--trace-event-categories` flag followed by a list of comma separated category
names. By default the `node`, `node.async_hooks`, and `v8` categories are
enabled.
names.

The available categories are:

* `node`
* `node.async_hooks` - Enables capture of detailed async_hooks trace data.
* `node.perf` - Enables capture of [Performance API] measurements.
* `node.perf.usertiming` - Enables capture of only Performance API User Timing
measures and marks.
* `node.perf.timerify` - Enables capture of only Performance API timerify
measurements.
* `v8`

By default the `node`, `node.async_hooks`, and `v8` categories are enabled.

```txt
node --trace-events-enabled --trace-event-categories v8,node,node.async_hooks server.js
Expand All @@ -24,3 +36,5 @@ tab of Chrome.
Starting with Node 10.0.0, the tracing system uses the same time source as the
one used by `process.hrtime()` however the trace-event timestamps are expressed
in microseconds, unlike `process.hrtime()` which returns nanoseconds.

[Performance API]: perf_hooks.html
6 changes: 4 additions & 2 deletions lib/perf_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
NODE_PERFORMANCE_MILESTONE_PRELOAD_MODULE_LOAD_END
} = constants;

const { AsyncResource } = require('async_hooks');
const L = require('internal/linkedlist');
const kInspect = require('internal/util').customInspectSymbol;
const { inherits } = require('util');
Expand Down Expand Up @@ -317,12 +318,13 @@ class PerformanceObserverEntryList {
}
}

class PerformanceObserver {
class PerformanceObserver extends AsyncResource {
constructor(callback) {
if (typeof callback !== 'function') {
const errors = lazyErrors();
throw new errors.TypeError('ERR_INVALID_CALLBACK');
}
super('PerformanceObserver');
Object.defineProperties(this, {
[kTypes]: {
enumerable: false,
Expand Down Expand Up @@ -568,7 +570,7 @@ function getObserversList(type) {

function doNotify() {
this[kQueued] = false;
this[kCallback](this[kBuffer], this);
this.runInAsyncScope(this[kCallback], this, this[kBuffer], this);
this[kBuffer][kEntries] = [];
L.init(this[kBuffer][kEntries]);
}
Expand Down
22 changes: 17 additions & 5 deletions src/node_perf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void PerformanceEntry::Notify(Environment* env,
if (type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID &&
observers[type]) {
node::MakeCallback(env->isolate(),
env->process_object(),
object.As<Object>(),
env->performance_entry_callback(),
1, &object,
node::async_context{0, 0}).ToLocalChecked();
Expand All @@ -105,8 +105,8 @@ void Mark(const FunctionCallbackInfo<Value>& args) {
auto marks = env->performance_marks();
(*marks)[*name] = now;

// TODO(jasnell): Once Tracing API is fully implemented, this should
// record a trace event also.
TRACE_EVENT_COPY_MARK_WITH_TIMESTAMP(
"node.perf,node.perf.usertiming", *name, now / 1000);

PerformanceEntry entry(env, *name, "mark", now, now);
Local<Object> obj = entry.ToObject();
Expand Down Expand Up @@ -153,8 +153,10 @@ void Measure(const FunctionCallbackInfo<Value>& args) {
if (endTimestamp < startTimestamp)
endTimestamp = startTimestamp;

// TODO(jasnell): Once Tracing API is fully implemented, this should
// record a trace event also.
TRACE_EVENT_COPY_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
"node.perf,node.perf.usertiming", *name, *name, startTimestamp / 1000);
TRACE_EVENT_COPY_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
"node.perf,node.perf.usertiming", *name, *name, endTimestamp / 1000);

PerformanceEntry entry(env, *name, "measure", startTimestamp, endTimestamp);
Local<Object> obj = entry.ToObject();
Expand Down Expand Up @@ -269,22 +271,32 @@ void TimerFunctionCall(const FunctionCallbackInfo<Value>& args) {
v8::TryCatch try_catch(isolate);
if (args.IsConstructCall()) {
start = PERFORMANCE_NOW();
TRACE_EVENT_COPY_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
"node.perf,node.perf.timerify", *name, *name, start / 1000);
v8::MaybeLocal<Object> ret = fn->NewInstance(context,
call_args.size(),
call_args.data());
end = PERFORMANCE_NOW();
TRACE_EVENT_COPY_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
"node.perf,node.perf.timerify", *name, *name, end / 1000);

if (ret.IsEmpty()) {
try_catch.ReThrow();
return;
}
args.GetReturnValue().Set(ret.ToLocalChecked());
} else {
start = PERFORMANCE_NOW();
TRACE_EVENT_COPY_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
"node.perf,node.perf.timerify", *name, *name, start / 1000);
v8::MaybeLocal<Value> ret = fn->Call(context,
args.This(),
call_args.size(),
call_args.data());
end = PERFORMANCE_NOW();
TRACE_EVENT_COPY_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
"node.perf,node.perf.timerify", *name, *name, end / 1000);

if (ret.IsEmpty()) {
try_catch.ReThrow();
return;
Expand Down
1 change: 1 addition & 0 deletions src/node_perf.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include "node.h"
#include "node_internals.h"
#include "node_perf_common.h"
#include "env.h"
#include "base_object-inl.h"
Expand Down
17 changes: 14 additions & 3 deletions src/tracing/trace_event.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,20 @@ enum CategoryGroupEnabledFlags {

// Adds a trace event with a given id, thread_id, and timestamp. Not
// Implemented.
#define INTERNAL_TRACE_EVENT_ADD_WITH_ID_TID_AND_TIMESTAMP( \
phase, category_group, name, id, thread_id, timestamp, flags, ...) \
UNIMPLEMENTED()
#define INTERNAL_TRACE_EVENT_ADD_WITH_ID_TID_AND_TIMESTAMP( \
Copy link
Member

Choose a reason for hiding this comment

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

Is a sync with upstream?

Copy link
Member Author

Choose a reason for hiding this comment

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

In sync with upstream? Nope, not yet. But that reminds me...

@ofrobots ... ^^ To implement this I needed this macro implemented also.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jasnell created https://chromium-review.googlesource.com/c/v8/v8/+/924507 that adds the same macro upstream.

Note that for the trace-event macros, a sync with upstream is no longer a necessity. Our copy of trace_event.h is derived from upstream, but has a slightly different implementation. It is okay for them to pick up features at a different pace. Please do continue to ping if we add more macros locally. There is no need to block on upstream.

phase, category_group, name, id, thread_id, timestamp, flags, ...) \
do { \
INTERNAL_TRACE_EVENT_GET_CATEGORY_INFO(category_group); \
if (INTERNAL_TRACE_EVENT_CATEGORY_GROUP_ENABLED_FOR_RECORDING_MODE()) { \
unsigned int trace_event_flags = flags | TRACE_EVENT_FLAG_HAS_ID; \
node::tracing::TraceID trace_event_trace_id(id, \
&trace_event_flags); \
node::tracing::AddTraceEventWithTimestamp( \
phase, INTERNAL_TRACE_EVENT_UID(category_group_enabled), name, \
trace_event_trace_id.scope(), trace_event_trace_id.raw_id(), \
node::tracing::kNoId, trace_event_flags, timestamp, ##__VA_ARGS__);\
} \
} while (0)

// Enter and leave a context based on the current scope.
#define INTERNAL_TRACE_EVENT_SCOPED_CONTEXT(category_group, name, context) \
Expand Down
82 changes: 82 additions & 0 deletions test/parallel/test-trace-events-perf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
const path = require('path');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');

if (process.argv[2] === 'child') {
const { performance } = require('perf_hooks');

// Will emit mark and measure trace events
performance.mark('A');
setTimeout(() => {
performance.mark('B');
performance.measure('A to B', 'A', 'B');
}, 1);

// Intentional non-op, part of the test
function f() {}
const ff = performance.timerify(f);
ff(); // Will emit a timerify trace event
} else {
tmpdir.refresh();
process.chdir(tmpdir.path);

const expectedMarks = ['A', 'B'];
const expectedBegins = [
{ cat: 'node.perf,node.perf.timerify', name: 'f' },
{ cat: 'node.perf,node.perf.usertiming', name: 'A to B' }
];
const expectedEnds = [
{ cat: 'node.perf,node.perf.timerify', name: 'f' },
{ cat: 'node.perf,node.perf.usertiming', name: 'A to B' }
];

const proc = cp.fork(__filename,
[
'child'
], {
execArgv: [
'--trace-events-enabled',
'--trace-event-categories',
'node.perf'
]
});

proc.once('exit', common.mustCall(() => {
const file = path.join(tmpdir.path, 'node_trace.1.log');

assert(common.fileExists(file));
fs.readFile(file, common.mustCall((err, data) => {
const traces = JSON.parse(data.toString()).traceEvents;
assert.strictEqual(traces.length,
expectedMarks.length +
expectedBegins.length +
expectedEnds.length);

traces.forEach((trace) => {
assert.strictEqual(trace.pid, proc.pid);
switch (trace.ph) {
case 'R':
assert.strictEqual(trace.cat, 'node.perf,node.perf.usertiming');
assert.strictEqual(trace.name, expectedMarks.shift());
break;
case 'b':
const expectedBegin = expectedBegins.shift();
assert.strictEqual(trace.cat, expectedBegin.cat);
assert.strictEqual(trace.name, expectedBegin.name);
break;
case 'e':
const expectedEnd = expectedEnds.shift();
assert.strictEqual(trace.cat, expectedEnd.cat);
assert.strictEqual(trace.name, expectedEnd.name);
break;
default:
assert.fail('Unexpected trace event phase');
}
});
}));
}));
}