Skip to content

Commit

Permalink
Use inspector for cpu profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Oct 21, 2022
1 parent 5ae0e9b commit f919d24
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 22 deletions.
18 changes: 11 additions & 7 deletions ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ export const heap = {

// If loaded with --require, start profiling.
if (module.parent && module.parent.id === 'internal/preload') {
const stop = time.start();
process.on('exit', () => {
// The process is going to terminate imminently. All work here needs to
// be synchronous.
const profile = stop();
const buffer = encodeSync(profile);
writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer);
time.start().then(stop => {
process.on('exit', async () => {
// The process is going to terminate imminently. All work here needs to
// be synchronous.

// TODO: this code no longer works because stop() cannot be run synchronously. Maybe
// beforeExit event would be a decent middleground, or can run this in a signal handler.
const profile = await stop();
const buffer = encodeSync(profile);
writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer);
});
});
}
113 changes: 113 additions & 0 deletions ts/src/time-profiler-inspector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {TimeProfile, TimeProfileNode} from './v8-types';
import * as inspector from 'node:inspector';

const session = new inspector.Session();
session.connect();

// Wrappers around inspector functions
export function startProfiling(): Promise<void> {
return new Promise<void>((resolve, reject) => {
session.post('Profiler.enable', err => {
if (err !== null) {
reject(err);
}
session.post('Profiler.start', err => {
if (err !== null) {
reject(err);
}
resolve();
});
});
});
}

export function stopProfiling(): Promise<TimeProfile> {
// return profiler.timeProfiler.stopProfiling(runName, includeLineInfo || false);
return new Promise<TimeProfile>((resolve, reject) => {
session.post('Profiler.stop', (err, {profile}) => {
if (err !== null) {
reject(err);
}
resolve(translateToTimeProfile(profile));
});
});
}

function translateToTimeProfile(
profile: inspector.Profiler.Profile
): TimeProfile {
const root: inspector.Profiler.ProfileNode | undefined = profile.nodes[0];
// Not sure if this could ever happen...
if (root === undefined) {
return {
endTime: profile.endTime,
startTime: profile.startTime,
topDownRoot: {
children: [],
hitCount: 0,
scriptName: '',
},
};
}

const nodesById: {[key: number]: inspector.Profiler.ProfileNode} = {};
profile.nodes.forEach(node => (nodesById[node.id] = node));

function translateNode({
hitCount,
children,
callFrame: {columnNumber, functionName, lineNumber, scriptId, url},
}: inspector.Profiler.ProfileNode): TimeProfileNode {
const parsedScriptId = parseInt(scriptId);
return {
name: functionName,
scriptName: url,

// Add 1 because these are zero-based
columnNumber: columnNumber + 1,
lineNumber: lineNumber + 1,

hitCount: hitCount ?? 0,
scriptId: Number.isNaN(parsedScriptId) ? 0 : parsedScriptId,
children:
children?.map(childId => translateNode(nodesById[childId])) ?? [],
};
}

return {
endTime: profile.endTime,
startTime: profile.startTime,
topDownRoot: translateNode(root),
};
}

export function setSamplingInterval(intervalMicros: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
session.post(
'Profiler.setSamplingInterval',
{interval: intervalMicros},
err => {
if (err !== null) {
reject(err);
}
resolve();
}
);
});
}
26 changes: 11 additions & 15 deletions ts/src/time-profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
setSamplingInterval,
startProfiling,
stopProfiling,
} from './time-profiler-bindings';
} from './time-profiler-inspector';

let profiling = false;

Expand All @@ -49,40 +49,36 @@ export interface TimeProfilerOptions {
}

export async function profile(options: TimeProfilerOptions) {
const stop = start(
const stop = await start(
options.intervalMicros || DEFAULT_INTERVAL_MICROS,
options.name,
options.sourceMapper,
options.lineNumbers
options.sourceMapper
);
await delay(options.durationMillis);
return stop();
return await stop();
}

export function start(
export async function start(
intervalMicros: Microseconds = DEFAULT_INTERVAL_MICROS,
name?: string,
sourceMapper?: SourceMapper,
lineNumbers?: boolean
sourceMapper?: SourceMapper
) {
if (profiling) {
throw new Error('already profiling');
}

profiling = true;
const runName = name || `pprof-${Date.now()}-${Math.random()}`;
setSamplingInterval(intervalMicros);
await setSamplingInterval(intervalMicros);
// Node.js contains an undocumented API for reporting idle status to V8.
// This lets the profiler distinguish idle time from time spent in native
// code. Ideally this should be default behavior. Until then, use the
// undocumented API.
// See https://github.com/nodejs/node/issues/19009#issuecomment-403161559.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process as any)._startProfilerIdleNotifier();
startProfiling(runName, lineNumbers);
return function stop() {
await startProfiling();
return async function stop() {
profiling = false;
const result = stopProfiling(runName, lineNumbers);
const result = await stopProfiling();
console.log(JSON.stringify(result));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process as any)._stopProfilerIdleNotifier();
const profile = serializeTimeProfile(result, intervalMicros, sourceMapper);
Expand Down

0 comments on commit f919d24

Please sign in to comment.