From feb322a72aa1131137b7e78a69e4371f807ba377 Mon Sep 17 00:00:00 2001 From: patak Date: Sun, 26 Mar 2023 23:40:52 +0200 Subject: [PATCH 1/8] perf: non-blocking write of optimized dep files --- packages/vite/src/node/optimizer/index.ts | 125 +++++++++++++----- packages/vite/src/node/optimizer/optimizer.ts | 2 +- .../vite/src/node/plugins/optimizedDeps.ts | 11 +- packages/vite/src/node/server/index.ts | 15 +-- .../src/node/server/middlewares/transform.ts | 5 +- 5 files changed, 108 insertions(+), 50 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index ab5462191e9911..3f5cf6fad581fa 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -359,6 +359,29 @@ export async function loadCachedDepOptimizationMetadata( const depsCacheDir = getDepsCacheDir(config, ssr) + const writingPath = path.join(depsCacheDir, '_writing') + const maxWaitTime = 500 + const tryAgainMs = 100 + let waited = 0 + let files + while (fs.existsSync(writingPath)) { + files ??= await fsp.readdir(depsCacheDir) + await new Promise((r) => setTimeout(r, tryAgainMs)) + waited += tryAgainMs + if (waited >= maxWaitTime) { + const newFiles = await fsp.readdir(depsCacheDir) + if (files.length === newFiles.length) { + // no new files, outdated deps cache + config.logger.info('Outdated deps cache, forcing re-optimization...') + await fsp.rm(depsCacheDir, { recursive: true, force: true }) + return + } else { + // new files were saved, try again + return loadCachedDepOptimizationMetadata(config, ssr, force, asCommand) + } + } + } + if (!force) { let cachedMetadata: DepOptimizationMetadata | undefined try { @@ -587,30 +610,36 @@ export function runOptimizeDeps( `Dependencies bundled in ${(performance.now() - start).toFixed(2)}ms`, ) - return { - metadata, - async commit() { - // Write this run of pre-bundled dependencies to the deps cache - - // Get a list of old files in the deps directory to delete the stale ones - const oldFilesPaths: string[] = [] - if (!fs.existsSync(depsCacheDir)) { - fs.mkdirSync(depsCacheDir, { recursive: true }) - } else { - oldFilesPaths.push( - ...(await fsp.readdir(depsCacheDir)).map((f) => - path.join(depsCacheDir, f), - ), - ) - } + async function commitFiles() { + // Write this run of pre-bundled dependencies to the deps cache - const newFilesPaths = new Set() - const files: Promise[] = [] - const write = (filePath: string, content: string) => { - newFilesPaths.add(filePath) - files.push(fsp.writeFile(filePath, content)) - } + // Keep the output files in memory while we write them to disk in the + // background. These files are going to be sent right away to the browser + optimizedDepsCache.set(metadata, result.outputFiles!) + + // Get a list of old files in the deps directory to delete the stale ones + const oldFilesPaths: string[] = [] + if (!fs.existsSync(depsCacheDir)) { + fs.mkdirSync(depsCacheDir, { recursive: true }) + } else { + oldFilesPaths.push( + ...(await fsp.readdir(depsCacheDir)).map((f) => + path.join(depsCacheDir, f), + ), + ) + } + + const writingFilePath = path.resolve(depsCacheDir, '_writing') + await fsp.writeFile(writingFilePath, '') + const newFilesPaths = new Set() + const files: Promise[] = [] + const write = (filePath: string, content: string) => { + newFilesPaths.add(filePath) + files.push(fsp.writeFile(filePath, content)) + } + + path.join(depsCacheDir, '_metadata.json'), // a hint for Node.js // all files in the cache directory should be recognized as ES modules write( @@ -618,19 +647,35 @@ export function runOptimizeDeps( '{\n "type": "module"\n}\n', ) - write( - path.join(depsCacheDir, '_metadata.json'), - stringifyDepsOptimizerMetadata(metadata, depsCacheDir), - ) + write( + path.join(depsCacheDir, '_metadata.json'), + stringifyDepsOptimizerMetadata(metadata, depsCacheDir), + ) - for (const outputFile of result.outputFiles!) - write(outputFile.path, outputFile.text) + for (const outputFile of result.outputFiles!) + write(outputFile.path, outputFile.text) - // Clean up old files in the background - for (const filePath of oldFilesPaths) - if (!newFilesPaths.has(filePath)) fs.unlink(filePath, () => {}) // ignore errors + // Clean up old files in the background + for (const filePath of oldFilesPaths) + if (!newFilesPaths.has(filePath)) fs.unlink(filePath, () => {}) // ignore errors + + await Promise.all(files) + + // Successful write + fsp.unlink(writingFilePath) + + setTimeout(() => { + // Free up memory, these files aren't going to be re-requested because + // the requests are cached. If they do, then let them read from disk. + optimizedDepsCache.delete(metadata) + }, 5000) + } - await Promise.all(files) + return { + metadata, + async commit() { + // No need to wait, files are written in the background + commitFiles() }, cancel: () => {}, } @@ -1291,3 +1336,19 @@ export async function optimizedDepNeedsInterop( } return depInfo?.needsInterop } + +const optimizedDepsCache = new WeakMap< + DepOptimizationMetadata, + esbuild.OutputFile[] +>() +export async function loadOptimizedDep( + file: string, + depsOptimizer: DepsOptimizer, +): Promise { + const outputFiles = optimizedDepsCache.get(depsOptimizer.metadata) + if (outputFiles) { + const outputFile = outputFiles.find((o) => normalizePath(o.path) === file) + if (outputFile) return outputFile.text + } + return fsp.readFile(file, 'utf-8') +} diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 0f76e12b49c412..3f3550dfa5f99c 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -195,7 +195,7 @@ async function createDepsOptimizer( const deps: Record = {} await addManuallyIncludedOptimizeDeps(deps, config, ssr) - const discovered = await toDiscoveredDependencies( + const discovered = toDiscoveredDependencies( config, deps, ssr, diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 2ede063462b6cd..134b8bf313db56 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -1,10 +1,13 @@ -import { promises as fs } from 'node:fs' import colors from 'picocolors' import type { ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { DEP_VERSION_RE } from '../constants' import { cleanUrl, createDebugger } from '../utils' -import { getDepsOptimizer, optimizedDepInfoFromFile } from '../optimizer' +import { + getDepsOptimizer, + loadOptimizedDep, + optimizedDepInfoFromFile, +} from '../optimizer' export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = 'ERR_OPTIMIZE_DEPS_PROCESSING_ERROR' @@ -67,7 +70,7 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { // load hooks to avoid race conditions, once processing is resolved, // we are sure that the file has been properly save to disk try { - return await fs.readFile(file, 'utf-8') + return loadOptimizedDep(file, depsOptimizer) } catch (e) { // Outdated non-entry points (CHUNK), loaded after a rerun throwOutdatedRequest(id) @@ -128,7 +131,7 @@ export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin { // load hooks to avoid race conditions, once processing is resolved, // we are sure that the file has been properly save to disk - return await fs.readFile(file, 'utf-8') + return loadOptimizedDep(file, depsOptimizer) }, } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index acef17fdc7b29d..463ee712cbbc0d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -341,10 +341,9 @@ export async function createServer( ): Promise { const config = await resolveConfig(inlineConfig, 'serve') - // start optimizer in the background - let depsOptimizerReady: Promise | undefined if (isDepsOptimizerEnabled(config, false)) { - depsOptimizerReady = initDepsOptimizer(config) + // start optimizer in the background, we still need to await the setup + await initDepsOptimizer(config) } const { root, server: serverConfig } = config @@ -665,13 +664,9 @@ export async function createServer( // when the optimizer is ready, hook server so that it can reload the page // or invalidate the module graph when needed - if (depsOptimizerReady) { - depsOptimizerReady.then(() => { - const depsOptimizer = getDepsOptimizer(config) - if (depsOptimizer) { - depsOptimizer.server = server - } - }) + const depsOptimizer = getDepsOptimizer(config) + if (depsOptimizer) { + depsOptimizer.server = server } if (!middlewareMode && httpServer) { diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 25b3843ad021c5..28923c0307f5df 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -1,4 +1,3 @@ -import { promises as fs } from 'node:fs' import path from 'node:path' import type { Connect } from 'dep-types/connect' import colors from 'picocolors' @@ -34,7 +33,7 @@ import { ERR_OPTIMIZE_DEPS_PROCESSING_ERROR, ERR_OUTDATED_OPTIMIZED_DEP, } from '../../plugins/optimizedDeps' -import { getDepsOptimizer } from '../../optimizer' +import { getDepsOptimizer, loadOptimizedDep } from '../../optimizer' const debugCache = createDebugger('vite:cache') const isDebug = !!process.env.DEBUG @@ -81,7 +80,7 @@ export function transformMiddleware( ensureVolumeInPath(path.resolve(root, url.slice(1))), ) try { - const map = await fs.readFile(mapFile, 'utf-8') + const map = await loadOptimizedDep(mapFile, depsOptimizer) return send(req, res, map, 'json', { headers: server.config.server.headers, }) From b111f5f51c0adbdbe1930e8ddd40aa95800bb044 Mon Sep 17 00:00:00 2001 From: patak Date: Sun, 26 Mar 2023 23:55:59 +0200 Subject: [PATCH 2/8] perf: avoid expensive lookup --- packages/vite/src/node/optimizer/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 3f5cf6fad581fa..6fc7bfffc595ad 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -615,7 +615,12 @@ export function runOptimizeDeps( // Keep the output files in memory while we write them to disk in the // background. These files are going to be sent right away to the browser - optimizedDepsCache.set(metadata, result.outputFiles!) + optimizedDepsCache.set( + metadata, + new Map( + result.outputFiles!.map((f) => [normalizePath(f.path), f.text]), + ), + ) // Get a list of old files in the deps directory to delete the stale ones const oldFilesPaths: string[] = [] @@ -1339,7 +1344,7 @@ export async function optimizedDepNeedsInterop( const optimizedDepsCache = new WeakMap< DepOptimizationMetadata, - esbuild.OutputFile[] + Map >() export async function loadOptimizedDep( file: string, @@ -1347,8 +1352,8 @@ export async function loadOptimizedDep( ): Promise { const outputFiles = optimizedDepsCache.get(depsOptimizer.metadata) if (outputFiles) { - const outputFile = outputFiles.find((o) => normalizePath(o.path) === file) - if (outputFile) return outputFile.text + const outputFile = outputFiles.get(file) + if (outputFile) return outputFile } return fsp.readFile(file, 'utf-8') } From b995b680b08661169e6ce76daa1401590357986a Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Mar 2023 14:08:29 +0800 Subject: [PATCH 3/8] refactor: write esbuild output with buffer --- packages/vite/src/node/optimizer/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 6fc7bfffc595ad..d93916cc138d21 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -639,7 +639,7 @@ export function runOptimizeDeps( const newFilesPaths = new Set() const files: Promise[] = [] - const write = (filePath: string, content: string) => { + const write = (filePath: string, content: string | Uint8Array) => { newFilesPaths.add(filePath) files.push(fsp.writeFile(filePath, content)) } @@ -658,7 +658,7 @@ export function runOptimizeDeps( ) for (const outputFile of result.outputFiles!) - write(outputFile.path, outputFile.text) + write(outputFile.path, outputFile.contents) // Clean up old files in the background for (const filePath of oldFilesPaths) From b7bde1281be63901c6739a76921b98bd6682414d Mon Sep 17 00:00:00 2001 From: patak Date: Mon, 27 Mar 2023 09:06:13 +0200 Subject: [PATCH 4/8] chore: setTimeout for commitFiles --- packages/vite/src/node/optimizer/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index d93916cc138d21..2db8faa1cbcfd1 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -613,15 +613,6 @@ export function runOptimizeDeps( async function commitFiles() { // Write this run of pre-bundled dependencies to the deps cache - // Keep the output files in memory while we write them to disk in the - // background. These files are going to be sent right away to the browser - optimizedDepsCache.set( - metadata, - new Map( - result.outputFiles!.map((f) => [normalizePath(f.path), f.text]), - ), - ) - // Get a list of old files in the deps directory to delete the stale ones const oldFilesPaths: string[] = [] if (!fs.existsSync(depsCacheDir)) { @@ -679,8 +670,17 @@ export function runOptimizeDeps( return { metadata, async commit() { + // Keep the output files in memory while we write them to disk in the + // background. These files are going to be sent right away to the browser + optimizedDepsCache.set( + metadata, + new Map( + result.outputFiles!.map((f) => [normalizePath(f.path), f.text]), + ), + ) + // No need to wait, files are written in the background - commitFiles() + setTimeout(commitFiles, 0) }, cancel: () => {}, } From cc9175fea01231038e506fb156d912071dde82e6 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Mar 2023 19:53:34 +0800 Subject: [PATCH 5/8] refactor: use function --- packages/vite/src/node/optimizer/index.ts | 73 ++++++++++++++++------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 2db8faa1cbcfd1..a9b566034c84af 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -27,6 +27,7 @@ import { transformWithEsbuild } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET } from '../constants' import { resolvePackageData } from '../packages' import type { ViteDevServer } from '../server' +import type { Logger } from '../logger' import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' import { scanImports } from './scan' export { @@ -359,27 +360,12 @@ export async function loadCachedDepOptimizationMetadata( const depsCacheDir = getDepsCacheDir(config, ssr) - const writingPath = path.join(depsCacheDir, '_writing') - const maxWaitTime = 500 - const tryAgainMs = 100 - let waited = 0 - let files - while (fs.existsSync(writingPath)) { - files ??= await fsp.readdir(depsCacheDir) - await new Promise((r) => setTimeout(r, tryAgainMs)) - waited += tryAgainMs - if (waited >= maxWaitTime) { - const newFiles = await fsp.readdir(depsCacheDir) - if (files.length === newFiles.length) { - // no new files, outdated deps cache - config.logger.info('Outdated deps cache, forcing re-optimization...') - await fsp.rm(depsCacheDir, { recursive: true, force: true }) - return - } else { - // new files were saved, try again - return loadCachedDepOptimizationMetadata(config, ssr, force, asCommand) - } - } + const unlockSuccess = await waitOptimizerWriteLock( + depsCacheDir, + config.logger, + ) + if (!unlockSuccess) { + return } if (!force) { @@ -1357,3 +1343,48 @@ export async function loadOptimizedDep( } return fsp.readFile(file, 'utf-8') } + +/** + * Processes that write to the deps cache directory adds a `_writing` lock to + * inform other processes of so. So before doing any work on it, they can wait + * for the file to be removed to know it's ready. + * + * Returns true if successfully waited for unlock, false if lock timed out. + */ +async function waitOptimizerWriteLock(depsCacheDir: string, logger: Logger) { + const writingPath = path.join(depsCacheDir, '_writing') + const tryAgainMs = 100 + + // if _writing exist, we wait for a maximum of 500ms before assuming something + // is not right + let maxWaitTime = 500 + let waited = 0 + let filesLength: number + + while (fs.existsSync(writingPath)) { + // on the first run, we check the number of files it started with for later use + filesLength ??= (await fsp.readdir(depsCacheDir)).length + + await new Promise((r) => setTimeout(r, tryAgainMs)) + waited += tryAgainMs + + if (waited >= maxWaitTime) { + const newFilesLength = (await fsp.readdir(depsCacheDir)).length + + // after 500ms, if the number of files is the same, assume previous process + // terminated and didn't cleanup `_writing` lock. clear the directory. + if (filesLength === newFilesLength) { + logger.info('Outdated deps cache, forcing re-optimization...') + await fsp.rm(depsCacheDir, { recursive: true, force: true }) + return false + } + // new files were saved, wait a bit longer to decide again. + else { + maxWaitTime += 500 + filesLength = newFilesLength + } + } + } + + return true +} From f4c7de5013e2d8272489ee9998603739f1bf2b80 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Mar 2023 20:04:15 +0800 Subject: [PATCH 6/8] chore: handle lock before writing --- packages/vite/src/node/optimizer/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index a9b566034c84af..e2803f3f6c8390 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -596,14 +596,21 @@ export function runOptimizeDeps( `Dependencies bundled in ${(performance.now() - start).toFixed(2)}ms`, ) + // Write this run of pre-bundled dependencies to the deps cache async function commitFiles() { - // Write this run of pre-bundled dependencies to the deps cache - // Get a list of old files in the deps directory to delete the stale ones const oldFilesPaths: string[] = [] - if (!fs.existsSync(depsCacheDir)) { + // File used to tell other processes that we're writing the deps cache directory + const writingFilePath = path.resolve(depsCacheDir, '_writing') + + if ( + !fs.existsSync(depsCacheDir) || + !(await waitOptimizerWriteLock(depsCacheDir, config.logger)) // unlock timed out + ) { fs.mkdirSync(depsCacheDir, { recursive: true }) + fs.writeFileSync(writingFilePath, '') } else { + fs.writeFileSync(writingFilePath, '') oldFilesPaths.push( ...(await fsp.readdir(depsCacheDir)).map((f) => path.join(depsCacheDir, f), @@ -611,9 +618,6 @@ export function runOptimizeDeps( ) } - const writingFilePath = path.resolve(depsCacheDir, '_writing') - await fsp.writeFile(writingFilePath, '') - const newFilesPaths = new Set() const files: Promise[] = [] const write = (filePath: string, content: string | Uint8Array) => { From c6c82ac9d5557d013a7505245d5caffec79ca2fc Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Mar 2023 20:06:10 +0800 Subject: [PATCH 7/8] refactor: improve code --- packages/vite/src/node/optimizer/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index e2803f3f6c8390..96ffe1dc998058 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -360,11 +360,8 @@ export async function loadCachedDepOptimizationMetadata( const depsCacheDir = getDepsCacheDir(config, ssr) - const unlockSuccess = await waitOptimizerWriteLock( - depsCacheDir, - config.logger, - ) - if (!unlockSuccess) { + // If the lock timed out, we cancel and return undefined + if (!(await waitOptimizerWriteLock(depsCacheDir, config.logger))) { return } From 0ec95e4d8b3f7fe701c3c2abb59ba659ec1ddb75 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 27 Mar 2023 20:23:25 +0800 Subject: [PATCH 8/8] chore: fix test --- packages/vite/src/node/optimizer/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 96ffe1dc998058..6b3441cd5d027d 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -616,6 +616,7 @@ export function runOptimizeDeps( } const newFilesPaths = new Set() + newFilesPaths.add(writingFilePath) const files: Promise[] = [] const write = (filePath: string, content: string | Uint8Array) => { newFilesPaths.add(filePath)