From 09d19892de427d0c64fb6c46b756bbe5dd6b9678 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 2 Jan 2023 18:42:09 +0100 Subject: [PATCH] perf: improve vi.mock performance (#2594) --- packages/vitest/src/integrations/vi.ts | 10 +-- packages/vitest/src/utils/error.ts | 22 ++++++ packages/vitest/src/utils/source-map.ts | 92 +++++++++++++------------ 3 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 packages/vitest/src/utils/error.ts diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 8fc981710e59..474befedd8c8 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -1,9 +1,10 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' -import { parseStacktrace } from '../utils/source-map' +import { parseSingleStack } from '../utils/source-map' import type { VitestMocker } from '../runtime/mocker' import type { ResolvedConfig, RuntimeConfig } from '../types' import { getWorkerState, resetModules, waitForImportsToResolve } from '../utils' import type { MockFactoryWithHelper } from '../types/mocker' +import { createSimpleStackTrace } from '../utils/error' import { FakeTimers } from './mock/timers' import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy' import { fn, isMockFunction, spies, spyOn } from './spy' @@ -110,9 +111,10 @@ class VitestUtils { fn = fn private getImporter() { - const err = new Error('mock') - const [,, importer] = parseStacktrace(err, true) - return importer.file + const stackTrace = createSimpleStackTrace({ stackTraceLimit: 4 }) + const importerStack = stackTrace.split('\n')[4] + const stack = parseSingleStack(importerStack) + return stack?.file || '' } /** diff --git a/packages/vitest/src/utils/error.ts b/packages/vitest/src/utils/error.ts new file mode 100644 index 000000000000..8189d03c7239 --- /dev/null +++ b/packages/vitest/src/utils/error.ts @@ -0,0 +1,22 @@ +interface ErrorOptions { + message?: string + stackTraceLimit?: number +} + +/** + * Get original stacktrace without source map support the most performant way. + * - Create only 1 stack frame. + * - Rewrite prepareStackTrace to bypass "support-stack-trace" (usually takes ~250ms). + */ +export function createSimpleStackTrace(options?: ErrorOptions) { + const { message = 'error', stackTraceLimit = 1 } = options || {} + const limit = Error.stackTraceLimit + const prepareStackTrace = Error.prepareStackTrace + Error.stackTraceLimit = stackTraceLimit + Error.prepareStackTrace = e => e.stack + const err = new Error(message) + const stackTrace = err.stack || '' + Error.prepareStackTrace = prepareStackTrace + Error.stackTraceLimit = limit + return stackTrace +} diff --git a/packages/vitest/src/utils/source-map.ts b/packages/vitest/src/utils/source-map.ts index 081e2ecff103..f071a1395b3f 100644 --- a/packages/vitest/src/utils/source-map.ts +++ b/packages/vitest/src/utils/source-map.ts @@ -27,63 +27,69 @@ function extractLocation(urlLike: string) { return [parts[1], parts[2] || undefined, parts[3] || undefined] } -export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] { - if (!e) - return [] +// Based on https://github.com/stacktracejs/error-stack-parser +// Credit to stacktracejs +export function parseSingleStack(raw: string): ParsedStack | null { + let line = raw.trim() - if (e.stacks) - return e.stacks + if (line.includes('(eval ')) + line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '') - const stackStr = e.stack || e.stackStr || '' - const stackFrames = stackStr - .split('\n') - // Based on https://github.com/stacktracejs/error-stack-parser - // Credit to stacktracejs - .map((raw): ParsedStack | null => { - let line = raw.trim() + let sanitizedLine = line + .replace(/^\s+/, '') + .replace(/\(eval code/g, '(') + .replace(/^.*?\s+/, '') - if (line.includes('(eval ')) - line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '') + // capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in + // case it has spaces in it, as the string is split on \s+ later on + const location = sanitizedLine.match(/ (\(.+\)$)/) - let sanitizedLine = line - .replace(/^\s+/, '') - .replace(/\(eval code/g, '(') - .replace(/^.*?\s+/, '') + // remove the parenthesized location from the line, if it was matched + sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine - // capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in - // case it has spaces in it, as the string is split on \s+ later on - const location = sanitizedLine.match(/ (\(.+\)$)/) + // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine + // because this line doesn't have function name + const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine) + let method = (location && sanitizedLine) || '' + let file = url && ['eval', ''].includes(url) ? undefined : url - // remove the parenthesized location from the line, if it was matched - sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine + if (!file || !lineNumber || !columnNumber) + return null - // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine - // because this line doesn't have function name - const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine) - let method = (location && sanitizedLine) || '' - let file = url && ['eval', ''].includes(url) ? undefined : url + if (method.startsWith('async ')) + method = method.slice(6) - if (!file || !lineNumber || !columnNumber) - return null + if (file.startsWith('file://')) + file = file.slice(7) - if (method.startsWith('async ')) - method = method.slice(6) + // normalize Windows path (\ -> /) + file = resolve(file) - if (file.startsWith('file://')) - file = file.slice(7) + return { + method, + file, + line: parseInt(lineNumber), + column: parseInt(columnNumber), + } +} - // normalize Windows path (\ -> /) - file = resolve(file) +export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] { + if (!e) + return [] + + if (e.stacks) + return e.stacks + + const stackStr = e.stack || e.stackStr || '' + const stackFrames = stackStr + .split('\n') + .map((raw): ParsedStack | null => { + const stack = parseSingleStack(raw) - if (!full && stackIgnorePatterns.some(p => file && file.includes(p))) + if (!stack || (!full && stackIgnorePatterns.some(p => stack.file.includes(p)))) return null - return { - method, - file, - line: parseInt(lineNumber), - column: parseInt(columnNumber), - } + return stack }) .filter(notNullish)