UNPKG

next

Version:

The React Framework

246 lines (245 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "startTurboTraceServerCli", { enumerable: true, get: function() { return startTurboTraceServerCli; } }); const _nodehttp = /*#__PURE__*/ _interop_require_default(require("node:http")); const _zod = /*#__PURE__*/ _interop_require_default(require("next/dist/compiled/zod")); const _swc = require("../../build/swc"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { McpServer } = require('next/dist/compiled/@modelcontextprotocol/sdk/server/mcp'); const { StreamableHTTPServerTransport } = require('next/dist/compiled/@modelcontextprotocol/sdk/server/streamableHttp'); const DEFAULT_WS_PORT = 5747; /** 100 internal ticks = 1 µs */ const TICKS_PER_US = 100; const TICKS_PER_MS = TICKS_PER_US * 1000; const TICKS_PER_S = TICKS_PER_MS * 1000; function formatDuration(ticks) { if (ticks < TICKS_PER_MS) { const us = ticks / TICKS_PER_US; return `${us.toFixed(0)}µs`; } if (ticks < TICKS_PER_S) { const ms = ticks / TICKS_PER_MS; return `${ms.toFixed(2)}ms`; } const s = ticks / TICKS_PER_S; return `${s.toFixed(3)}s`; } function formatRelative(ticks) { // ticks may be negative if the child starts before the parent reference point const prefix = ticks < 0 ? '-' : ''; return prefix + formatDuration(Math.abs(ticks)); } function formatBytes(bytes) { const KB = 1024; const MB = KB * 1024; const GB = MB * 1024; if (bytes >= GB) return `${(bytes / GB).toFixed(2)} GB`; if (bytes >= MB) return `${(bytes / MB).toFixed(2)} MB`; if (bytes >= KB) return `${(bytes / KB).toFixed(2)} KB`; return `${bytes} B`; } function summarizeMemorySamples(samples) { if (!samples || samples.length === 0) return null; const bytes = samples.map((s)=>s[1]); const pressures = samples.map((s)=>s[2] ?? 0); const min = Math.min(...bytes); const max = Math.max(...bytes); const first = bytes[0]; const last = bytes[bytes.length - 1]; const delta = last - first; const deltaSign = delta >= 0 ? '+' : '-'; const maxPressure = Math.max(...pressures); return `samples=${samples.length}, min=${formatBytes(min)}, max=${formatBytes(max)}, ` + `start=${formatBytes(first)}, end=${formatBytes(last)}, ` + `Δ=${deltaSign}${formatBytes(Math.abs(delta))}, maxPressure=${maxPressure}`; } /** * Render a single span (or aggregated span group) as a markdown section. */ function renderSpanMarkdown(span) { let md = `### \`${span.name}\` (ID: \`${span.id}\`)\n`; if (span.isAggregated && span.count !== undefined && span.count > 1) { md += `- **Count:** ${span.count} spans\n`; if (span.totalCpuDuration !== undefined) { md += `- **Total CPU Duration:** ${formatDuration(span.totalCpuDuration)}\n`; } if (span.avgCpuDuration !== undefined) { md += `- **Avg CPU Duration:** ${formatDuration(span.avgCpuDuration)}\n`; } if (span.totalCorrectedDuration !== undefined) { md += `- **Total Corrected Duration:** ${formatDuration(span.totalCorrectedDuration)}\n`; } if (span.avgCorrectedDuration !== undefined) { md += `- **Avg Corrected Duration:** ${formatDuration(span.avgCorrectedDuration)}\n`; } md += `- **Start (relative to parent):** ${formatRelative(span.startRelativeToParent)}\n`; md += `- **End (relative to parent):** ${formatRelative(span.endRelativeToParent)}\n`; const exampleId = span.firstSpanId ?? span.id; md += `\n#### First span as example (ID: \`${exampleId}\`)\n`; md += `- **CPU Duration:** ${formatDuration(span.cpuDuration)}\n`; md += `- **Corrected Duration:** ${formatDuration(span.correctedDuration)}\n`; } else { md += `- **CPU Duration:** ${formatDuration(span.cpuDuration)}\n`; md += `- **Corrected Duration:** ${formatDuration(span.correctedDuration)}\n`; md += `- **Start (relative to parent):** ${formatRelative(span.startRelativeToParent)}\n`; md += `- **End (relative to parent):** ${formatRelative(span.endRelativeToParent)}\n`; } if (span.args && span.args.length > 0) { md += `\n**Attributes:**\n`; for (const [k, v] of span.args){ md += `- \`${k}\`: ${v}\n`; } } const memSummary = summarizeMemorySamples(span.memorySamples); if (memSummary) { md += `\n**Memory (TurboMalloc live bytes):** ${memSummary}\n`; } md += '\n---\n\n'; return md; } async function startTurboTraceServerCli(file, port, mcpPort) { const wsPort = port ?? DEFAULT_WS_PORT; const httpPort = mcpPort ?? wsPort + 1; let bindings; try { bindings = await (0, _swc.loadBindings)(); } catch { console.error('Error: Could not load native bindings. The trace server requires native (non-WASM) bindings.'); process.exit(1); } let handle; try { // Start the WebSocket trace server on a background thread (non-blocking). handle = bindings.turbo.startTurbopackTraceServerHandle(file, wsPort); } catch (err) { console.error(`Error: Could not start trace server for "${file}": ${err instanceof Error ? err.message : err}`); process.exit(1); } console.log(`Turbopack trace server started. View trace at https://trace.nextjs.org?port=${wsPort}`); // Create the MCP server. const mcpServer = new McpServer({ name: 'Next.js Trace Server MCP', version: '0.1.0' }); mcpServer.registerTool('query_spans', { description: 'Query spans from a turbopack trace file. Returns spans with timing, CPU usage, attribute details, and TurboMalloc live-memory samples recorded while each span was active. Set `outputType` to "json" for machine-readable output (including the raw `memorySamples` array of `[ts_offset_ticks, bytes, pressure]` triples per span — pressure is 0 = none, higher = more memory pressure) or "markdown" (default) for a human-readable summary. Use the `parent` parameter (with an ID from a previous result) to drill into children. Results are paginated to 20 spans per page.', inputSchema: { parent: _zod.default.string().optional().describe('Span ID to enumerate children of. Omit for root-level spans. Use the `id` field from a previous result.'), aggregated: _zod.default.boolean().optional().describe('When true (default), aggregate spans with the same name into a single entry. Set to false to see individual raw spans.'), sort: _zod.default.enum([ 'value', 'name' ]).optional().describe('Sort mode: "value" for corrected duration descending, "name" for alphabetical. Omit for execution order.'), search: _zod.default.string().optional().describe('Substring search query applied to span name and category.'), page: _zod.default.number().optional().describe('1-based page number. Default 1.'), outputType: _zod.default.enum([ 'markdown', 'json' ]).optional().describe('Output format. "markdown" (default) returns human-readable markdown. "json" returns structured JSON with all span fields.') } }, (args)=>{ const result = bindings.turbo.queryTraceSpans(handle, { parent: args.parent, aggregated: args.aggregated ?? true, sort: args.sort, search: args.search, page: args.page ?? 1 }); const { spans, page, totalPages, totalCount } = result; if (args.outputType === 'json') { return { content: [ { type: 'text', text: JSON.stringify({ spans, page, totalPages, totalCount }) } ] }; } const parentLabel = args.parent ? `children of ID \`${args.parent}\`` : 'root level'; let md = `## Spans at ${parentLabel} — Page ${page} of ${totalPages} (${totalCount} total)\n\n`; if (spans.length === 0) { md += '_No spans found._\n'; } for (const span of spans){ md += renderSpanMarkdown(span); } if (page < totalPages) { md += `Use \`page=${page + 1}\` to see more results.\n`; } return { content: [ { type: 'text', text: md } ] }; }); // Start the HTTP server for MCP (served at /mcp). const server = _nodehttp.default.createServer(async (req, res)=>{ if (req.url !== '/mcp') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found. MCP endpoint is at /mcp\n'); return; } const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); try { res.on('close', ()=>transport.close()); await mcpServer.connect(transport); let body = ''; req.setEncoding('utf8'); await new Promise((resolve, reject)=>{ req.on('data', (chunk)=>{ body += chunk; }); req.on('end', resolve); req.on('error', reject); }); await transport.handleRequest(req, res, body ? JSON.parse(body) : undefined); } catch (err) { if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Internal server error' }, id: null })); } } }); server.on('error', (err)=>{ if (err.code === 'EADDRINUSE') { console.error(`Error: MCP port ${httpPort} is already in use. Use --mcp-port to specify a different port.`); } else { console.error(`Error starting MCP server: ${err.message}`); } process.exit(1); }); server.listen(httpPort, '127.0.0.1', ()=>{ console.log(`Query this trace from the command line: next internal query-trace --help --port ${httpPort}`); console.log(`Alternatively, connect an MCP client to http://127.0.0.1:${httpPort}/mcp`); }); } //# sourceMappingURL=turbo-trace-server.js.map