Skip to content

Commit 09d1989

Browse files
authoredJan 2, 2023
perf: improve vi.mock performance (#2594)
1 parent c8e6fb6 commit 09d1989

File tree

3 files changed

+77
-47
lines changed

3 files changed

+77
-47
lines changed
 

‎packages/vitest/src/integrations/vi.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
2-
import { parseStacktrace } from '../utils/source-map'
2+
import { parseSingleStack } from '../utils/source-map'
33
import type { VitestMocker } from '../runtime/mocker'
44
import type { ResolvedConfig, RuntimeConfig } from '../types'
55
import { getWorkerState, resetModules, waitForImportsToResolve } from '../utils'
66
import type { MockFactoryWithHelper } from '../types/mocker'
7+
import { createSimpleStackTrace } from '../utils/error'
78
import { FakeTimers } from './mock/timers'
89
import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy'
910
import { fn, isMockFunction, spies, spyOn } from './spy'
@@ -110,9 +111,10 @@ class VitestUtils {
110111
fn = fn
111112

112113
private getImporter() {
113-
const err = new Error('mock')
114-
const [,, importer] = parseStacktrace(err, true)
115-
return importer.file
114+
const stackTrace = createSimpleStackTrace({ stackTraceLimit: 4 })
115+
const importerStack = stackTrace.split('\n')[4]
116+
const stack = parseSingleStack(importerStack)
117+
return stack?.file || ''
116118
}
117119

118120
/**

‎packages/vitest/src/utils/error.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
interface ErrorOptions {
2+
message?: string
3+
stackTraceLimit?: number
4+
}
5+
6+
/**
7+
* Get original stacktrace without source map support the most performant way.
8+
* - Create only 1 stack frame.
9+
* - Rewrite prepareStackTrace to bypass "support-stack-trace" (usually takes ~250ms).
10+
*/
11+
export function createSimpleStackTrace(options?: ErrorOptions) {
12+
const { message = 'error', stackTraceLimit = 1 } = options || {}
13+
const limit = Error.stackTraceLimit
14+
const prepareStackTrace = Error.prepareStackTrace
15+
Error.stackTraceLimit = stackTraceLimit
16+
Error.prepareStackTrace = e => e.stack
17+
const err = new Error(message)
18+
const stackTrace = err.stack || ''
19+
Error.prepareStackTrace = prepareStackTrace
20+
Error.stackTraceLimit = limit
21+
return stackTrace
22+
}

‎packages/vitest/src/utils/source-map.ts

+49-43
Original file line numberDiff line numberDiff line change
@@ -27,63 +27,69 @@ function extractLocation(urlLike: string) {
2727
return [parts[1], parts[2] || undefined, parts[3] || undefined]
2828
}
2929

30-
export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] {
31-
if (!e)
32-
return []
30+
// Based on https://github.com/stacktracejs/error-stack-parser
31+
// Credit to stacktracejs
32+
export function parseSingleStack(raw: string): ParsedStack | null {
33+
let line = raw.trim()
3334

34-
if (e.stacks)
35-
return e.stacks
35+
if (line.includes('(eval '))
36+
line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '')
3637

37-
const stackStr = e.stack || e.stackStr || ''
38-
const stackFrames = stackStr
39-
.split('\n')
40-
// Based on https://github.com/stacktracejs/error-stack-parser
41-
// Credit to stacktracejs
42-
.map((raw): ParsedStack | null => {
43-
let line = raw.trim()
38+
let sanitizedLine = line
39+
.replace(/^\s+/, '')
40+
.replace(/\(eval code/g, '(')
41+
.replace(/^.*?\s+/, '')
4442

45-
if (line.includes('(eval '))
46-
line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '')
43+
// capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in
44+
// case it has spaces in it, as the string is split on \s+ later on
45+
const location = sanitizedLine.match(/ (\(.+\)$)/)
4746

48-
let sanitizedLine = line
49-
.replace(/^\s+/, '')
50-
.replace(/\(eval code/g, '(')
51-
.replace(/^.*?\s+/, '')
47+
// remove the parenthesized location from the line, if it was matched
48+
sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine
5249

53-
// capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in
54-
// case it has spaces in it, as the string is split on \s+ later on
55-
const location = sanitizedLine.match(/ (\(.+\)$)/)
50+
// if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine
51+
// because this line doesn't have function name
52+
const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine)
53+
let method = (location && sanitizedLine) || ''
54+
let file = url && ['eval', '<anonymous>'].includes(url) ? undefined : url
5655

57-
// remove the parenthesized location from the line, if it was matched
58-
sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine
56+
if (!file || !lineNumber || !columnNumber)
57+
return null
5958

60-
// if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine
61-
// because this line doesn't have function name
62-
const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine)
63-
let method = (location && sanitizedLine) || ''
64-
let file = url && ['eval', '<anonymous>'].includes(url) ? undefined : url
59+
if (method.startsWith('async '))
60+
method = method.slice(6)
6561

66-
if (!file || !lineNumber || !columnNumber)
67-
return null
62+
if (file.startsWith('file://'))
63+
file = file.slice(7)
6864

69-
if (method.startsWith('async '))
70-
method = method.slice(6)
65+
// normalize Windows path (\ -> /)
66+
file = resolve(file)
7167

72-
if (file.startsWith('file://'))
73-
file = file.slice(7)
68+
return {
69+
method,
70+
file,
71+
line: parseInt(lineNumber),
72+
column: parseInt(columnNumber),
73+
}
74+
}
7475

75-
// normalize Windows path (\ -> /)
76-
file = resolve(file)
76+
export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] {
77+
if (!e)
78+
return []
79+
80+
if (e.stacks)
81+
return e.stacks
82+
83+
const stackStr = e.stack || e.stackStr || ''
84+
const stackFrames = stackStr
85+
.split('\n')
86+
.map((raw): ParsedStack | null => {
87+
const stack = parseSingleStack(raw)
7788

78-
if (!full && stackIgnorePatterns.some(p => file && file.includes(p)))
89+
if (!stack || (!full && stackIgnorePatterns.some(p => stack.file.includes(p))))
7990
return null
8091

81-
return {
82-
method,
83-
file,
84-
line: parseInt(lineNumber),
85-
column: parseInt(columnNumber),
86-
}
92+
return stack
8793
})
8894
.filter(notNullish)
8995

0 commit comments

Comments
 (0)
Please sign in to comment.