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: 17 additions & 3 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ 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.
*/
softInvalidated: boolean | null = null

/**
* @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870
Expand Down Expand Up @@ -131,6 +137,7 @@ export class ModuleGraph {
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = [],
softInvalidate = false,
): void {
if (seen.has(mod)) {
return
Expand All @@ -145,8 +152,13 @@ export class ModuleGraph {
}
// 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
mod.ssrTransformResult = null
if (softInvalidate && mod.softInvalidated !== false) {
bluwy marked this conversation as resolved.
Show resolved Hide resolved
mod.softInvalidated = true
} else {
mod.softInvalidated = false
mod.transformResult = null
mod.ssrTransformResult = null
}
mod.ssrModule = null
mod.ssrError = null
bluwy marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -160,7 +172,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 +326,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
70 changes: 65 additions & 5 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 @@ -133,11 +139,13 @@ async function doTransform(
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.
// if a module is soft-invalidated, use its previous cached result and update
// the import timestamps only
if (module.softInvalidated) {
module.softInvalidated = null
debugCache?.(`[memory-hmr] ${prettyUrl}`)
return await transformImportTimestamps(module, ssr)
}

debugCache?.(`[memory] ${prettyUrl}`)
return cached
Expand Down Expand Up @@ -359,3 +367,55 @@ function createConvertSourceMapReadMap(originalFileName: string) {
)
}
}

async function transformImportTimestamps(mod: ModuleNode, ssr: boolean) {
await init

// The callee should have transform result before calling this
const transformResult = ssr ? mod.ssrTransformResult! : mod.transformResult!
const importedModules = ssr ? mod.ssrImportedModules : mod.importedModules
const source = transformResult.code
const s = new MagicString(source)
const [imports] = parseImports(source)

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
}
}

transformResult.code = s.toString()

// TODO: is this sourcemap generation correct?
// const oldMap = transformResult.map
// const newMap = s.generateMap({ hires: 'boundary' })
// if (oldMap) {
// transformResult.map = combineSourcemaps(mod.id ?? mod.url, [
// newMap as RawSourceMap,
// oldMap as RawSourceMap,
// ]) as SourceMap
// } else {
// transformResult.map = newMap as SourceMap
// }

return transformResult
}
Loading