middleware/utils/pprofConverter.js (98 lines of code) (raw):

const { exec } = require('child_process'); const util = require('util'); // Promisified version of exec const execAsync = util.promisify(exec); /** * Convert pprof profile to markdown format * @param {string} profilePath - Path to the .pprof file * @returns {Promise<string>} - Markdown representation of the profile */ async function convertPprofToMarkdown(profilePath) { try { // Run pprof command to get text output const cmd = `go tool pprof -text ${profilePath}`; const { stdout, stderr } = await execAsync(cmd); if (stderr && !stderr.includes('Main binary filename not available')) { console.error('pprof warning:', stderr); } // Parse pprof text output const data = parsePprofText(stdout); // Convert to markdown return toMarkdown(data); } catch (error) { console.error(`Error converting profile: ${error.message}`); throw new Error(`Failed to convert profile: ${error.message}`); } } /** * Parse pprof text output into structured format */ function parsePprofText(content) { // Extract header information const timeMatch = content.match(/Time: (.*)/); const timeStr = timeMatch ? timeMatch[1] : 'Unknown'; const samplesMatch = content.match(/Showing nodes accounting for (\d+)samples, ([\d.]+)% of (\d+)samples total/); let shownSamples = 0, shownPercent = 0, totalSamples = 0; if (samplesMatch) { shownSamples = parseInt(samplesMatch[1], 10); shownPercent = parseFloat(samplesMatch[2]); totalSamples = parseInt(samplesMatch[3], 10); } const droppedMatch = content.match(/Dropped (\d+) nodes \(cum <= (\d+)samples\)/); const droppedNodes = droppedMatch ? parseInt(droppedMatch[1], 10) : 0; // Parse profiling data const dataLines = content.split('\n'); let startIdx = 0; for (let i = 0; i < dataLines.length; i++) { if (dataLines[i].trim().startsWith('flat flat%')) { startIdx = i + 1; break; } } const profileEntries = []; const lineRegex = /(\d+)samples\s+([\d.]+)%\s+([\d.]+)%\s+(\d+)samples\s+([\d.]+)%\s+(.+)/; for (let i = startIdx; i < dataLines.length; i++) { const line = dataLines[i].trim(); if (!line) continue; const match = line.match(lineRegex); if (match) { const entry = { flat_samples: parseInt(match[1], 10), flat_percent: parseFloat(match[2]), sum_percent: parseFloat(match[3]), cum_samples: parseInt(match[4], 10), cum_percent: parseFloat(match[5]), function: match[6].trim() }; profileEntries.push(entry); } } // Build the result return { metadata: { time: timeStr, total_samples: totalSamples, shown_samples: shownSamples, shown_percent: shownPercent, dropped_nodes: droppedNodes }, profile_entries: profileEntries }; } /** * Convert data to Markdown */ function toMarkdown(data) { let md = [`# CPU Profile Analysis\n\n`]; md.push(`**Time:** ${data.metadata.time}\n\n`); md.push(`**Samples:** ${data.metadata.shown_samples} of ${data.metadata.total_samples} (${data.metadata.shown_percent}%)\n\n`); md.push(`**Dropped Nodes:** ${data.metadata.dropped_nodes}\n\n`); md.push(`## Profile Data\n\n`); md.push(`| Flat Samples | Flat % | Sum % | Cum Samples | Cum % | Function |\n`); md.push(`|-------------|--------|-------|-------------|-------|----------|\n`); for (const entry of data.profile_entries) { md.push(`| ${entry.flat_samples} | ${entry.flat_percent}% | ${entry.sum_percent}% | ` + `${entry.cum_samples} | ${entry.cum_percent}% | \`${entry.function}\` |\n`); } // Add summary section with insights md.push(`\n## Performance Insights\n\n`); // Top functions by flat and cumulative time const flatTop = [...data.profile_entries] .sort((a, b) => b.flat_samples - a.flat_samples) .slice(0, 5); const cumTop = [...data.profile_entries] .sort((a, b) => b.cum_samples - a.cum_samples) .slice(0, 5); md.push(`### Top Functions by Self Time\n\n`); flatTop.forEach((entry, i) => { md.push(`${i+1}. \`${entry.function}\` - ${entry.flat_percent}% of total time\n`); }); md.push(`\n### Top Functions by Cumulative Time\n\n`); cumTop.forEach((entry, i) => { md.push(`${i+1}. \`${entry.function}\` - ${entry.cum_percent}% of total time\n`); }); return md.join(''); } module.exports = { convertPprofToMarkdown };