-
Notifications
You must be signed in to change notification settings - Fork 28
feat!: use inspector for time profiles #227
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
base: main
Are you sure you want to change the base?
Changes from all commits
b9bab1c
f777979
5663cff
6fd2c76
f411224
e5dd21d
7211b6d
b5483e3
c844b8d
b510c6e
3650dbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** | ||
* Copyright 2022 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); | ||
aabmass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
session.post('Profiler.start', err => { | ||
aabmass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (err !== null) { | ||
reject(err); | ||
aabmass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
export function stopProfiling(): Promise<TimeProfile> { | ||
return new Promise<TimeProfile>((resolve, reject) => { | ||
session.post('Profiler.stop', (err, {profile}) => { | ||
if (err !== null) { | ||
reject(err); | ||
aabmass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
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); | ||
const scriptName = url.startsWith('file:/') ? url.slice(6) : url; | ||
return { | ||
name: functionName, | ||
scriptName, | ||
|
||
// 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); | ||
aabmass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
resolve(); | ||
} | ||
); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,14 +15,15 @@ | |
*/ | ||
|
||
import delay from 'delay'; | ||
import type {perftools} from '../../proto/profile'; | ||
|
||
import {serializeTimeProfile} from './profile-serializer'; | ||
import {SourceMapper} from './sourcemapper/sourcemapper'; | ||
import { | ||
setSamplingInterval, | ||
startProfiling, | ||
stopProfiling, | ||
} from './time-profiler-bindings'; | ||
} from './time-profiler-inspector'; | ||
|
||
let profiling = false; | ||
|
||
|
@@ -37,52 +38,40 @@ export interface TimeProfilerOptions { | |
/** average time in microseconds between samples */ | ||
intervalMicros?: Microseconds; | ||
sourceMapper?: SourceMapper; | ||
name?: string; | ||
|
||
/** | ||
* This configuration option is experimental. | ||
* When set to true, functions will be aggregated at the line level, rather | ||
* than at the function level. | ||
* This defaults to false. | ||
*/ | ||
lineNumbers?: boolean; | ||
} | ||
|
||
export async function profile(options: TimeProfilerOptions) { | ||
const stop = start( | ||
export async function profile( | ||
options: TimeProfilerOptions | ||
): Promise<perftools.profiles.IProfile> { | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This now has to be async which is another breaking change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a chance (depending on underlying implementation) this will result in a fix for googleapis/cloud-profiler-nodejs#683 (event loop blocked at start of profile collection). This would be a good change if it does that (though needing to update agent code will be a bummer) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great! I will assume this fixes that issue and close it once this change gets pull into that repo. |
||
intervalMicros: Microseconds = DEFAULT_INTERVAL_MICROS, | ||
name?: string, | ||
sourceMapper?: SourceMapper, | ||
lineNumbers?: boolean | ||
) { | ||
sourceMapper?: SourceMapper | ||
): Promise<() => Promise<perftools.profiles.IProfile>> { | ||
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(); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(process as any)._stopProfilerIdleNotifier(); | ||
const profile = serializeTimeProfile(result, intervalMicros, sourceMapper); | ||
|
Uh oh!
There was an error while loading. Please reload this page.