Skip to content

Commit 6b9a1b5

Browse files
authored
perf(experimental): if fsCacheModule is enabled, read from the memory when possible (#9076)
1 parent 332afa0 commit 6b9a1b5

File tree

2 files changed

+75
-57
lines changed

2 files changed

+75
-57
lines changed

packages/vitest/src/node/cache/fsModuleCache.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const cacheCommentLength = cacheComment.length
1919

2020
const METADATA_FILE = '_metadata.json'
2121

22+
const parallelFsCacheRead = new Map<string, Promise<{ code: string; meta: CachedInlineModuleMeta } | undefined>>()
23+
2224
/**
2325
* @experimental
2426
*/
@@ -68,28 +70,38 @@ export class FileSystemModuleCache {
6870
}
6971
}
7072

73+
private readCachedFileConcurrently(cachedFilePath: string) {
74+
if (!parallelFsCacheRead.has(cachedFilePath)) {
75+
parallelFsCacheRead.set(cachedFilePath, readFile(cachedFilePath, 'utf-8').then((code) => {
76+
const matchIndex = code.lastIndexOf(cacheComment)
77+
if (matchIndex === -1) {
78+
debugFs?.(`${c.red('[empty]')} ${cachedFilePath} exists, but doesn't have a ${cacheComment} comment, transforming by vite instead`)
79+
return
80+
}
81+
82+
return { code, meta: this.fromBase64(code.slice(matchIndex + cacheCommentLength)) }
83+
}).finally(() => {
84+
parallelFsCacheRead.delete(cachedFilePath)
85+
}))
86+
}
87+
return parallelFsCacheRead.get(cachedFilePath)!
88+
}
89+
7190
async getCachedModule(cachedFilePath: string): Promise<
7291
CachedInlineModuleMeta
73-
| Extract<FetchResult, { externalize: string }>
7492
| undefined
7593
> {
7694
if (!existsSync(cachedFilePath)) {
7795
debugFs?.(`${c.red('[empty]')} ${cachedFilePath} doesn't exist, transforming by vite instead`)
7896
return
7997
}
8098

81-
const code = await readFile(cachedFilePath, 'utf-8')
82-
const matchIndex = code.lastIndexOf(cacheComment)
83-
if (matchIndex === -1) {
84-
debugFs?.(`${c.red('[empty]')} ${cachedFilePath} exists, but doesn't have a ${cacheComment} comment, transforming by vite instead`)
99+
const fileResult = await this.readCachedFileConcurrently(cachedFilePath)
100+
if (!fileResult) {
85101
return
86102
}
103+
const { code, meta } = fileResult
87104

88-
const meta = this.fromBase64(code.slice(matchIndex + cacheCommentLength))
89-
if (meta.externalize) {
90-
debugFs?.(`${c.green('[read]')} ${meta.externalize} is externalized inside ${cachedFilePath}`)
91-
return { externalize: meta.externalize, type: meta.type }
92-
}
93105
debugFs?.(`${c.green('[read]')} ${meta.id} is cached in ${cachedFilePath}`)
94106

95107
return {
@@ -108,11 +120,7 @@ export class FileSystemModuleCache {
108120
importers: string[] = [],
109121
mappings: boolean = false,
110122
): Promise<void> {
111-
if ('externalize' in fetchResult) {
112-
debugFs?.(`${c.yellow('[write]')} ${fetchResult.externalize} is externalized inside ${cachedFilePath}`)
113-
await atomicWriteFile(cachedFilePath, `${cacheComment}${this.toBase64(fetchResult)}`)
114-
}
115-
else if ('code' in fetchResult) {
123+
if ('code' in fetchResult) {
116124
const result = {
117125
file: fetchResult.file,
118126
id: fetchResult.id,
@@ -169,8 +177,6 @@ export class FileSystemModuleCache {
169177
id: string,
170178
fileContent: string,
171179
): string | null {
172-
let hashString = ''
173-
174180
// bail out if file has import.meta.glob because it depends on other files
175181
// TODO: figure out a way to still support it
176182
if (fileContent.includes('import.meta.glob(')) {
@@ -179,6 +185,8 @@ export class FileSystemModuleCache {
179185
return null
180186
}
181187

188+
let hashString = ''
189+
182190
for (const generator of this.fsCacheKeyGenerators) {
183191
const result = generator({ environment, id, sourceCode: fileContent })
184192
if (typeof result === 'string') {
@@ -214,13 +222,6 @@ export class FileSystemModuleCache {
214222
environment: environment.name,
215223
// this affects Vitest CSS plugin
216224
css: vitestConfig.css,
217-
// this affect externalization
218-
resolver: {
219-
inline: resolver.options.inline,
220-
external: resolver.options.external,
221-
inlineFiles: resolver.options.inlineFiles,
222-
moduleDirectories: resolver.options.moduleDirectories,
223-
},
224225
},
225226
(_, value) => {
226227
if (typeof value === 'function' || value instanceof RegExp) {

packages/vitest/src/node/environments/fetchModule.ts

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class ModuleFetcher {
139139
private recordResult(trace: Span, result: FetchResult | FetchCachedFileSystemResult): void {
140140
if ('externalize' in result) {
141141
trace.setAttributes({
142-
'vitest.module.external': result.externalize,
142+
'vitest.fetched_module.external': result.externalize,
143143
'vitest.fetched_module.type': result.type,
144144
})
145145
}
@@ -213,44 +213,55 @@ class ModuleFetcher {
213213
environment: DevEnvironment,
214214
moduleGraphModule: EnvironmentModuleNode,
215215
): Promise<FetchResult | FetchCachedFileSystemResult | undefined> {
216-
const cachedModule = await this.fsCache.getCachedModule(cachePath)
217-
218-
if (cachedModule && 'code' in cachedModule) {
219-
// keep the module graph in sync
220-
if (!moduleGraphModule.transformResult) {
221-
let map: Rollup.SourceMap | null | { mappings: '' } = extractSourceMap(cachedModule.code)
222-
if (map && cachedModule.file) {
223-
map.file = cachedModule.file
224-
}
225-
// mappings is a special source map identifier in rollup
226-
if (!map && cachedModule.mappings) {
227-
map = { mappings: '' }
228-
}
229-
moduleGraphModule.transformResult = {
230-
code: cachedModule.code,
231-
map,
232-
ssr: true,
233-
}
234-
235-
// we populate the module graph to make the watch mode work because it relies on importers
236-
cachedModule.importers.forEach((importer) => {
237-
const environmentNode = environment.moduleGraph.getModuleById(importer)
238-
if (environmentNode) {
239-
moduleGraphModule.importers.add(environmentNode)
240-
}
241-
})
242-
}
216+
if (moduleGraphModule.transformResult?.__vitestTmp) {
243217
return {
244218
cached: true as const,
245-
file: cachedModule.file,
246-
id: cachedModule.id,
247-
tmp: cachePath,
248-
url: cachedModule.url,
219+
file: moduleGraphModule.file,
220+
id: moduleGraphModule.id!,
221+
tmp: moduleGraphModule.transformResult.__vitestTmp,
222+
url: moduleGraphModule.url,
249223
invalidate: false,
250224
}
251225
}
252226

253-
return cachedModule
227+
const cachedModule = await this.fsCache.getCachedModule(cachePath)
228+
229+
if (!cachedModule) {
230+
return
231+
}
232+
233+
// keep the module graph in sync
234+
let map: Rollup.SourceMap | null | { mappings: '' } = extractSourceMap(cachedModule.code)
235+
if (map && cachedModule.file) {
236+
map.file = cachedModule.file
237+
}
238+
// mappings is a special source map identifier in rollup
239+
if (!map && cachedModule.mappings) {
240+
map = { mappings: '' }
241+
}
242+
moduleGraphModule.transformResult = {
243+
code: cachedModule.code,
244+
map,
245+
ssr: true,
246+
__vitestTmp: cachePath,
247+
}
248+
249+
// we populate the module graph to make the watch mode work because it relies on importers
250+
cachedModule.importers.forEach((importer) => {
251+
const environmentNode = environment.moduleGraph.getModuleById(importer)
252+
if (environmentNode) {
253+
moduleGraphModule.importers.add(environmentNode)
254+
}
255+
})
256+
257+
return {
258+
cached: true as const,
259+
file: cachedModule.file,
260+
id: cachedModule.id,
261+
tmp: cachePath,
262+
url: cachedModule.url,
263+
invalidate: false,
264+
}
254265
}
255266

256267
private async fetchAndProcess(
@@ -485,3 +496,9 @@ export function handleRollupError(e: unknown): never {
485496
}
486497
throw e
487498
}
499+
500+
declare module 'vite' {
501+
export interface TransformResult {
502+
__vitestTmp?: string
503+
}
504+
}

0 commit comments

Comments
 (0)