Skip to content

Commit 2f5f968

Browse files
patak-devbluwy
andauthoredMar 27, 2023
perf: non-blocking write of optimized dep files (#12603)
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
1 parent c881971 commit 2f5f968

File tree

5 files changed

+146
-50
lines changed

5 files changed

+146
-50
lines changed
 

‎packages/vite/src/node/optimizer/index.ts

+131-32
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { transformWithEsbuild } from '../plugins/esbuild'
2727
import { ESBUILD_MODULES_TARGET } from '../constants'
2828
import { resolvePackageData } from '../packages'
2929
import type { ViteDevServer } from '../server'
30+
import type { Logger } from '../logger'
3031
import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin'
3132
import { scanImports } from './scan'
3233
export {
@@ -359,6 +360,11 @@ export async function loadCachedDepOptimizationMetadata(
359360

360361
const depsCacheDir = getDepsCacheDir(config, ssr)
361362

363+
// If the lock timed out, we cancel and return undefined
364+
if (!(await waitOptimizerWriteLock(depsCacheDir, config.logger))) {
365+
return
366+
}
367+
362368
if (!force) {
363369
let cachedMetadata: DepOptimizationMetadata | undefined
364370
try {
@@ -587,50 +593,82 @@ export function runOptimizeDeps(
587593
`Dependencies bundled in ${(performance.now() - start).toFixed(2)}ms`,
588594
)
589595

590-
return {
591-
metadata,
592-
async commit() {
593-
// Write this run of pre-bundled dependencies to the deps cache
594-
595-
// Get a list of old files in the deps directory to delete the stale ones
596-
const oldFilesPaths: string[] = []
597-
if (!fs.existsSync(depsCacheDir)) {
598-
fs.mkdirSync(depsCacheDir, { recursive: true })
599-
} else {
600-
oldFilesPaths.push(
601-
...(await fsp.readdir(depsCacheDir)).map((f) =>
602-
path.join(depsCacheDir, f),
603-
),
604-
)
605-
}
596+
// Write this run of pre-bundled dependencies to the deps cache
597+
async function commitFiles() {
598+
// Get a list of old files in the deps directory to delete the stale ones
599+
const oldFilesPaths: string[] = []
600+
// File used to tell other processes that we're writing the deps cache directory
601+
const writingFilePath = path.resolve(depsCacheDir, '_writing')
602+
603+
if (
604+
!fs.existsSync(depsCacheDir) ||
605+
!(await waitOptimizerWriteLock(depsCacheDir, config.logger)) // unlock timed out
606+
) {
607+
fs.mkdirSync(depsCacheDir, { recursive: true })
608+
fs.writeFileSync(writingFilePath, '')
609+
} else {
610+
fs.writeFileSync(writingFilePath, '')
611+
oldFilesPaths.push(
612+
...(await fsp.readdir(depsCacheDir)).map((f) =>
613+
path.join(depsCacheDir, f),
614+
),
615+
)
616+
}
606617

607-
const newFilesPaths = new Set<string>()
608-
const files: Promise<void>[] = []
609-
const write = (filePath: string, content: string) => {
610-
newFilesPaths.add(filePath)
611-
files.push(fsp.writeFile(filePath, content))
612-
}
618+
const newFilesPaths = new Set<string>()
619+
newFilesPaths.add(writingFilePath)
620+
const files: Promise<void>[] = []
621+
const write = (filePath: string, content: string | Uint8Array) => {
622+
newFilesPaths.add(filePath)
623+
files.push(fsp.writeFile(filePath, content))
624+
}
613625

626+
path.join(depsCacheDir, '_metadata.json'),
614627
// a hint for Node.js
615628
// all files in the cache directory should be recognized as ES modules
616629
write(
617630
path.resolve(depsCacheDir, 'package.json'),
618631
'{\n "type": "module"\n}\n',
619632
)
620633

621-
write(
622-
path.join(depsCacheDir, '_metadata.json'),
623-
stringifyDepsOptimizerMetadata(metadata, depsCacheDir),
624-
)
634+
write(
635+
path.join(depsCacheDir, '_metadata.json'),
636+
stringifyDepsOptimizerMetadata(metadata, depsCacheDir),
637+
)
625638

626-
for (const outputFile of result.outputFiles!)
627-
write(outputFile.path, outputFile.text)
639+
for (const outputFile of result.outputFiles!)
640+
write(outputFile.path, outputFile.contents)
628641

629-
// Clean up old files in the background
630-
for (const filePath of oldFilesPaths)
631-
if (!newFilesPaths.has(filePath)) fs.unlink(filePath, () => {}) // ignore errors
642+
// Clean up old files in the background
643+
for (const filePath of oldFilesPaths)
644+
if (!newFilesPaths.has(filePath)) fs.unlink(filePath, () => {}) // ignore errors
645+
646+
await Promise.all(files)
647+
648+
// Successful write
649+
fsp.unlink(writingFilePath)
650+
651+
setTimeout(() => {
652+
// Free up memory, these files aren't going to be re-requested because
653+
// the requests are cached. If they do, then let them read from disk.
654+
optimizedDepsCache.delete(metadata)
655+
}, 5000)
656+
}
632657

633-
await Promise.all(files)
658+
return {
659+
metadata,
660+
async commit() {
661+
// Keep the output files in memory while we write them to disk in the
662+
// background. These files are going to be sent right away to the browser
663+
optimizedDepsCache.set(
664+
metadata,
665+
new Map(
666+
result.outputFiles!.map((f) => [normalizePath(f.path), f.text]),
667+
),
668+
)
669+
670+
// No need to wait, files are written in the background
671+
setTimeout(commitFiles, 0)
634672
},
635673
cancel: () => {},
636674
}
@@ -1291,3 +1329,64 @@ export async function optimizedDepNeedsInterop(
12911329
}
12921330
return depInfo?.needsInterop
12931331
}
1332+
1333+
const optimizedDepsCache = new WeakMap<
1334+
DepOptimizationMetadata,
1335+
Map<string, string>
1336+
>()
1337+
export async function loadOptimizedDep(
1338+
file: string,
1339+
depsOptimizer: DepsOptimizer,
1340+
): Promise<string> {
1341+
const outputFiles = optimizedDepsCache.get(depsOptimizer.metadata)
1342+
if (outputFiles) {
1343+
const outputFile = outputFiles.get(file)
1344+
if (outputFile) return outputFile
1345+
}
1346+
return fsp.readFile(file, 'utf-8')
1347+
}
1348+
1349+
/**
1350+
* Processes that write to the deps cache directory adds a `_writing` lock to
1351+
* inform other processes of so. So before doing any work on it, they can wait
1352+
* for the file to be removed to know it's ready.
1353+
*
1354+
* Returns true if successfully waited for unlock, false if lock timed out.
1355+
*/
1356+
async function waitOptimizerWriteLock(depsCacheDir: string, logger: Logger) {
1357+
const writingPath = path.join(depsCacheDir, '_writing')
1358+
const tryAgainMs = 100
1359+
1360+
// if _writing exist, we wait for a maximum of 500ms before assuming something
1361+
// is not right
1362+
let maxWaitTime = 500
1363+
let waited = 0
1364+
let filesLength: number
1365+
1366+
while (fs.existsSync(writingPath)) {
1367+
// on the first run, we check the number of files it started with for later use
1368+
filesLength ??= (await fsp.readdir(depsCacheDir)).length
1369+
1370+
await new Promise((r) => setTimeout(r, tryAgainMs))
1371+
waited += tryAgainMs
1372+
1373+
if (waited >= maxWaitTime) {
1374+
const newFilesLength = (await fsp.readdir(depsCacheDir)).length
1375+
1376+
// after 500ms, if the number of files is the same, assume previous process
1377+
// terminated and didn't cleanup `_writing` lock. clear the directory.
1378+
if (filesLength === newFilesLength) {
1379+
logger.info('Outdated deps cache, forcing re-optimization...')
1380+
await fsp.rm(depsCacheDir, { recursive: true, force: true })
1381+
return false
1382+
}
1383+
// new files were saved, wait a bit longer to decide again.
1384+
else {
1385+
maxWaitTime += 500
1386+
filesLength = newFilesLength
1387+
}
1388+
}
1389+
}
1390+
1391+
return true
1392+
}

‎packages/vite/src/node/optimizer/optimizer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ async function createDepsOptimizer(
195195
const deps: Record<string, string> = {}
196196
await addManuallyIncludedOptimizeDeps(deps, config, ssr)
197197

198-
const discovered = await toDiscoveredDependencies(
198+
const discovered = toDiscoveredDependencies(
199199
config,
200200
deps,
201201
ssr,

‎packages/vite/src/node/plugins/optimizedDeps.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { promises as fs } from 'node:fs'
21
import colors from 'picocolors'
32
import type { ResolvedConfig } from '..'
43
import type { Plugin } from '../plugin'
54
import { DEP_VERSION_RE } from '../constants'
65
import { cleanUrl, createDebugger } from '../utils'
7-
import { getDepsOptimizer, optimizedDepInfoFromFile } from '../optimizer'
6+
import {
7+
getDepsOptimizer,
8+
loadOptimizedDep,
9+
optimizedDepInfoFromFile,
10+
} from '../optimizer'
811

912
export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR =
1013
'ERR_OPTIMIZE_DEPS_PROCESSING_ERROR'
@@ -67,7 +70,7 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin {
6770
// load hooks to avoid race conditions, once processing is resolved,
6871
// we are sure that the file has been properly save to disk
6972
try {
70-
return await fs.readFile(file, 'utf-8')
73+
return loadOptimizedDep(file, depsOptimizer)
7174
} catch (e) {
7275
// Outdated non-entry points (CHUNK), loaded after a rerun
7376
throwOutdatedRequest(id)
@@ -128,7 +131,7 @@ export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin {
128131
// load hooks to avoid race conditions, once processing is resolved,
129132
// we are sure that the file has been properly save to disk
130133

131-
return await fs.readFile(file, 'utf-8')
134+
return loadOptimizedDep(file, depsOptimizer)
132135
},
133136
}
134137
}

‎packages/vite/src/node/server/index.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,9 @@ export async function createServer(
341341
): Promise<ViteDevServer> {
342342
const config = await resolveConfig(inlineConfig, 'serve')
343343

344-
// start optimizer in the background
345-
let depsOptimizerReady: Promise<void> | undefined
346344
if (isDepsOptimizerEnabled(config, false)) {
347-
depsOptimizerReady = initDepsOptimizer(config)
345+
// start optimizer in the background, we still need to await the setup
346+
await initDepsOptimizer(config)
348347
}
349348

350349
const { root, server: serverConfig } = config
@@ -665,13 +664,9 @@ export async function createServer(
665664

666665
// when the optimizer is ready, hook server so that it can reload the page
667666
// or invalidate the module graph when needed
668-
if (depsOptimizerReady) {
669-
depsOptimizerReady.then(() => {
670-
const depsOptimizer = getDepsOptimizer(config)
671-
if (depsOptimizer) {
672-
depsOptimizer.server = server
673-
}
674-
})
667+
const depsOptimizer = getDepsOptimizer(config)
668+
if (depsOptimizer) {
669+
depsOptimizer.server = server
675670
}
676671

677672
if (!middlewareMode && httpServer) {

‎packages/vite/src/node/server/middlewares/transform.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { promises as fs } from 'node:fs'
21
import path from 'node:path'
32
import type { Connect } from 'dep-types/connect'
43
import colors from 'picocolors'
@@ -34,7 +33,7 @@ import {
3433
ERR_OPTIMIZE_DEPS_PROCESSING_ERROR,
3534
ERR_OUTDATED_OPTIMIZED_DEP,
3635
} from '../../plugins/optimizedDeps'
37-
import { getDepsOptimizer } from '../../optimizer'
36+
import { getDepsOptimizer, loadOptimizedDep } from '../../optimizer'
3837

3938
const debugCache = createDebugger('vite:cache')
4039
const isDebug = !!process.env.DEBUG
@@ -81,7 +80,7 @@ export function transformMiddleware(
8180
ensureVolumeInPath(path.resolve(root, url.slice(1))),
8281
)
8382
try {
84-
const map = await fs.readFile(mapFile, 'utf-8')
83+
const map = await loadOptimizedDep(mapFile, depsOptimizer)
8584
return send(req, res, map, 'json', {
8685
headers: server.config.server.headers,
8786
})

0 commit comments

Comments
 (0)
Please sign in to comment.