From 0685a696693d7f30f776a251f07ede8d6c7a48d0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Jan 2023 11:32:01 +0100 Subject: [PATCH 1/4] perf: improve vi.mock performance --- packages/vitest/src/integrations/vi.ts | 19 ++++- packages/vitest/src/utils/source-map.ts | 92 +++++++++++++------------ 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 8fc981710e59..fda58163dafc 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -1,5 +1,5 @@ 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' @@ -109,10 +109,23 @@ class VitestUtils { spyOn = spyOn fn = fn + /** + * Get importer with the most performant way. + * - Create only 3 stack frames, because the last one is the importer. + * - Rewrite prepareStackTrace to bypass support-stack-trace (usually takes ~250ms). + */ private getImporter() { + const limit = Error.stackTraceLimit + const prepareStackTrace = Error.prepareStackTrace + Error.stackTraceLimit = 3 + Error.prepareStackTrace = e => e.stack const err = new Error('mock') - const [,, importer] = parseStacktrace(err, true) - return importer.file + const stackTrace = err.stack || '' + Error.prepareStackTrace = prepareStackTrace + Error.stackTraceLimit = limit + const importerStack = stackTrace.split('\n')[3] + const stack = parseSingleStack(importerStack) + return stack?.file || '' } /** 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) From aec0df83785ba46c4e0f3da7491bb5a408dde554 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Jan 2023 11:46:10 +0100 Subject: [PATCH 2/4] refactor: move simple stacktrace to a standalone function --- packages/vitest/src/integrations/vi.ts | 10 ++-------- packages/vitest/src/utils/error.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 8 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 fda58163dafc..5d10e7c70a5e 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -4,6 +4,7 @@ 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' @@ -115,14 +116,7 @@ class VitestUtils { * - Rewrite prepareStackTrace to bypass support-stack-trace (usually takes ~250ms). */ private getImporter() { - const limit = Error.stackTraceLimit - const prepareStackTrace = Error.prepareStackTrace - Error.stackTraceLimit = 3 - Error.prepareStackTrace = e => e.stack - const err = new Error('mock') - const stackTrace = err.stack || '' - Error.prepareStackTrace = prepareStackTrace - Error.stackTraceLimit = limit + const stackTrace = createSimpleStackTrace({ stackTraceLimit: 3 }) const importerStack = stackTrace.split('\n')[3] 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..d0cb758de127 --- /dev/null +++ b/packages/vitest/src/utils/error.ts @@ -0,0 +1,17 @@ +interface ErrorOptions { + message?: string + stackTraceLimit?: number +} + +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 +} From e87cb773f22a7671b4a777e435afffc49752c206 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Jan 2023 11:48:23 +0100 Subject: [PATCH 3/4] chore: cleanup --- packages/vitest/src/integrations/vi.ts | 5 ----- packages/vitest/src/utils/error.ts | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 5d10e7c70a5e..daeab21ca234 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -110,11 +110,6 @@ class VitestUtils { spyOn = spyOn fn = fn - /** - * Get importer with the most performant way. - * - Create only 3 stack frames, because the last one is the importer. - * - Rewrite prepareStackTrace to bypass support-stack-trace (usually takes ~250ms). - */ private getImporter() { const stackTrace = createSimpleStackTrace({ stackTraceLimit: 3 }) const importerStack = stackTrace.split('\n')[3] diff --git a/packages/vitest/src/utils/error.ts b/packages/vitest/src/utils/error.ts index d0cb758de127..8189d03c7239 100644 --- a/packages/vitest/src/utils/error.ts +++ b/packages/vitest/src/utils/error.ts @@ -3,6 +3,11 @@ interface ErrorOptions { 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 From 85a85a1d195a2c0bf76f817f904192f646b2f1b4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 2 Jan 2023 11:57:25 +0100 Subject: [PATCH 4/4] chore: cleanup --- packages/vitest/src/integrations/vi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index daeab21ca234..474befedd8c8 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -111,8 +111,8 @@ class VitestUtils { fn = fn private getImporter() { - const stackTrace = createSimpleStackTrace({ stackTraceLimit: 3 }) - const importerStack = stackTrace.split('\n')[3] + const stackTrace = createSimpleStackTrace({ stackTraceLimit: 4 }) + const importerStack = stackTrace.split('\n')[4] const stack = parseSingleStack(importerStack) return stack?.file || '' }