From af23248627a227b5c498d31d937ca8724f207c50 Mon Sep 17 00:00:00 2001 From: OJ Kwon Date: Wed, 20 Apr 2022 01:06:52 -0700 Subject: [PATCH] feat(trace): next/trace to event format converter (#36281) This PR adds a small utility script for the existing `next/trace` converts emitted output into chromium's trace event format (https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?mode=html#). Converted output can be loaded in a couple of visualizers that understand trace event format. - `chrome://tracing` - `https://ui.perfetto.dev/` Technically it is possible to make generated event format to be compatible to chrome devtool's profiler as well, but that is not dealt with initial implementation. This is very straightforward, naive conversion between generated trace to the specific format instead of trying to augment existing traces: in result, some of the values are dummy (pid, tid, category). Depends on usecases we can potentially expand & correct later if needed. Below screenshot is perfetto from `bench/nested-deps`. Screen Shot 2022-04-19 at 10 35 28 AM ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- scripts/trace-to-event-format.mjs | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 scripts/trace-to-event-format.mjs diff --git a/scripts/trace-to-event-format.mjs b/scripts/trace-to-event-format.mjs new file mode 100644 index 000000000000..88ab8262375c --- /dev/null +++ b/scripts/trace-to-event-format.mjs @@ -0,0 +1,190 @@ +import { createReadStream, createWriteStream } from 'fs' +import { createInterface } from 'readline' +import path from 'path' +import { EOL } from 'os' + +const createEvent = (trace, ph, cat) => ({ + name: trace.name, + // Category. We don't collect this for now. + cat: cat ?? '-', + ts: trace.timestamp, + // event category. We only use duration events (B/E) for now. + ph, + // process id. We don't collect this for now, putting arbitary numbers. + pid: 1, + // thread id. We don't collect this for now, putting arbitary numebers. + tid: 10, + args: trace.tags, +}) + +const cleanFilename = (filename) => { + if (filename.includes('&absolutePagePath=')) { + filename = + 'page ' + + decodeURIComponent( + filename.replace(/.+&absolutePagePath=/, '').slice(0, -1) + ) + } + filename = filename.replace(/.+!(?!$)/, '') + return filename +} + +const getPackageName = (filename) => { + const match = /.+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec( + cleanFilename(filename) + ) + return match && match[1] +} + +/** + * Create, reports spans recursively with its inner child spans. + */ +const reportSpanRecursively = (stream, trace, parentSpan) => { + // build-* span contains tags with path to the modules, trying to clean up if possible + const isBuildModule = trace.name.startsWith('build-module-') + if (isBuildModule) { + trace.packageName = getPackageName(trace.tags.name) + // replace name to cleaned up pkg name + trace.tags.name = trace.packageName + if (trace.children) { + const queue = [...trace.children] + trace.children = [] + for (const e of queue) { + if (e.name.startsWith('build-module-')) { + const pkgName = getPackageName(e.tags.name) + if (!trace.packageName || pkgName !== trace.packageName) { + trace.children.push(e) + } else { + if (e.children) queue.push(...e.children) + } + } + } + } + } + + /** + * interface TraceEvent { + * traceId: string; + * parentId: number; + * name: string; + * id: number; + * startTime: number; + * timestamp: number; + * duration: number; + * tags: Record + * } + */ + stream.write(JSON.stringify(createEvent(trace, 'B'))) + stream.write(',') + + // Spans should be reported in chronological order + trace.children?.sort((a, b) => a.startTime - b.startTime) + trace.children?.forEach((childTrace) => + reportSpanRecursively(stream, childTrace) + ) + + stream.write( + JSON.stringify( + createEvent( + { + ...trace, + timestamp: trace.timestamp + trace.duration, + }, + 'E' + ) + ) + ) + stream.write(',') +} + +/** + * Read generated trace from file system, augment & sent it to the remote tracer. + */ +const collectTraces = async (filePath, outFilePath, metadata) => { + const readLineInterface = createInterface({ + input: createReadStream(filePath), + crlfDelay: Infinity, + }) + + const writeStream = createWriteStream(outFilePath) + writeStream.write(`[${EOL}`) + + const traces = new Map() + const rootTraces = [] + + // Input trace file contains newline-seperated sets of traces, where each line is valid JSON + // type of Array. Read it line-by-line to manually reconstruct trace trees. + // + // We have to read through end of the trace - + // Trace events in the input file can appear out of order, so we need to remodel the shape of the span tree before reporting + for await (const line of readLineInterface) { + JSON.parse(line).forEach((trace) => traces.set(trace.id, trace)) + } + + // Link inner, child spans to the parents to reconstruct span with correct relations + for (const event of traces.values()) { + if (event.parentId) { + event.parent = traces.get(event.parentId) + if (event.parent) { + if (!event.parent.children) event.parent.children = [] + event.parent.children.push(event) + } + } + + if (!event.parent) { + rootTraces.push(event) + } + } + + for (const trace of rootTraces) { + reportSpanRecursively(writeStream, trace) + } + + writeStream.write( + JSON.stringify({ + name: 'trace', + ph: 'M', + args: metadata, + }) + ) + writeStream.write(`${EOL}]`) +} + +/** + * Naively validate, collect necessary args. + */ +const validateArgs = async () => { + // Collect necessary default metadata. Script should pass cli args as in order of + // - trace file to read + // - output file path (optional) + // - path to next.config.js (optional) + const [, , traceFilePath, outFile, configFilePath] = process.argv + const outFilePath = outFile ?? `${traceFilePath}.event` + const config = configFilePath + ? (await import(path.resolve(process.cwd(), configFilePath))).default + : {} + + if (!traceFilePath) { + throw new Error( + `Cannot collect traces without necessary metadata. +Try to run script with below args: + +node trace-to-event-format.mjs tracefilepath [outfilepath] [configfilepath]` + ) + } + + const metadata = { + config, + } + + return [traceFilePath, outFilePath, metadata] +} + +validateArgs() + .then(([traceFilePath, outFilePath, metadata]) => + collectTraces(traceFilePath, outFilePath, metadata) + ) + .catch((e) => { + console.error(`Failed to generate traces`) + console.error(e) + })