Skip to content

Instantly share code, notes, and snippets.

@jazmon
Created October 4, 2023 14:08
Show Gist options
  • Save jazmon/257c56503e81e0d1573a784797c45c26 to your computer and use it in GitHub Desktop.
Save jazmon/257c56503e81e0d1573a784797c45c26 to your computer and use it in GitHub Desktop.
Next.js build trace tree formatting
/* eslint-disable no-bitwise */
// ISC License
// Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// https://github.com/alexeyraspopov/picocolors/blob/b6261487e7b81aaab2440e397a356732cad9e342/picocolors.js#L1
const { env, stdout } = globalThis?.process ?? {};
const enabled = env && !env.NO_COLOR && (env.FORCE_COLOR || (stdout?.isTTY && !env.CI && env.TERM !== 'dumb'));
const replaceClose = (str, close, replace, index) => {
const start = str.substring(0, index) + replace;
const end = str.substring(index + close.length);
const nextIndex = end.indexOf(close);
return ~nextIndex ? start + replaceClose(end, close, replace, nextIndex) : start + end;
};
const formatter =
(open, close, replace = open) =>
input => {
const string = `${input}`;
const index = string.indexOf(close, open.length);
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
};
const reset = enabled ? s => `\x1b[0m${s}\x1b[0m` : String;
const bold = enabled ? formatter('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m') : String;
const dim = enabled ? formatter('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m') : String;
const italic = enabled ? formatter('\x1b[3m', '\x1b[23m') : String;
const underline = enabled ? formatter('\x1b[4m', '\x1b[24m') : String;
const inverse = enabled ? formatter('\x1b[7m', '\x1b[27m') : String;
const hidden = enabled ? formatter('\x1b[8m', '\x1b[28m') : String;
const strikethrough = enabled ? formatter('\x1b[9m', '\x1b[29m') : String;
const black = enabled ? formatter('\x1b[30m', '\x1b[39m') : String;
const red = enabled ? formatter('\x1b[31m', '\x1b[39m') : String;
const green = enabled ? formatter('\x1b[32m', '\x1b[39m') : String;
const yellow = enabled ? formatter('\x1b[33m', '\x1b[39m') : String;
const blue = enabled ? formatter('\x1b[34m', '\x1b[39m') : String;
const magenta = enabled ? formatter('\x1b[35m', '\x1b[39m') : String;
const cyan = enabled ? formatter('\x1b[36m', '\x1b[39m') : String;
const white = enabled ? formatter('\x1b[37m', '\x1b[39m') : String;
const gray = enabled ? formatter('\x1b[90m', '\x1b[39m') : String;
const bgBlack = enabled ? formatter('\x1b[40m', '\x1b[49m') : String;
const bgRed = enabled ? formatter('\x1b[41m', '\x1b[49m') : String;
const bgGreen = enabled ? formatter('\x1b[42m', '\x1b[49m') : String;
const bgYellow = enabled ? formatter('\x1b[43m', '\x1b[49m') : String;
const bgBlue = enabled ? formatter('\x1b[44m', '\x1b[49m') : String;
const bgMagenta = enabled ? formatter('\x1b[45m', '\x1b[49m') : String;
const bgCyan = enabled ? formatter('\x1b[46m', '\x1b[49m') : String;
const bgWhite = enabled ? formatter('\x1b[47m', '\x1b[49m') : String;
module.exports = {
reset,
bold,
dim,
italic,
underline,
inverse,
hidden,
strikethrough,
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
gray,
bgBlack,
bgRed,
bgGreen,
bgYellow,
bgBlue,
bgMagenta,
bgCyan,
bgWhite,
};
/* eslint-disable no-console */
/* eslint-disable no-plusplus */
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable no-param-reassign */
const fs = require('fs');
const eventStream = require('event-stream');
const { bold, blue, cyan, green, magenta, red, yellow } = require('./picolors');
const file = fs.createReadStream(process.argv[2]);
const sum = (...args) => args.reduce((a, b) => a + b, 0);
const aggregate = event => {
const isBuildModule = event.name.startsWith('build-module-');
event.range = event.timestamp + (event.duration || 0);
event.total = isBuildModule ? event.duration : 0;
if (isBuildModule) {
event.packageName = getPackageName(event.tags.name);
if (event.children) {
const queue = [...event.children];
event.children = [];
event.childrenTimings = {};
event.mergedChildren = 0;
for (const e of queue) {
if (!e.name.startsWith('build-module-')) {
event.childrenTimings[e.name] = (event.childrenTimings[e.name] || 0) + e.duration;
continue;
}
const pkgName = getPackageName(e.tags.name);
if (!event.packageName || pkgName !== event.packageName) {
event.children.push(e);
} else {
event.duration += e.duration;
event.mergedChildren++;
if (e.children) queue.push(...e.children);
}
}
}
}
if (event.children) {
event.children.forEach(aggregate);
event.children.sort((a, b) => a.timestamp - b.timestamp);
event.range = Math.max(event.range, ...event.children.map(c => c.range || event.timestamp));
event.total += isBuildModule ? sum(...event.children.map(c => c.total || 0)) : 0;
}
};
const formatDuration = (duration, isBold) => {
const color = isBold ? bold : x => x;
if (duration < 1000) {
return color(`${duration} Β΅s`);
}
if (duration < 10000) {
return color(`${Math.round(duration / 100) / 10} ms`);
}
if (duration < 100000) {
return color(`${Math.round(duration / 1000)} ms`);
}
if (duration < 1_000_000) {
return color(cyan(`${Math.round(duration / 1000)} ms`));
}
if (duration < 10_000_000) {
return color(green(`${Math.round(duration / 100000) / 10}s`));
}
if (duration < 20_000_000) {
return color(yellow(`${Math.round(duration / 1000000)}s`));
}
if (duration < 100_000_000) {
return color(red(`${Math.round(duration / 1000000)}s`));
}
return color(`πŸ”₯${red(`${Math.round(duration / 1000000)}s`)}`);
};
const formatTimes = event => {
const range = event.range - event.timestamp;
const additionalInfo = [];
if (event.total && event.total !== range) additionalInfo.push(`total ${formatDuration(event.total)}`);
if (event.duration !== range) additionalInfo.push(`self ${formatDuration(event.duration, bold)}`);
return `${formatDuration(range, additionalInfo.length === 0)}${
additionalInfo.length ? ` (${additionalInfo.join(', ')})` : ''
}`;
};
const formatFilename = filename => {
return cleanFilename(filename).replace(/.+[\\/]node_modules[\\/]/, '');
};
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];
};
const formatEvent = event => {
let head;
switch (event.name) {
case 'webpack-compilation':
head = `${bold(`${event.tags.name} compilation`)} ${formatTimes(event)}`;
break;
case 'webpack-invalidated-client':
case 'webpack-invalidated-server':
head = `${bold(`${event.name.slice(-6)} recompilation`)} ${
event.tags.trigger === 'manual' ? '(new page discovered)' : `(${formatFilename(event.tags.trigger)})`
} ${formatTimes(event)}`;
break;
case 'add-entry':
head = `${blue('entry')} ${formatFilename(event.tags.request)}`;
break;
case 'hot-reloader':
head = `${bold(green(`hot reloader`))}`;
break;
case 'export-page':
head = `${event.name} ${event.tags.path} ${formatTimes(event)}`;
break;
default:
if (event.name.startsWith('build-module-')) {
const { mergedChildren, childrenTimings, packageName } = event;
head = `${magenta('module')} ${
packageName
? `${bold(cyan(packageName))} (${formatFilename(event.tags.name)}${
mergedChildren ? ` + ${mergedChildren}` : ''
})`
: formatFilename(event.tags.name)
} ${formatTimes(event)}`;
if (childrenTimings && Object.keys(childrenTimings).length) {
head += ` [${Object.keys(childrenTimings)
.map(key => `${key} ${formatDuration(childrenTimings[key])}`)
.join(', ')}]`;
}
} else {
head = `${event.name} ${formatTimes(event)}`;
}
break;
}
if (event.children && event.children.length) {
return `${head}\n${treeChildren(event.children.map(formatEvent))}`;
}
return head;
};
const indentWith = (str, firstLinePrefix, otherLinesPrefix) => {
return firstLinePrefix + str.replace(/\n/g, `\n${otherLinesPrefix}`);
};
const treeChildren = items => {
let str = '';
for (let i = 0; i < items.length; i++) {
if (i !== items.length - 1) {
str += `${indentWith(items[i], 'β”œβ”€ ', 'β”‚ ')}\n`;
} else {
str += indentWith(items[i], '└─ ', ' ');
}
}
return str;
};
const tracesById = new Map();
file
.pipe(eventStream.split())
.pipe(
eventStream.mapSync(data => {
if (!data) return;
const json = JSON.parse(data);
json.forEach(event => {
tracesById.set(event.id, event);
});
}),
)
.on('end', () => {
const rootEvents = [];
for (const event of tracesById.values()) {
if (event.parentId) {
event.parent = tracesById.get(event.parentId);
if (event.parent) {
if (!event.parent.children) event.parent.children = [];
event.parent.children.push(event);
}
}
if (!event.parent) rootEvents.push(event);
}
for (const event of rootEvents) {
aggregate(event);
}
console.log(`Explanation:
${formatEvent({
name: 'build-module-js',
tags: { name: '/Users/next-user/src/magic-ui/pages/index.js' },
duration: 163000,
timestamp: 0,
range: 24000000,
total: 33000000,
childrenTimings: { 'read-resource': 873, 'next-babel-turbo-loader': 135000 },
})}
════════╀═══════════════════════════════════ ═╀═ ═╀═ ═╀════ ═══════════╀════════════════════════════════════════
└─ name of the processed module β”‚ β”‚ β”‚ └─ timings of nested steps
β”‚ β”‚ └─ building the module itself (including overlapping parallel actions)
β”‚ └─ total build time of this modules and all nested ones (including overlapping parallel actions)
└─ how long until the module and all nested modules took compiling (wall time, without overlapping actions)
${formatEvent({
name: 'build-module-js',
tags: {
name: '/Users/next-user/src/magic-ui/node_modules/lodash/camelCase.js',
},
packageName: 'lodash',
duration: 958000,
timestamp: 0,
range: 295000,
childrenTimings: { 'read-resource': 936000 },
mergedChildren: 281,
})}
═╀════ ══════╀════════════ ═╀═
β”‚ β”‚ └─ number of modules that are merged into that line
β”‚ └─ first module that is imported
└─ npm package name
`);
for (const event of rootEvents) {
console.log(formatEvent(event));
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment