Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: non-blocking write of optimized dep files #12603

Merged
merged 8 commits into from Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 93 additions & 32 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -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)
}
}
}
bluwy marked this conversation as resolved.
Show resolved Hide resolved

if (!force) {
let cachedMetadata: DepOptimizationMetadata | undefined
try {
Expand Down Expand Up @@ -587,50 +610,72 @@ 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<string>()
const files: Promise<void>[] = []
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<string>()
const files: Promise<void>[] = []
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(
path.resolve(depsCacheDir, 'package.json'),
'{\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)
bluwy marked this conversation as resolved.
Show resolved Hide resolved

// 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: () => {},
}
Expand Down Expand Up @@ -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<string> {
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')
}
2 changes: 1 addition & 1 deletion packages/vite/src/node/optimizer/optimizer.ts
Expand Up @@ -195,7 +195,7 @@ async function createDepsOptimizer(
const deps: Record<string, string> = {}
await addManuallyIncludedOptimizeDeps(deps, config, ssr)

const discovered = await toDiscoveredDependencies(
const discovered = toDiscoveredDependencies(
config,
deps,
ssr,
Expand Down
11 changes: 7 additions & 4 deletions 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'
Expand Down Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we stream .contents, or use them here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to make it a string here. For sourcemaps, since they're handled in the transform middleware, we could, but I tried that and it doesn't work since etags are generated via the Buffer instance, and Buffer.isBuffer(new Uint8Array(2)) is false.

} catch (e) {
// Outdated non-entry points (CHUNK), loaded after a rerun
throwOutdatedRequest(id)
Expand Down Expand Up @@ -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)
},
}
}
Expand Down
15 changes: 5 additions & 10 deletions packages/vite/src/node/server/index.ts
Expand Up @@ -341,10 +341,9 @@ export async function createServer(
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve')

// start optimizer in the background
let depsOptimizerReady: Promise<void> | undefined
if (isDepsOptimizerEnabled(config, false)) {
depsOptimizerReady = initDepsOptimizer(config)
// start optimizer in the background, we still need to await the setup
await initDepsOptimizer(config)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bluwy we need to await the optimizer init here. The setup is very lean and it starts the processing in the background so this shouldn't take more than a few ms. The issue was evident when I tried to implement the wait 500ms when there is a _writing file.

}

const { root, server: serverConfig } = config
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 2 additions & 3 deletions 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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
Expand Down