next
Version:
The React Framework
246 lines (245 loc) • 10.9 kB
JavaScript
;
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