UNPKG

@stacksleuth/core

Version:

Advanced TypeScript-based core profiling engine for StackSleuth - Real-time performance monitoring with flexible profiler, span tracing, and unified agent architecture. Features comprehensive metrics collection, memory optimization, and production-ready i

276 lines 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlamegraphGenerator = void 0; /** * Utility class for generating flamegraph data from traces */ class FlamegraphGenerator { /** * Convert a trace to flamegraph data */ static generateFromTrace(trace) { if (!trace.spans.length) { return { root: { id: trace.id, name: trace.name, value: trace.timing.duration || 0, children: [], depth: 0, start: 0, end: trace.timing.duration || 0, percentage: 100 }, totalDuration: trace.timing.duration || 0, maxDepth: 0, nodeCount: 1 }; } // Build span hierarchy const spanMap = new Map(); const children = new Map(); // Index all spans trace.spans.forEach(span => { spanMap.set(span.id, span); if (!children.has(span.parentId || trace.rootSpanId)) { children.set(span.parentId || trace.rootSpanId, []); } children.get(span.parentId || trace.rootSpanId).push(span); }); // Find root span or create virtual root const rootSpan = trace.spans.find(s => s.id === trace.rootSpanId) || trace.spans[0]; const totalDuration = trace.timing.duration || 0; // Build flamegraph tree const root = this.buildNode(rootSpan, children, spanMap, 0, totalDuration); const stats = this.calculateStats(root); return { root, totalDuration, maxDepth: stats.maxDepth, nodeCount: stats.nodeCount }; } /** * Generate flamegraph from multiple traces for comparison */ static generateComparison(traces) { return traces.map(trace => this.generateFromTrace(trace)); } /** * Merge multiple flamegraphs for aggregated view */ static mergeFlamegrahs(flamegraphs) { if (flamegraphs.length === 0) { throw new Error('Cannot merge empty flamegraph array'); } if (flamegraphs.length === 1) { return flamegraphs[0]; } // Create merged root node const totalDuration = flamegraphs.reduce((sum, fg) => sum + fg.totalDuration, 0); const avgDuration = totalDuration / flamegraphs.length; const mergedRoot = { id: 'merged-root', name: 'Merged Traces', value: avgDuration, children: [], depth: 0, start: 0, end: avgDuration, percentage: 100, metadata: { traceCount: flamegraphs.length, totalDuration, avgDuration } }; // Merge children by name const childrenMap = new Map(); flamegraphs.forEach(fg => { fg.root.children.forEach(child => { if (!childrenMap.has(child.name)) { childrenMap.set(child.name, []); } childrenMap.get(child.name).push(child); }); }); // Create merged children mergedRoot.children = Array.from(childrenMap.entries()).map(([name, nodes]) => { const avgValue = nodes.reduce((sum, node) => sum + node.value, 0) / nodes.length; const merged = { id: `merged-${name}`, name, value: avgValue, children: [], depth: 1, start: 0, end: avgValue, percentage: (avgValue / avgDuration) * 100, metadata: { nodeCount: nodes.length, minValue: Math.min(...nodes.map(n => n.value)), maxValue: Math.max(...nodes.map(n => n.value)), avgValue } }; // Recursively merge children merged.children = this.mergeNodeChildren(nodes, merged.depth + 1, avgDuration); return merged; }); const stats = this.calculateStats(mergedRoot); return { root: mergedRoot, totalDuration: avgDuration, maxDepth: stats.maxDepth, nodeCount: stats.nodeCount }; } /** * Build flamegraph node from span */ static buildNode(span, children, spanMap, depth, totalDuration) { const duration = span.timing.duration || 0; const start = span.timing.start.nanos; const end = span.timing.end?.nanos || start; const node = { id: span.id, name: span.name, value: duration, children: [], depth, start: (start - (spanMap.get(span.traceId)?.timing.start.nanos || start)) / 1000000, // Convert to ms end: (end - (spanMap.get(span.traceId)?.timing.start.nanos || start)) / 1000000, percentage: totalDuration > 0 ? (duration / totalDuration) * 100 : 0, color: this.getColorForSpanType(span.type), metadata: { type: span.type, status: span.status, ...span.metadata } }; // Add children const spanChildren = children.get(span.id) || []; node.children = spanChildren .sort((a, b) => a.timing.start.nanos - b.timing.start.nanos) .map(child => this.buildNode(child, children, spanMap, depth + 1, totalDuration)); return node; } /** * Merge children from multiple nodes */ static mergeNodeChildren(nodes, depth, totalDuration) { const childrenMap = new Map(); nodes.forEach(node => { node.children.forEach(child => { if (!childrenMap.has(child.name)) { childrenMap.set(child.name, []); } childrenMap.get(child.name).push(child); }); }); return Array.from(childrenMap.entries()).map(([name, children]) => { const avgValue = children.reduce((sum, child) => sum + child.value, 0) / children.length; const merged = { id: `merged-${depth}-${name}`, name, value: avgValue, children: this.mergeNodeChildren(children, depth + 1, totalDuration), depth, start: 0, end: avgValue, percentage: (avgValue / totalDuration) * 100, metadata: { nodeCount: children.length, minValue: Math.min(...children.map(n => n.value)), maxValue: Math.max(...children.map(n => n.value)), avgValue } }; return merged; }); } /** * Calculate statistics for a flamegraph */ static calculateStats(root) { let maxDepth = 0; let nodeCount = 0; const traverse = (node, depth) => { maxDepth = Math.max(maxDepth, depth); nodeCount++; node.children.forEach(child => traverse(child, depth + 1)); }; traverse(root, 0); return { maxDepth, nodeCount }; } /** * Get color for span type */ static getColorForSpanType(spanType) { const colors = { 'http_request': '#3B82F6', // Blue 'db_query': '#10B981', // Green 'react_render': '#F59E0B', // Yellow 'function_call': '#8B5CF6', // Purple 'custom': '#6B7280' // Gray }; return colors[spanType] || colors.custom; } /** * Filter flamegraph by minimum duration */ static filterByDuration(flamegraph, minDuration) { const filterNode = (node) => { if (node.value < minDuration) { return null; } const filteredChildren = node.children .map(child => filterNode(child)) .filter((child) => child !== null); return { ...node, children: filteredChildren }; }; const filteredRoot = filterNode(flamegraph.root); if (!filteredRoot) { throw new Error('All nodes filtered out'); } const stats = this.calculateStats(filteredRoot); return { root: filteredRoot, totalDuration: flamegraph.totalDuration, maxDepth: stats.maxDepth, nodeCount: stats.nodeCount }; } /** * Convert flamegraph to SVG format */ static toSVG(flamegraph, width = 1200, height = 600) { const { root, maxDepth } = flamegraph; const rowHeight = height / (maxDepth + 1); let svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">`; svg += '<style>.node { stroke: #fff; stroke-width: 1; } .text { font-family: Arial; font-size: 12px; fill: #fff; }</style>'; const renderNode = (node, x, width) => { const nodeWidth = (node.percentage / 100) * width; const y = node.depth * rowHeight; svg += `<rect class="node" x="${x}" y="${y}" width="${nodeWidth}" height="${rowHeight}" fill="${node.color || '#6B7280'}">`; svg += `<title>${node.name} (${node.value.toFixed(2)}ms)</title>`; svg += '</rect>'; if (nodeWidth > 50) { // Only show text if there's enough space svg += `<text class="text" x="${x + 5}" y="${y + rowHeight / 2 + 4}">${node.name}</text>`; } let childX = x; node.children.forEach(child => { renderNode(child, childX, nodeWidth); childX += (child.percentage / 100) * nodeWidth; }); }; renderNode(root, 0, width); svg += '</svg>'; return svg; } } exports.FlamegraphGenerator = FlamegraphGenerator; //# sourceMappingURL=flamegraph.js.map