Skip to content

Commit a0ee1a7

Browse files
committed
feat!: use inspector for time profiles
1 parent 5ae0e9b commit a0ee1a7

File tree

3 files changed

+135
-22
lines changed

3 files changed

+135
-22
lines changed

ts/src/index.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ export const heap = {
3737

3838
// If loaded with --require, start profiling.
3939
if (module.parent && module.parent.id === 'internal/preload') {
40-
const stop = time.start();
41-
process.on('exit', () => {
42-
// The process is going to terminate imminently. All work here needs to
43-
// be synchronous.
44-
const profile = stop();
45-
const buffer = encodeSync(profile);
46-
writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer);
40+
time.start().then(stop => {
41+
process.on('exit', async () => {
42+
// The process is going to terminate imminently. All work here needs to
43+
// be synchronous.
44+
45+
// TODO: this code no longer works because stop() cannot be run synchronously. Maybe
46+
// beforeExit event would be a decent middleground, or can run this in a signal handler.
47+
const profile = await stop();
48+
const buffer = encodeSync(profile);
49+
writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer);
50+
});
4751
});
4852
}

ts/src/time-profiler-inspector.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2018 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {TimeProfile, TimeProfileNode} from './v8-types';
18+
import * as inspector from 'node:inspector';
19+
20+
const session = new inspector.Session();
21+
session.connect();
22+
23+
// Wrappers around inspector functions
24+
export function startProfiling(): Promise<void> {
25+
return new Promise<void>((resolve, reject) => {
26+
session.post('Profiler.enable', err => {
27+
if (err !== null) {
28+
reject(err);
29+
}
30+
session.post('Profiler.start', err => {
31+
if (err !== null) {
32+
reject(err);
33+
}
34+
resolve();
35+
});
36+
});
37+
});
38+
}
39+
40+
export function stopProfiling(): Promise<TimeProfile> {
41+
// return profiler.timeProfiler.stopProfiling(runName, includeLineInfo || false);
42+
return new Promise<TimeProfile>((resolve, reject) => {
43+
session.post('Profiler.stop', (err, {profile}) => {
44+
if (err !== null) {
45+
reject(err);
46+
}
47+
resolve(translateToTimeProfile(profile));
48+
});
49+
});
50+
}
51+
52+
function translateToTimeProfile(
53+
profile: inspector.Profiler.Profile
54+
): TimeProfile {
55+
const root: inspector.Profiler.ProfileNode | undefined = profile.nodes[0];
56+
// Not sure if this could ever happen...
57+
if (root === undefined) {
58+
return {
59+
endTime: profile.endTime,
60+
startTime: profile.startTime,
61+
topDownRoot: {
62+
children: [],
63+
hitCount: 0,
64+
scriptName: '',
65+
},
66+
};
67+
}
68+
69+
const nodesById: {[key: number]: inspector.Profiler.ProfileNode} = {};
70+
profile.nodes.forEach(node => (nodesById[node.id] = node));
71+
72+
function translateNode({
73+
hitCount,
74+
children,
75+
callFrame: {columnNumber, functionName, lineNumber, scriptId, url},
76+
}: inspector.Profiler.ProfileNode): TimeProfileNode {
77+
const parsedScriptId = parseInt(scriptId);
78+
return {
79+
name: functionName,
80+
scriptName: url,
81+
82+
// Add 1 because these are zero-based
83+
columnNumber: columnNumber + 1,
84+
lineNumber: lineNumber + 1,
85+
86+
hitCount: hitCount ?? 0,
87+
scriptId: Number.isNaN(parsedScriptId) ? 0 : parsedScriptId,
88+
children:
89+
children?.map(childId => translateNode(nodesById[childId])) ?? [],
90+
};
91+
}
92+
93+
return {
94+
endTime: profile.endTime,
95+
startTime: profile.startTime,
96+
topDownRoot: translateNode(root),
97+
};
98+
}
99+
100+
export function setSamplingInterval(intervalMicros: number): Promise<void> {
101+
return new Promise<void>((resolve, reject) => {
102+
session.post(
103+
'Profiler.setSamplingInterval',
104+
{interval: intervalMicros},
105+
err => {
106+
if (err !== null) {
107+
reject(err);
108+
}
109+
resolve();
110+
}
111+
);
112+
});
113+
}

ts/src/time-profiler.ts

+11-15
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
setSamplingInterval,
2323
startProfiling,
2424
stopProfiling,
25-
} from './time-profiler-bindings';
25+
} from './time-profiler-inspector';
2626

2727
let profiling = false;
2828

@@ -49,40 +49,36 @@ export interface TimeProfilerOptions {
4949
}
5050

5151
export async function profile(options: TimeProfilerOptions) {
52-
const stop = start(
52+
const stop = await start(
5353
options.intervalMicros || DEFAULT_INTERVAL_MICROS,
54-
options.name,
55-
options.sourceMapper,
56-
options.lineNumbers
54+
options.sourceMapper
5755
);
5856
await delay(options.durationMillis);
59-
return stop();
57+
return await stop();
6058
}
6159

62-
export function start(
60+
export async function start(
6361
intervalMicros: Microseconds = DEFAULT_INTERVAL_MICROS,
64-
name?: string,
65-
sourceMapper?: SourceMapper,
66-
lineNumbers?: boolean
62+
sourceMapper?: SourceMapper
6763
) {
6864
if (profiling) {
6965
throw new Error('already profiling');
7066
}
7167

7268
profiling = true;
73-
const runName = name || `pprof-${Date.now()}-${Math.random()}`;
74-
setSamplingInterval(intervalMicros);
69+
await setSamplingInterval(intervalMicros);
7570
// Node.js contains an undocumented API for reporting idle status to V8.
7671
// This lets the profiler distinguish idle time from time spent in native
7772
// code. Ideally this should be default behavior. Until then, use the
7873
// undocumented API.
7974
// See https://github.com/nodejs/node/issues/19009#issuecomment-403161559.
8075
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8176
(process as any)._startProfilerIdleNotifier();
82-
startProfiling(runName, lineNumbers);
83-
return function stop() {
77+
await startProfiling();
78+
return async function stop() {
8479
profiling = false;
85-
const result = stopProfiling(runName, lineNumbers);
80+
const result = await stopProfiling();
81+
console.log(JSON.stringify(result));
8682
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8783
(process as any)._stopProfilerIdleNotifier();
8884
const profile = serializeTimeProfile(result, intervalMicros, sourceMapper);

0 commit comments

Comments
 (0)