perf_hooks: add HttpRequest statistics monitoring #28445
```js const { PerformanceObserver, performance } = require('perf_hooks'); const http = require('http'); const obs = new PerformanceObserver((items) => { const entry = items.getEntries()[0]; console.log(entry.name, entry.duration); }); obs.observe({ entryTypes: ['http'] }); const server = http.Server(function(req, res) { server.close(); res.writeHead(200); res.end('hello world\n'); }); server.listen(0, function() { const req = http.request({ port: this.address().port, path: '/', method: 'POST' }).end(); }); ``` PR-URL: https://github.com/nodejs/node/pull/28486 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
parent
ca0884a60b
commit
0ebf01dc53
@ -183,7 +183,7 @@ added: v8.5.0
|
|||||||
* {string}
|
* {string}
|
||||||
|
|
||||||
The type of the performance entry. Currently it may be one of: `'node'`,
|
The type of the performance entry. Currently it may be one of: `'node'`,
|
||||||
`'mark'`, `'measure'`, `'gc'`, `'function'`, or `'http2'`.
|
`'mark'`, `'measure'`, `'gc'`, `'function'`, `'http2'` or `'http'`.
|
||||||
|
|
||||||
### performanceEntry.kind
|
### performanceEntry.kind
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
|
@ -39,7 +39,12 @@ const {
|
|||||||
prepareError,
|
prepareError,
|
||||||
} = require('_http_common');
|
} = require('_http_common');
|
||||||
const { OutgoingMessage } = require('_http_outgoing');
|
const { OutgoingMessage } = require('_http_outgoing');
|
||||||
const { outHeadersKey, ondrain, nowDate } = require('internal/http');
|
const {
|
||||||
|
outHeadersKey,
|
||||||
|
ondrain,
|
||||||
|
nowDate,
|
||||||
|
emitStatistics
|
||||||
|
} = require('internal/http');
|
||||||
const {
|
const {
|
||||||
defaultTriggerAsyncIdScope,
|
defaultTriggerAsyncIdScope,
|
||||||
getOrSetAsyncId
|
getOrSetAsyncId
|
||||||
@ -56,8 +61,11 @@ const {
|
|||||||
DTRACE_HTTP_SERVER_REQUEST,
|
DTRACE_HTTP_SERVER_REQUEST,
|
||||||
DTRACE_HTTP_SERVER_RESPONSE
|
DTRACE_HTTP_SERVER_RESPONSE
|
||||||
} = require('internal/dtrace');
|
} = require('internal/dtrace');
|
||||||
|
const { observerCounts, constants } = internalBinding('performance');
|
||||||
|
const { NODE_PERFORMANCE_ENTRY_TYPE_HTTP } = constants;
|
||||||
|
|
||||||
const kServerResponse = Symbol('ServerResponse');
|
const kServerResponse = Symbol('ServerResponse');
|
||||||
|
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
|
||||||
|
|
||||||
const STATUS_CODES = {
|
const STATUS_CODES = {
|
||||||
100: 'Continue',
|
100: 'Continue',
|
||||||
@ -147,12 +155,22 @@ function ServerResponse(req) {
|
|||||||
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
|
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
|
||||||
this.shouldKeepAlive = false;
|
this.shouldKeepAlive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const httpObserverCount = observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_HTTP];
|
||||||
|
if (httpObserverCount > 0) {
|
||||||
|
this[kServerResponseStatistics] = {
|
||||||
|
startTime: process.hrtime()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Object.setPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype);
|
Object.setPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype);
|
||||||
Object.setPrototypeOf(ServerResponse, OutgoingMessage);
|
Object.setPrototypeOf(ServerResponse, OutgoingMessage);
|
||||||
|
|
||||||
ServerResponse.prototype._finish = function _finish() {
|
ServerResponse.prototype._finish = function _finish() {
|
||||||
DTRACE_HTTP_SERVER_RESPONSE(this.connection);
|
DTRACE_HTTP_SERVER_RESPONSE(this.connection);
|
||||||
|
if (this[kServerResponseStatistics] !== undefined) {
|
||||||
|
emitStatistics(this[kServerResponseStatistics]);
|
||||||
|
}
|
||||||
OutgoingMessage.prototype._finish.call(this);
|
OutgoingMessage.prototype._finish.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { setUnrefTimeout } = require('internal/timers');
|
const { setUnrefTimeout } = require('internal/timers');
|
||||||
|
const { PerformanceEntry, notify } = internalBinding('performance');
|
||||||
|
|
||||||
var nowCache;
|
var nowCache;
|
||||||
var utcCache;
|
var utcCache;
|
||||||
@ -31,9 +32,26 @@ function ondrain() {
|
|||||||
if (this._httpMessage) this._httpMessage.emit('drain');
|
if (this._httpMessage) this._httpMessage.emit('drain');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HttpRequestTiming extends PerformanceEntry {
|
||||||
|
constructor(statistics) {
|
||||||
|
super();
|
||||||
|
this.name = 'HttpRequest';
|
||||||
|
this.entryType = 'http';
|
||||||
|
const startTime = statistics.startTime;
|
||||||
|
const diff = process.hrtime(startTime);
|
||||||
|
this.duration = diff[0] * 1000 + diff[1] / 1e6;
|
||||||
|
this.startTime = startTime[0] * 1000 + startTime[1] / 1e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitStatistics(statistics) {
|
||||||
|
notify('http', new HttpRequestTiming(statistics));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
outHeadersKey: Symbol('outHeadersKey'),
|
outHeadersKey: Symbol('outHeadersKey'),
|
||||||
ondrain,
|
ondrain,
|
||||||
nowDate,
|
nowDate,
|
||||||
utcDate
|
utcDate,
|
||||||
|
emitStatistics
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ const {
|
|||||||
NODE_PERFORMANCE_ENTRY_TYPE_GC,
|
NODE_PERFORMANCE_ENTRY_TYPE_GC,
|
||||||
NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION,
|
NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION,
|
||||||
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
|
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
|
||||||
|
NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
|
||||||
|
|
||||||
NODE_PERFORMANCE_MILESTONE_NODE_START,
|
NODE_PERFORMANCE_MILESTONE_NODE_START,
|
||||||
NODE_PERFORMANCE_MILESTONE_V8_START,
|
NODE_PERFORMANCE_MILESTONE_V8_START,
|
||||||
@ -70,7 +71,8 @@ const observerableTypes = [
|
|||||||
'measure',
|
'measure',
|
||||||
'gc',
|
'gc',
|
||||||
'function',
|
'function',
|
||||||
'http2'
|
'http2',
|
||||||
|
'http'
|
||||||
];
|
];
|
||||||
|
|
||||||
const IDX_STREAM_STATS_ID = 0;
|
const IDX_STREAM_STATS_ID = 0;
|
||||||
@ -508,6 +510,7 @@ function mapTypes(i) {
|
|||||||
case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
|
case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
|
||||||
case 'function': return NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
|
case 'function': return NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
|
||||||
case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
|
case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
|
||||||
|
case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,6 +366,21 @@ void Timerify(const FunctionCallbackInfo<Value>& args) {
|
|||||||
args.GetReturnValue().Set(wrap);
|
args.GetReturnValue().Set(wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify a custom PerformanceEntry to observers
|
||||||
|
void Notify(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
Utf8Value type(env->isolate(), args[0]);
|
||||||
|
Local<Value> entry = args[1];
|
||||||
|
PerformanceEntryType entry_type = ToPerformanceEntryTypeEnum(*type);
|
||||||
|
AliasedUint32Array& observers = env->performance_state()->observers;
|
||||||
|
if (entry_type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID &&
|
||||||
|
observers[entry_type]) {
|
||||||
|
USE(env->performance_entry_callback()->
|
||||||
|
Call(env->context(), Undefined(env->isolate()), 1, &entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Event Loop Timing Histogram
|
// Event Loop Timing Histogram
|
||||||
namespace {
|
namespace {
|
||||||
static void ELDHistogramMin(const FunctionCallbackInfo<Value>& args) {
|
static void ELDHistogramMin(const FunctionCallbackInfo<Value>& args) {
|
||||||
@ -562,6 +577,7 @@ void Initialize(Local<Object> target,
|
|||||||
env->SetMethod(target, "timerify", Timerify);
|
env->SetMethod(target, "timerify", Timerify);
|
||||||
env->SetMethod(
|
env->SetMethod(
|
||||||
target, "setupGarbageCollectionTracking", SetupGarbageCollectionTracking);
|
target, "setupGarbageCollectionTracking", SetupGarbageCollectionTracking);
|
||||||
|
env->SetMethod(target, "notify", Notify);
|
||||||
|
|
||||||
Local<Object> constants = Object::New(isolate);
|
Local<Object> constants = Object::New(isolate);
|
||||||
|
|
||||||
|
@ -35,7 +35,8 @@ extern uint64_t performance_v8_start;
|
|||||||
V(MEASURE, "measure") \
|
V(MEASURE, "measure") \
|
||||||
V(GC, "gc") \
|
V(GC, "gc") \
|
||||||
V(FUNCTION, "function") \
|
V(FUNCTION, "function") \
|
||||||
V(HTTP2, "http2")
|
V(HTTP2, "http2") \
|
||||||
|
V(HTTP, "http")
|
||||||
|
|
||||||
enum PerformanceMilestone {
|
enum PerformanceMilestone {
|
||||||
#define V(name, _) NODE_PERFORMANCE_MILESTONE_##name,
|
#define V(name, _) NODE_PERFORMANCE_MILESTONE_##name,
|
||||||
|
58
test/parallel/test-http-perf_hooks.js
Normal file
58
test/parallel/test-http-perf_hooks.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const { PerformanceObserver } = require('perf_hooks');
|
||||||
|
|
||||||
|
const obs = new PerformanceObserver(common.mustCall((items) => {
|
||||||
|
const entry = items.getEntries()[0];
|
||||||
|
assert.strictEqual(entry.entryType, 'http');
|
||||||
|
assert.strictEqual(typeof entry.startTime, 'number');
|
||||||
|
assert.strictEqual(typeof entry.duration, 'number');
|
||||||
|
}, 2));
|
||||||
|
|
||||||
|
obs.observe({ entryTypes: ['http'] });
|
||||||
|
|
||||||
|
const expected = 'Post Body For Test';
|
||||||
|
const makeRequest = (options) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.request(options, common.mustCall((res) => {
|
||||||
|
resolve();
|
||||||
|
})).on('error', reject).end(options.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.Server(common.mustCall((req, res) => {
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
req.setEncoding('utf8');
|
||||||
|
req.on('data', function(chunk) {
|
||||||
|
result += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(function() {
|
||||||
|
assert.strictEqual(result, expected);
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end('hello world\n');
|
||||||
|
}));
|
||||||
|
}, 2));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
makeRequest({
|
||||||
|
port: server.address().port,
|
||||||
|
path: '/',
|
||||||
|
method: 'POST',
|
||||||
|
data: expected
|
||||||
|
}),
|
||||||
|
makeRequest({
|
||||||
|
port: server.address().port,
|
||||||
|
path: '/',
|
||||||
|
method: 'POST',
|
||||||
|
data: expected
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
server.close();
|
||||||
|
}));
|
Loading…
x
Reference in New Issue
Block a user