Skip to content

Commit

Permalink
feat(trace): next/trace to event format converter (#36281)
Browse files Browse the repository at this point in the history
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`.

<img width="1166" alt="Screen Shot 2022-04-19 at 10 35 28 AM" src="https://user-images.githubusercontent.com/1210596/164062522-4d34a8c0-d66a-4c9e-9e15-08e0cd6d41a7.png">

## 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`
  • Loading branch information
kwonoj committed Apr 20, 2022
1 parent 835aeae commit af23248
Showing 1 changed file with 190 additions and 0 deletions.
190 changes: 190 additions & 0 deletions 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<string, any>
* }
*/
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<TraceEvent>. 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)
})

0 comments on commit af23248

Please sign in to comment.