Skip to content

Commit f84e2fa

Browse files
committed
inspector: reduce http inspection overhead
Encode initiator stacks directly into CBOR to avoid the intermediate protocol value tree. Retain asynchronous caller frames, cycle checks and the maximum nesting depth. Pass Buffer and Uint8Array chunks directly to the native request-body cache, which synchronously copies the bytes, and omit unused fields from the body-completion notification. Add regression coverage for reused request buffers, delayed and async initiators, and stack serialization. Signed-off-by: GrinZero <774933704@qq.com> Assisted-by: Codex
1 parent 71b18a4 commit f84e2fa

6 files changed

Lines changed: 300 additions & 82 deletions

‎lib/internal/inspector/network_http.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ function onClientRequestBodyChunkSent({ request, chunk, encoding }) {
195195

196196
const buffer = typeof chunk === 'string' ?
197197
Buffer.from(chunk, encoding ?? undefined) :
198-
Buffer.from(chunk);
198+
chunk;
199+
// The native request-body cache synchronously copies the bytes, so there is
200+
// no need to copy Buffer or Uint8Array chunks before passing them to it.
199201
Network.dataSent({
200202
requestId: request[kInspectorRequestId],
201203
timestamp: getMonotonicTime(),
@@ -215,9 +217,6 @@ function onClientRequestBodySent({ request }) {
215217

216218
Network.dataSent({
217219
requestId: request[kInspectorRequestId],
218-
timestamp: getMonotonicTime(),
219-
dataLength: 0,
220-
data: Buffer.alloc(0),
221220
finished: true,
222221
});
223222
}

‎src/inspector/network_agent.cc‎

Lines changed: 70 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "network_agent.h"
22
#include <string>
3+
#include "crdtp/cbor.h"
34
#include "debug_utils-inl.h"
45
#include "env-inl.h"
56
#include "inspector/inspector_object_utils.h"
@@ -37,93 +38,88 @@ static void ThrowEventError(v8::Isolate* isolate, const std::string& message) {
3738
v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked()));
3839
}
3940

40-
static std::unique_ptr<protocol::Value> V8ToProtocolValue(
41-
Isolate* isolate,
42-
Local<Context> context,
43-
Local<Value> value,
44-
LocalVector<Object>* ancestors) {
41+
// Encode directly into the format consumed by the imported V8 StackTrace
42+
// parser, avoiding an intermediate tree of protocol::Value allocations.
43+
static bool EncodeV8Value(Isolate* isolate,
44+
Local<Context> context,
45+
Local<Value> value,
46+
LocalVector<Object>* ancestors,
47+
std::vector<uint8_t>* bytes,
48+
bool* has_call_frames = nullptr) {
49+
namespace cbor = crdtp::cbor;
4550
if (value->IsNullOrUndefined()) {
46-
return protocol::Value::null();
51+
bytes->push_back(cbor::EncodeNull());
52+
return true;
4753
}
4854
if (value->IsBoolean()) {
49-
return protocol::FundamentalValue::create(value.As<Boolean>()->Value());
55+
bytes->push_back(value.As<Boolean>()->Value() ? cbor::EncodeTrue()
56+
: cbor::EncodeFalse());
57+
return true;
5058
}
5159
if (value->IsInt32()) {
52-
return protocol::FundamentalValue::create(value.As<Int32>()->Value());
60+
cbor::EncodeInt32(value.As<Int32>()->Value(), bytes);
61+
return true;
5362
}
5463
if (value->IsNumber()) {
55-
return protocol::FundamentalValue::create(value.As<Number>()->Value());
64+
cbor::EncodeDouble(value.As<Number>()->Value(), bytes);
65+
return true;
5666
}
5767
if (value->IsString()) {
58-
return protocol::StringValue::create(ToProtocolString(isolate, value));
59-
}
60-
61-
if (!value->IsObject()) {
62-
return nullptr;
68+
Utf8Value string(isolate, value);
69+
cbor::EncodeString8(
70+
crdtp::span<uint8_t>(reinterpret_cast<const uint8_t*>(*string),
71+
string.length()),
72+
bytes);
73+
return true;
6374
}
75+
if (!value->IsObject()) return false;
6476

6577
Local<Object> object = value.As<Object>();
66-
if (ancestors->size() >= kMaxProtocolValueDepth) {
67-
return nullptr;
68-
}
78+
if (ancestors->size() >= kMaxProtocolValueDepth) return false;
6979
for (const auto& ancestor : *ancestors) {
70-
if (ancestor == object) {
71-
return nullptr;
72-
}
80+
if (ancestor == object) return false;
7381
}
7482
ancestors->push_back(object);
7583
auto pop_ancestor = OnScopeLeave([ancestors]() { ancestors->pop_back(); });
76-
84+
cbor::EnvelopeEncoder envelope;
85+
envelope.EncodeStart(bytes);
7786
if (value->IsArray()) {
7887
Local<Array> array = value.As<Array>();
79-
std::unique_ptr<protocol::ListValue> list = protocol::ListValue::create();
80-
list->reserve(array->Length());
88+
bytes->push_back(cbor::EncodeIndefiniteLengthArrayStart());
8189
for (uint32_t i = 0; i < array->Length(); i++) {
8290
Local<Value> element;
83-
if (!array->Get(context, i).ToLocal(&element)) {
84-
return nullptr;
85-
}
86-
std::unique_ptr<protocol::Value> protocol_value =
87-
V8ToProtocolValue(isolate, context, element, ancestors);
88-
if (!protocol_value) {
89-
return nullptr;
91+
if (!array->Get(context, i).ToLocal(&element) ||
92+
!EncodeV8Value(isolate, context, element, ancestors, bytes)) {
93+
return false;
9094
}
91-
list->pushValue(std::move(protocol_value));
92-
}
93-
return list;
94-
}
95-
96-
Local<Array> property_names;
97-
if (!object->GetOwnPropertyNames(context).ToLocal(&property_names)) {
98-
return nullptr;
99-
}
100-
std::unique_ptr<protocol::DictionaryValue> dict =
101-
protocol::DictionaryValue::create();
102-
for (uint32_t i = 0; i < property_names->Length(); i++) {
103-
Local<Value> property_name;
104-
if (!property_names->Get(context, i).ToLocal(&property_name) ||
105-
!property_name->IsString()) {
106-
return nullptr;
10795
}
108-
Local<Value> property;
109-
if (!object->Get(context, property_name).ToLocal(&property)) {
110-
return nullptr;
96+
} else {
97+
Local<Array> property_names;
98+
if (!object->GetOwnPropertyNames(context).ToLocal(&property_names)) {
99+
return false;
111100
}
112-
std::unique_ptr<protocol::Value> protocol_value =
113-
V8ToProtocolValue(isolate, context, property, ancestors);
114-
if (!protocol_value) {
115-
return nullptr;
101+
bytes->push_back(cbor::EncodeIndefiniteLengthMapStart());
102+
for (uint32_t i = 0; i < property_names->Length(); i++) {
103+
Local<Value> property_name;
104+
if (!property_names->Get(context, i).ToLocal(&property_name) ||
105+
!property_name->IsString()) {
106+
return false;
107+
}
108+
if (has_call_frames != nullptr &&
109+
property_name.As<v8::String>()->StringEquals(
110+
FIXED_ONE_BYTE_STRING(isolate, "callFrames"))) {
111+
*has_call_frames = true;
112+
}
113+
Local<Value> property;
114+
if (!object->Get(context, property_name).ToLocal(&property) ||
115+
!EncodeV8Value(isolate, context, property_name, ancestors, bytes) ||
116+
!EncodeV8Value(isolate, context, property, ancestors, bytes)) {
117+
return false;
118+
}
116119
}
117-
dict->setValue(ToProtocolString(isolate, property_name),
118-
std::move(protocol_value));
119120
}
120-
return dict;
121-
}
122-
123-
static std::unique_ptr<protocol::Value> V8ToProtocolValue(
124-
Isolate* isolate, Local<Context> context, Local<Value> value) {
125-
LocalVector<Object> ancestors(isolate);
126-
return V8ToProtocolValue(isolate, context, value, &ancestors);
121+
bytes->push_back(cbor::EncodeStop());
122+
return envelope.EncodeStop(bytes);
127123
}
128124

129125
// Create a protocol::Network::Headers from the v8 object.
@@ -179,25 +175,21 @@ NetworkAgent::createInitiatorFromObject(v8::Local<v8::Context> context,
179175

180176
Local<Object> stack_obj;
181177
if (ObjectGetObject(context, initiator_obj, "stack").ToLocal(&stack_obj)) {
182-
std::unique_ptr<protocol::Value> stack_value =
183-
V8ToProtocolValue(isolate, context, stack_obj);
184-
if (!stack_value) {
178+
LocalVector<Object> ancestors(isolate);
179+
std::vector<uint8_t> bytes;
180+
bool has_call_frames = false;
181+
if (!EncodeV8Value(isolate,
182+
context,
183+
stack_obj,
184+
&ancestors,
185+
&bytes,
186+
&has_call_frames) ||
187+
!has_call_frames) {
185188
ThrowEventError(isolate, "Invalid initiator.stack in event");
186189
return {};
187190
}
188-
189-
protocol::DictionaryValue* stack_dict =
190-
protocol::DictionaryValue::cast(stack_value.get());
191-
if (!stack_dict || stack_dict->get("callFrames") == nullptr) {
192-
ThrowEventError(isolate, "Invalid initiator.stack in event");
193-
return {};
194-
}
195-
196-
protocol::ErrorSupport errors;
197-
std::unique_ptr<v8_inspector::protocol::Runtime::API::StackTrace> stack =
198-
protocol::ValueConversions<v8_inspector::protocol::Runtime::API::
199-
StackTrace>::fromValue(stack_value.get(),
200-
&errors);
191+
auto stack = v8_inspector::protocol::Runtime::API::StackTrace::fromBinary(
192+
bytes.data(), bytes.size());
201193
if (!stack) {
202194
ThrowEventError(isolate, "Invalid initiator.stack in event");
203195
return {};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Flags: --experimental-network-inspection
2+
'use strict';
3+
const common = require('../common');
4+
common.skipIfInspectorDisabled();
5+
6+
const assert = require('node:assert');
7+
const http = require('node:http');
8+
const { Session } = require('node:inspector/promises');
9+
10+
// The inspector cache must own its bytes even when the HTTP caller reuses
11+
// a Buffer or a view into a larger Uint8Array after writing it.
12+
async function main() {
13+
const session = new Session();
14+
session.connect();
15+
await session.post('Network.enable');
16+
17+
const server = http.createServer(common.mustCall((req, res) => {
18+
const chunks = [];
19+
req.on('data', (chunk) => chunks.push(chunk));
20+
req.on('end', common.mustCall(() => {
21+
assert.strictEqual(Buffer.concat(chunks).toString(), 'buffer-view-ü');
22+
res.end();
23+
}));
24+
}));
25+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
26+
27+
let requestId;
28+
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
29+
requestId = params.requestId;
30+
assert.strictEqual(params.request.hasPostData, true);
31+
}));
32+
const finished = new Promise((resolve) => {
33+
session.once('Network.loadingFinished', resolve);
34+
});
35+
const req = http.request({
36+
host: '127.0.0.1',
37+
port: server.address().port,
38+
method: 'POST',
39+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
40+
}, common.mustCall((res) => res.resume()));
41+
req.on('error', common.mustNotCall());
42+
43+
const buffer = Buffer.from('buffer-');
44+
await new Promise((resolve) => req.write(buffer, resolve));
45+
buffer.fill(0x78);
46+
47+
const backing = new Uint8Array(Buffer.from('!view-?'));
48+
const view = backing.subarray(1, backing.length - 1);
49+
await new Promise((resolve) => req.write(view, resolve));
50+
backing.fill(0x79);
51+
req.end('ü', 'utf8');
52+
53+
await finished;
54+
const { postData } = await session.post('Network.getRequestPostData', { requestId });
55+
assert.strictEqual(postData, 'buffer-view-ü');
56+
session.disconnect();
57+
await new Promise((resolve) => server.close(resolve));
58+
}
59+
60+
main().then(common.mustCall());
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Flags: --experimental-network-inspection
2+
'use strict';
3+
const common = require('../common');
4+
common.skipIfInspectorDisabled();
5+
const assert = require('node:assert');
6+
const http = require('node:http');
7+
const { Session } = require('node:inspector/promises');
8+
9+
async function main() {
10+
const session = new Session();
11+
session.connect();
12+
await session.post('Network.enable');
13+
const server = http.createServer(common.mustCall((req, res) => res.end()));
14+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
15+
const event = new Promise((resolve) => session.once('Network.requestWillBeSent', resolve));
16+
const finished = new Promise((resolve) => session.once('Network.loadingFinished', resolve));
17+
async function requestFromAsync() {
18+
await Promise.resolve();
19+
const req = http.request({ port: server.address().port }, (res) => res.resume());
20+
req.on('error', common.mustNotCall());
21+
req.end();
22+
}
23+
24+
async function parentAsync() {
25+
await requestFromAsync();
26+
}
27+
await parentAsync();
28+
const { params } = await event;
29+
const frames = params.initiator.stack.callFrames;
30+
assert.strictEqual(frames[0].functionName, 'requestFromAsync');
31+
assert.strictEqual(frames[0].url, __filename);
32+
assert.ok(frames.some((frame) => frame.functionName === 'parentAsync' && frame.url === __filename));
33+
assert.ok(frames.some((frame) => frame.functionName === 'main' && frame.url === __filename));
34+
await finished;
35+
session.disconnect();
36+
await new Promise((resolve) => server.close(resolve));
37+
}
38+
main().then(common.mustCall());
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Flags: --experimental-network-inspection
2+
'use strict';
3+
const common = require('../common');
4+
common.skipIfInspectorDisabled();
5+
6+
const assert = require('node:assert');
7+
const http = require('node:http');
8+
const { Session } = require('node:inspector/promises');
9+
10+
async function main() {
11+
const session = new Session();
12+
session.connect();
13+
await session.post('Network.enable');
14+
const server = http.createServer(common.mustCall((req, res) => res.end()));
15+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
16+
17+
// Capture the original caller even if the body is sent from a later callback,
18+
// and do not depend on user changes to Error stack formatting or limits.
19+
const oldLimit = Error.stackTraceLimit;
20+
const oldPrepare = Error.prepareStackTrace;
21+
Error.stackTraceLimit = 0;
22+
Error.prepareStackTrace = common.mustNotCall();
23+
let request;
24+
function createRequest() {
25+
// Keep this next line paired with the marker used below.
26+
request = http.request({
27+
host: '127.0.0.1',
28+
port: server.address().port,
29+
method: 'POST',
30+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
31+
}, (res) => res.resume());
32+
request.on('error', common.mustNotCall());
33+
}
34+
35+
function nested(depth) {
36+
if (depth === 0) createRequest();
37+
else nested(depth - 1);
38+
}
39+
40+
function originalCaller() {
41+
nested(220);
42+
}
43+
const source = require('node:fs').readFileSync(__filename, 'utf8').split('\n');
44+
const creationLine = source.findIndex((line) => line.trim().startsWith('request = http.request('));
45+
originalCaller();
46+
Error.stackTraceLimit = oldLimit;
47+
Error.prepareStackTrace = oldPrepare;
48+
49+
const event = new Promise((resolve) => session.once('Network.requestWillBeSent', resolve));
50+
const finished = new Promise((resolve) => session.once('Network.loadingFinished', resolve));
51+
setImmediate(function sendFromLaterCallback() {
52+
request.end('hello');
53+
});
54+
const { params } = await event;
55+
const frames = params.initiator.stack.callFrames;
56+
assert.strictEqual(frames[0].url, __filename);
57+
assert.strictEqual(frames[0].functionName, 'createRequest');
58+
assert.strictEqual(frames[0].lineNumber, creationLine);
59+
assert.ok(frames[0].columnNumber >= 0);
60+
assert.ok(frames.some((frame) => frame.functionName === 'originalCaller'));
61+
assert.ok(!frames.some((frame) => frame.functionName === 'sendFromLaterCallback'));
62+
await finished;
63+
const { postData } = await session.post('Network.getRequestPostData', { requestId: params.requestId });
64+
assert.strictEqual(postData, 'hello');
65+
session.disconnect();
66+
await new Promise((resolve) => server.close(resolve));
67+
}
68+
main().then(common.mustCall());

0 commit comments

Comments
 (0)