-
Notifications
You must be signed in to change notification settings - Fork 28
feat: use inspector for heap profiles #236
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
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,180 @@ | ||
import * as inspector from 'node:inspector'; | ||
import {AllocationProfileNode, Allocation} from './v8-types'; | ||
|
||
const session = new inspector.Session(); | ||
|
||
export interface SamplingHeapProfileSample { | ||
size: number; | ||
nodeId: number; | ||
ordinal: number; | ||
} | ||
|
||
export interface SamplingHeapProfileNode { | ||
callFrame: inspector.Runtime.CallFrame; | ||
selfSize: number; | ||
id: number; | ||
children: SamplingHeapProfileNode[]; | ||
} | ||
|
||
/** | ||
* Need to create this interface since the type definitions file for node inspector | ||
* at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/inspector.d.ts | ||
* has not been updated with the latest changes yet. | ||
* | ||
* The types defined through this interface are in sync with the documentation found at - | ||
* https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler/ | ||
*/ | ||
export interface CompatibleSamplingHeapProfile { | ||
head: SamplingHeapProfileNode; | ||
samples: SamplingHeapProfileSample[]; | ||
} | ||
|
||
export function startSamplingHeapProfiler( | ||
heapIntervalBytes: number, | ||
stackDepth: number | ||
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. Please remove "stack depth", since it's no longer in use. |
||
): Promise<void> { | ||
session.connect(); | ||
return new Promise<void>((resolve, reject) => { | ||
session.post( | ||
'HeapProfiler.startSampling', | ||
{heapIntervalBytes}, | ||
(err: Error | null): void => { | ||
if (err !== null) { | ||
console.error(`Error starting heap sampling: ${err}`); | ||
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. Nothing should be logged within this repo (cloud-profiler-nodejs will log errors occurring; logging anything directly here will either result in things being double logged, or prevent users from being able to control what is logged) |
||
reject(err); | ||
return; | ||
} | ||
console.log( | ||
`Started Heap Sampling with interval bytes ${heapIntervalBytes}` | ||
); | ||
resolve(); | ||
} | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Stops the sampling heap profile and discards the current profile. | ||
*/ | ||
export function stopSamplingHeapProfiler(): Promise<void> { | ||
return new Promise<void>((resolve, reject) => { | ||
session.post( | ||
'HeapProfiler.stopSampling', | ||
( | ||
err: Error | null, | ||
profile: inspector.HeapProfiler.StopSamplingReturnType | ||
) => { | ||
if (err !== null) { | ||
console.error(`Error stopping heap sampling ${err}`); | ||
reject(err); | ||
return; | ||
} | ||
console.log( | ||
`Stopped sampling heap, discarding current profile: ${profile}` | ||
); | ||
session.disconnect(); | ||
console.log('Disconnected from current profiling session'); | ||
resolve(); | ||
} | ||
); | ||
}); | ||
} | ||
|
||
export async function getAllocationProfile(): Promise<AllocationProfileNode> { | ||
return new Promise<AllocationProfileNode>((resolve, reject) => { | ||
session.post( | ||
'HeapProfiler.getSamplingProfile', | ||
( | ||
err: Error | null, | ||
result: inspector.HeapProfiler.GetSamplingProfileReturnType | ||
) => { | ||
if (err !== null) { | ||
console.error(`Error getting sampling profile ${err}`); | ||
reject(err); | ||
return; | ||
} | ||
const compatibleHeapProfile = | ||
result.profile as CompatibleSamplingHeapProfile; | ||
resolve( | ||
translateToAllocationProfileNode( | ||
compatibleHeapProfile.head, | ||
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. Nit since these are the only two fields in compatibleHeapProfile, just pass it directly here |
||
compatibleHeapProfile.samples | ||
) | ||
); | ||
} | ||
); | ||
}); | ||
} | ||
|
||
function translateToAllocationProfileNode( | ||
node: SamplingHeapProfileNode, | ||
samples: SamplingHeapProfileSample[] | ||
): AllocationProfileNode { | ||
const allocationProfileNode: AllocationProfileNode = { | ||
allocations: [], | ||
name: node.callFrame.functionName, | ||
scriptName: node.callFrame.url, | ||
scriptId: Number(node.callFrame.scriptId), | ||
lineNumber: node.callFrame.lineNumber, | ||
columnNumber: node.callFrame.columnNumber, | ||
children: [], | ||
}; | ||
|
||
const children: AllocationProfileNode[] = new Array<AllocationProfileNode>( | ||
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.
|
||
node.children.length | ||
); | ||
for (let i = 0; i < node.children.length; i++) { | ||
children.splice( | ||
i, | ||
1, | ||
translateToAllocationProfileNode(node.children[i], samples) | ||
); | ||
} | ||
allocationProfileNode.children = children; | ||
|
||
// find all samples belonging to this node Id | ||
const samplesForCurrentNodeId: SamplingHeapProfileSample[] = | ||
filterSamplesBasedOnNodeId(node.id, samples); | ||
const mappedAllocationsForNodeId: Allocation[] = | ||
createAllocationsFromSamplesForNode(samplesForCurrentNodeId); | ||
|
||
allocationProfileNode.allocations = mappedAllocationsForNodeId; | ||
return allocationProfileNode; | ||
} | ||
|
||
function filterSamplesBasedOnNodeId( | ||
nodeId: number, | ||
samples: SamplingHeapProfileSample[] | ||
): SamplingHeapProfileSample[] { | ||
const filtered = samples.filter((sample: SamplingHeapProfileSample) => { | ||
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. I'd create a map in the outer scope or closure of the recursive function to do this lookup in constant time as this is n^2 now. Check out my PR for cpu time if you don't what i mean |
||
return sample.nodeId === nodeId; | ||
}); | ||
return filtered; | ||
} | ||
|
||
function createAllocationsFromSamplesForNode( | ||
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. Couldnt this be done once in linear time for all nodes, since the samples already have node ID? Group by node ID then for each group, group by byte sizes being equal? |
||
samplesForNode: SamplingHeapProfileSample[] | ||
): Allocation[] { | ||
const sampleSizeToCountMap = new Map<number, number>(); | ||
samplesForNode.forEach((sample: SamplingHeapProfileSample) => { | ||
const currentCountForSize: number | undefined = sampleSizeToCountMap.get( | ||
sample.size | ||
); | ||
if (currentCountForSize !== undefined) { | ||
sampleSizeToCountMap.set(sample.size, currentCountForSize + 1); | ||
} else { | ||
sampleSizeToCountMap.set(sample.size, 1); | ||
} | ||
}); | ||
|
||
const mappedAllocations: Allocation[] = []; | ||
sampleSizeToCountMap.forEach((size: number, count: number) => { | ||
const mappedAllocation: Allocation = { | ||
sizeBytes: size, | ||
count: count, | ||
}; | ||
mappedAllocations.push(mappedAllocation); | ||
}); | ||
|
||
return mappedAllocations; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,26 +20,25 @@ import { | |
getAllocationProfile, | ||
startSamplingHeapProfiler, | ||
stopSamplingHeapProfiler, | ||
} from './heap-profiler-bindings'; | ||
} from './heap-profiler-inspector'; | ||
import {serializeHeapProfile} from './profile-serializer'; | ||
import {SourceMapper} from './sourcemapper/sourcemapper'; | ||
import {AllocationProfileNode} from './v8-types'; | ||
|
||
let enabled = false; | ||
let heapIntervalBytes = 0; | ||
let heapStackDepth = 0; | ||
|
||
/* | ||
* Collects a heap profile when heapProfiler is enabled. Otherwise throws | ||
* an error. | ||
* | ||
* Data is returned in V8 allocation profile format. | ||
*/ | ||
export function v8Profile(): AllocationProfileNode { | ||
export async function v8Profile(): Promise<AllocationProfileNode> { | ||
if (!enabled) { | ||
throw new Error('Heap profiler is not enabled.'); | ||
} | ||
return getAllocationProfile(); | ||
return await getAllocationProfile(); | ||
} | ||
|
||
/** | ||
|
@@ -49,12 +48,12 @@ export function v8Profile(): AllocationProfileNode { | |
* @param ignoreSamplePath | ||
* @param sourceMapper | ||
*/ | ||
export function profile( | ||
export async function profile( | ||
ignoreSamplePath?: string, | ||
sourceMapper?: SourceMapper | ||
): perftools.profiles.IProfile { | ||
): Promise<perftools.profiles.IProfile> { | ||
const startTimeNanos = Date.now() * 1000 * 1000; | ||
const result = v8Profile(); | ||
const result = await v8Profile(); | ||
// Add node for external memory usage. | ||
// Current type definitions do not have external. | ||
// TODO: remove any once type definition is updated to include external. | ||
|
@@ -84,17 +83,17 @@ export function profile( | |
* started with different parameters, this throws an error. | ||
* | ||
* @param intervalBytes - average number of bytes between samples. | ||
* @param stackDepth - maximum stack depth for samples collected. | ||
* @param stackDepth - maximum stack depth for samples collected. This is currently no-op. | ||
* Default stack depth of 128 will be used. Kept to avoid making 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. I'd favor making the breaking change here (over keeping behavior which no longer works). |
||
*/ | ||
export function start(intervalBytes: number, stackDepth: number) { | ||
if (enabled) { | ||
throw new Error( | ||
`Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${stackDepth}` | ||
`Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth 128` | ||
); | ||
} | ||
heapIntervalBytes = intervalBytes; | ||
heapStackDepth = stackDepth; | ||
startSamplingHeapProfiler(heapIntervalBytes, heapStackDepth); | ||
startSamplingHeapProfiler(heapIntervalBytes, stackDepth); | ||
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 function will return before the promise completes as it's fire and forget with this promise, might be why the test is failing |
||
enabled = true; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might open an issue in that repo and link it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, will do that.