Nuxt/debug/plugins/timings-babel.mjs
2025-01-17 22:43:00 +00:00

153 lines
4.9 KiB
JavaScript

// @ts-check
import { declare } from '@babel/helper-plugin-utils'
import { types as t } from '@babel/core'
// inlined from https://github.com/danielroe/errx
function captureStackTrace () {
const IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[a-z]:[/\\]/i
const LINE_RE = /^\s+at (?:(?<function>[^)]+) \()?(?<source>[^)]+)\)?$/u
const SOURCE_RE = /^(?<source>.+):(?<line>\d+):(?<column>\d+)$/u
if (!Error.captureStackTrace) {
return []
}
// eslint-disable-next-line unicorn/error-message
const stack = new Error()
Error.captureStackTrace(stack)
const trace = []
for (const line of stack.stack?.split('\n') || []) {
const parsed = LINE_RE.exec(line)?.groups
if (!parsed) {
continue
}
if (!parsed.source) {
continue
}
const parsedSource = SOURCE_RE.exec(parsed.source)?.groups
if (parsedSource) {
Object.assign(parsed, parsedSource)
}
if (IS_ABSOLUTE_RE.test(parsed.source)) {
parsed.source = `file://${parsed.source}`
}
if (parsed.source === import.meta.url) {
continue
}
for (const key of ['line', 'column']) {
if (parsed[key]) {
// @ts-expect-error
parsed[key] = Number(parsed[key])
}
}
trace.push(parsed)
}
return trace
}
export const leading = `
const ___captureStackTrace = ${captureStackTrace.toString()};
globalThis.___calls ||= {};
globalThis.___timings ||= {};
globalThis.___callers ||= {};`
function onExit () {
if (globalThis.___logged) { return }
globalThis.___logged = true
// worst by total time
const timings = Object.entries(globalThis.___timings)
const topFunctionsTotalTime = timings
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([name, time]) => ({
name,
time: Number(Number(time).toFixed(2)),
calls: globalThis.___calls[name],
callers: globalThis.___callers[name] && Object.entries(globalThis.___callers[name]).map(([name, count]) => `${name.trim()} (${count})`).join(', '),
}))
// eslint-disable-next-line no-console
console.log('Top 10 functions by total time:')
// eslint-disable-next-line no-console
console.table(topFunctionsTotalTime)
// worst by average time (excluding single calls)
const topFunctionsAverageTime = timings
.filter(([name]) => (globalThis.___calls[name] || 0) > 1)
.map(([name, time]) => [name, time / (globalThis.___calls[name] || 1)])
// @ts-expect-error
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([name, time]) => ({
name,
time: Number(Number(time).toFixed(2)),
calls: name && globalThis.___calls[name],
callers: name && globalThis.___callers[name] && Object.entries(globalThis.___callers[name]).sort((a, b) => b[1] - a[1]).map(([name, count]) => `${name.trim()} (${count})`).join(', '),
}))
// eslint-disable-next-line no-console
console.log('Top 10 functions by average time:')
// eslint-disable-next-line no-console
console.table(topFunctionsAverageTime)
}
export const trailing = `process.on("exit", ${onExit.toString()})`
/** @param {string} functionName */
export function generateInitCode (functionName) {
return `
___calls.${functionName} = (___calls.${functionName} || 0) + 1;
___timings.${functionName} ||= 0;
const ___now = Date.now();`
}
/** @param {string} functionName */
export function generateFinallyCode (functionName) {
return `
___timings.${functionName} += Date.now() - ___now;
try {
const ___callee = ___captureStackTrace()[1]?.function;
if (___callee) {
___callers.${functionName} ||= {};
___callers.${functionName}[' ' + ___callee] = (___callers.${functionName}[' ' + ___callee] || 0) + 1;
}
} catch {}`
}
export default declare((api) => {
api.assertVersion(7)
return {
name: 'annotate-function-timings',
visitor: {
Program (path) {
path.unshiftContainer('body', t.expressionStatement(t.identifier(leading)))
path.pushContainer('body', t.expressionStatement(t.identifier(trailing)))
},
FunctionDeclaration (path) {
const functionName = path.node.id?.name
const start = path.get('body').get('body')[0]
const end = path.get('body').get('body').pop()
if (!functionName || ['createJiti', '___captureStackTrace', '_interopRequireDefault'].includes(functionName) || !start || !end) { return }
const initCode = generateInitCode(functionName)
const finallyCode = generateFinallyCode(functionName)
const originalCode = path.get('body').get('body').map(statement => statement.node)
path.get('body').get('body').forEach(statement => statement.remove())
path.get('body').unshiftContainer('body', t.expressionStatement(t.identifier(initCode)))
path.get('body').pushContainer('body', t.tryStatement(
t.blockStatement(originalCode),
t.catchClause(t.identifier('e'), t.blockStatement([])),
t.blockStatement([t.expressionStatement(t.identifier(finallyCode))]),
))
},
},
}
})