@ai-capabilities-suite/mcp-debugger-core
Version:
Core debugging engine for Node.js and TypeScript applications. Provides Inspector Protocol integration, breakpoint management, variable inspection, execution control, profiling, hang detection, and source map support.
246 lines • 9.02 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CPUProfiler = void 0;
/**
* CPU Profiler for capturing and analyzing CPU profiles
* Uses the Profiler domain of Chrome DevTools Protocol
*/
class CPUProfiler {
constructor(inspector) {
this.profiling = false;
this.currentProfile = null;
this.inspector = inspector;
}
/**
* Start CPU profiling
* Enables the Profiler domain and starts collecting CPU profile data
*/
async start() {
if (this.profiling) {
throw new Error('CPU profiling is already active');
}
// Enable the Profiler domain
await this.inspector.send('Profiler.enable');
// Start profiling with high precision
await this.inspector.send('Profiler.start');
this.profiling = true;
this.currentProfile = null;
}
/**
* Stop CPU profiling and return the profile data
* @returns The captured CPU profile
*/
async stop() {
if (!this.profiling) {
throw new Error('CPU profiling is not active');
}
// Stop profiling and get the profile
const result = await this.inspector.send('Profiler.stop');
this.profiling = false;
// Store the profile
this.currentProfile = result.profile;
// Disable the Profiler domain
await this.inspector.send('Profiler.disable');
return this.currentProfile;
}
/**
* Check if profiling is currently active
*/
isProfiling() {
return this.profiling;
}
/**
* Get the current profile (if available)
*/
getCurrentProfile() {
return this.currentProfile;
}
/**
* Generate a flame graph from the CPU profile
* @param profile The CPU profile to convert
* @returns Root node of the flame graph
*/
generateFlameGraph(profile) {
const nodeMap = new Map();
for (const node of profile.nodes) {
nodeMap.set(node.id, node);
}
// Build the flame graph tree
const root = {
name: '(root)',
value: 0,
children: [],
};
// Calculate total time for each node
const nodeTimes = new Map();
if (profile.samples && profile.timeDeltas) {
for (let i = 0; i < profile.samples.length; i++) {
const nodeId = profile.samples[i];
const timeDelta = profile.timeDeltas[i] || 0;
nodeTimes.set(nodeId, (nodeTimes.get(nodeId) || 0) + timeDelta);
}
}
// Build flame graph nodes
const buildFlameNode = (nodeId) => {
const node = nodeMap.get(nodeId);
if (!node)
return null;
const time = nodeTimes.get(nodeId) || node.hitCount || 0;
const flameNode = {
name: node.callFrame.functionName || '(anonymous)',
value: time,
file: node.callFrame.url,
line: node.callFrame.lineNumber,
children: [],
};
// Add children
if (node.children) {
for (const childId of node.children) {
const childNode = buildFlameNode(childId);
if (childNode && childNode.value > 0) {
flameNode.children.push(childNode);
}
}
}
return flameNode;
};
// Find root nodes (nodes with no parents)
const childIds = new Set();
for (const node of profile.nodes) {
if (node.children) {
for (const childId of node.children) {
childIds.add(childId);
}
}
}
for (const node of profile.nodes) {
if (!childIds.has(node.id)) {
const flameNode = buildFlameNode(node.id);
if (flameNode && flameNode.value > 0) {
root.children.push(flameNode);
root.value += flameNode.value;
}
}
}
return root;
}
/**
* Generate a call tree from the CPU profile
* Similar to flame graph but organized differently
* @param profile The CPU profile to convert
* @returns Root node of the call tree
*/
generateCallTree(profile) {
// For now, call tree is the same as flame graph
// In a more sophisticated implementation, these could differ
return this.generateFlameGraph(profile);
}
/**
* Analyze the CPU profile to identify bottlenecks
* @param profile The CPU profile to analyze
* @returns Analysis results with top functions and bottlenecks
*/
analyzeProfile(profile) {
const totalTime = profile.endTime - profile.startTime;
// Calculate time spent in each function
const functionTimes = new Map();
// Build node map for quick lookup
const nodeMap = new Map();
for (const node of profile.nodes) {
nodeMap.set(node.id, node);
}
// Calculate self time from samples
if (profile.samples && profile.timeDeltas) {
for (let i = 0; i < profile.samples.length; i++) {
const nodeId = profile.samples[i];
const node = nodeMap.get(nodeId);
if (!node)
continue;
const timeDelta = profile.timeDeltas[i] || 0;
const key = `${node.callFrame.functionName}:${node.callFrame.url}:${node.callFrame.lineNumber}`;
const existing = functionTimes.get(key);
if (existing) {
existing.selfTime += timeDelta;
existing.totalTime += timeDelta;
}
else {
functionTimes.set(key, {
selfTime: timeDelta,
totalTime: timeDelta,
node,
});
}
}
}
else {
// Fallback to hitCount if samples not available
for (const node of profile.nodes) {
const key = `${node.callFrame.functionName}:${node.callFrame.url}:${node.callFrame.lineNumber}`;
const existing = functionTimes.get(key);
if (existing) {
existing.selfTime += node.hitCount;
existing.totalTime += node.hitCount;
}
else {
functionTimes.set(key, {
selfTime: node.hitCount,
totalTime: node.hitCount,
node,
});
}
}
}
// Sort by self time to find top functions
const sortedFunctions = Array.from(functionTimes.entries())
.sort((a, b) => b[1].selfTime - a[1].selfTime)
.slice(0, 20); // Top 20 functions
const topFunctions = sortedFunctions.map(([key, data]) => ({
functionName: data.node.callFrame.functionName || '(anonymous)',
file: data.node.callFrame.url,
line: data.node.callFrame.lineNumber,
selfTime: data.selfTime,
totalTime: data.totalTime,
percentage: totalTime > 0 ? (data.selfTime / totalTime) * 100 : 0,
}));
// Identify bottlenecks (functions taking >5% of total time)
const bottlenecks = topFunctions
.filter((fn) => fn.percentage > 5)
.map((fn) => ({
functionName: fn.functionName,
file: fn.file,
line: fn.line,
reason: `Takes ${fn.percentage.toFixed(2)}% of total execution time`,
impact: fn.percentage,
}));
return {
totalTime,
topFunctions,
bottlenecks,
};
}
/**
* Format profile analysis as a human-readable string
* @param analysis The profile analysis to format
* @returns Formatted string
*/
formatAnalysis(analysis) {
const lines = [];
lines.push(`Total Time: ${analysis.totalTime.toFixed(2)}μs`);
lines.push('');
if (analysis.bottlenecks.length > 0) {
lines.push('Bottlenecks:');
for (const bottleneck of analysis.bottlenecks) {
lines.push(` - ${bottleneck.functionName} (${bottleneck.file}:${bottleneck.line})`);
lines.push(` ${bottleneck.reason}`);
}
lines.push('');
}
lines.push('Top Functions by Self Time:');
for (const fn of analysis.topFunctions.slice(0, 10)) {
lines.push(` ${fn.percentage.toFixed(2)}% - ${fn.functionName} (${fn.file}:${fn.line})`);
}
return lines.join('\n');
}
}
exports.CPUProfiler = CPUProfiler;
//# sourceMappingURL=cpu-profiler.js.map