import fs from 'fs' import eventStream from 'event-stream' import chalk from 'chalk' 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, bold) => { const color = bold ? chalk.bold : (x) => x if (duration < 1000) { return color(`${duration} ยตs`) } else if (duration < 10000) { return color(`${Math.round(duration / 100) / 10} ms`) } else if (duration < 100000) { return color(`${Math.round(duration / 1000)} ms`) } else if (duration < 1_000_000) { return color(chalk.cyan(`${Math.round(duration / 1000)} ms`)) } else if (duration < 10_000_000) { return color(chalk.green(`${Math.round(duration / 100000) / 10}s`)) } else if (duration < 20_000_000) { return color(chalk.yellow(`${Math.round(duration / 1000000)}s`)) } else if (duration < 100_000_000) { return color(chalk.red(`${Math.round(duration / 1000000)}s`)) } else { return color('๐Ÿ”ฅ' + chalk.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, chalk.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 = `${chalk.bold(`${event.tags.name} compilation`)} ${formatTimes( event )}` break case 'webpack-invalidated-client': case 'webpack-invalidated-server': head = `${chalk.bold(`${event.name.slice(-6)} recompilation`)} ${ event.tags.trigger === 'manual' ? '(new page discovered)' : `(${formatFilename(event.tags.trigger)})` } ${formatTimes(event)}` break case 'add-entry': head = `${chalk.blueBright('entry')} ${formatFilename( event.tags.request )}` break case 'hot-reloader': head = `${chalk.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 = `${chalk.magentaBright('module')} ${ packageName ? `${chalk.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)) } else { 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)) } })