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(hmr): implement soft invalidation #14654

Merged
merged 12 commits into from
Oct 25, 2023
20 changes: 19 additions & 1 deletion packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ export class ModuleNode {
ssrError: Error | null = null
lastHMRTimestamp = 0
lastInvalidationTimestamp = 0
/**
* If the module only needs to update its imports timestamp (e.g. within an HMR chain),
* it is considered soft-invalidated. In this state, its `transformResult` should exist,
* and the next `transformRequest` for this module will replace the timestamps.
* @internal
*/
softInvalidatedTransformResult: TransformResult | null = null
/**
* @internal
*/
softInvalidatedSsrTransformResult: TransformResult | null = null

/**
* @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870
Expand Down Expand Up @@ -131,6 +142,7 @@ export class ModuleGraph {
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = [],
softInvalidate = false,
): void {
if (seen.has(mod)) {
return
Expand All @@ -143,6 +155,10 @@ export class ModuleGraph {
// processing being done for this module
mod.lastInvalidationTimestamp = timestamp
}
if (softInvalidate) {
mod.softInvalidatedTransformResult = mod.transformResult
bluwy marked this conversation as resolved.
Show resolved Hide resolved
mod.softInvalidatedSsrTransformResult = mod.ssrTransformResult
}
// Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
// Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
mod.transformResult = null
Expand All @@ -160,7 +176,7 @@ export class ModuleGraph {
}
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
this.invalidateModule(importer, seen, timestamp, isHmr)
this.invalidateModule(importer, seen, timestamp, isHmr, undefined, true)
}
})
}
Expand Down Expand Up @@ -314,6 +330,8 @@ export class ModuleGraph {
else if (!this.urlToModuleMap.has(url)) {
this.urlToModuleMap.set(url, mod)
}
// if module exists before, reset its soft-invalidation state
mod.softInvalidated = null
bluwy marked this conversation as resolved.
Show resolved Hide resolved
this._setUnresolvedUrlToModule(rawUrl, mod, ssr)
return mod
})()
Expand Down
77 changes: 70 additions & 7 deletions packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import path from 'node:path'
import { performance } from 'node:perf_hooks'
import getEtag from 'etag'
import convertSourceMap from 'convert-source-map'
import MagicString from 'magic-string'
import { init, parse as parseImports } from 'es-module-lexer'
import type { PartialResolvedId, SourceDescription, SourceMap } from 'rollup'
import colors from 'picocolors'
import type { ModuleNode, ViteDevServer } from '..'
import {
blankReplacer,
cleanUrl,
// combineSourcemaps,
createDebugger,
ensureWatchedFile,
injectQuery,
isObject,
prettifyUrl,
removeImportQuery,
removeTimestampQuery,
timeFrom,
} from '../utils'
Expand All @@ -21,6 +26,7 @@ import { getDepsOptimizer } from '../optimizer'
import { applySourcemapIgnoreList, injectSourcesContent } from './sourcemap'
import { isFileServingAllowed } from './middlewares/static'
import { throwClosedServerError } from './pluginContainer'
// import { RawSourceMap } from '@ampproject/remapping'

export const ERR_LOAD_URL = 'ERR_LOAD_URL'
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
Expand Down Expand Up @@ -129,17 +135,18 @@ async function doTransform(

const module = await server.moduleGraph.getModuleByUrl(url, ssr)

// tries to handle soft invalidation of the module if available,
// returns a boolean true is successful, or false if no handling is needed
const softInvalidated =
module && (await handleModuleSoftInvalidation(module, ssr))

// check if we have a fresh cache
const cached =
module && (ssr ? module.ssrTransformResult : module.transformResult)
if (cached) {
// TODO: check if the module is "partially invalidated" - i.e. an import
// down the chain has been fully invalidated, but this current module's
// content has not changed.
// in this case, we can reuse its previous cached result and only update
// its import timestamps.

debugCache?.(`[memory] ${prettyUrl}`)
debugCache?.(
`${softInvalidated ? '[memory-hmr]' : '[memory]'} ${prettyUrl}`,
)
return cached
}

Expand Down Expand Up @@ -359,3 +366,59 @@ function createConvertSourceMapReadMap(originalFileName: string) {
)
}
}

async function handleModuleSoftInvalidation(mod: ModuleNode, ssr: boolean) {
const transformResult = ssr
? mod.softInvalidatedSsrTransformResult
: mod.softInvalidatedTransformResult
if (!transformResult) return false

if (ssr ? mod.ssrTransformResult : mod.transformResult) {
throw new Error(
'Internal server error: Soft-invalidated module should not have existing tranform result',
)
}

const importedModules = ssr ? mod.ssrImportedModules : mod.importedModules
const source = transformResult.code
const s = new MagicString(source)
await init
const [imports] = parseImports(source)

// TODO: Actually in SSR there's no import at all! We just need to decide whether to do a hard invalidation
// or not if there module watches another module that's not part of the import
for (const imp of imports) {
let rawUrl = source.slice(imp.s, imp.e)
if (rawUrl === 'import.meta') continue

const hasQuotes = rawUrl[0] === '"' || rawUrl[0] === "'"
if (hasQuotes) {
rawUrl = rawUrl.slice(1, -1)
}

const hmrUrl = removeImportQuery(removeTimestampQuery(rawUrl))
for (const importedMod of importedModules) {
if (importedMod.url !== hmrUrl) continue
if (importedMod.lastHMRTimestamp > 0) {
const replacedUrl = injectQuery(
removeTimestampQuery(rawUrl),
`t=${importedMod.lastHMRTimestamp}`,
)
const start = hasQuotes ? imp.s + 1 : imp.s
const end = hasQuotes ? imp.e - 1 : imp.e
s.overwrite(start, end, replacedUrl)
}
break
}
}

if (ssr) {
mod.ssrTransformResult = { ...transformResult, code: s.toString() }
mod.softInvalidatedSsrTransformResult = null
} else {
mod.transformResult = { ...transformResult, code: s.toString() }
mod.softInvalidatedTransformResult = null
}

return true
}
Loading