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: cached fs utils with shared trees #15294

Closed
wants to merge 26 commits into from
Closed
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d666ab
perf: cached fs utils
patak-dev Dec 7, 2023
aac3690
chore: merge main
patak-dev Dec 7, 2023
524a52a
fix: temporal guard for custom watchers
patak-dev Dec 7, 2023
c929f96
feat: add experimental server.fs.cacheChecks
patak-dev Dec 7, 2023
2fbf00a
feat: also enable fsUtils in createResolver
patak-dev Dec 7, 2023
2f25f9e
chore: update types
patak-dev Dec 7, 2023
a343c66
feat: optimize on add file or directory
patak-dev Dec 7, 2023
89df6d5
feat: optimize tryResolveRealFileWithExtensions
patak-dev Dec 8, 2023
d3d45fc
feat: avoid double normalizePath calls
patak-dev Dec 8, 2023
8ea355e
chore: update
patak-dev Dec 8, 2023
df42d68
chore: remove watch null and custom ignored guard
patak-dev Dec 8, 2023
93572c8
chore: lint
patak-dev Dec 8, 2023
c3d504b
chore: merge main
patak-dev Dec 8, 2023
cfb8693
feat: shared dirent caches
patak-dev Dec 8, 2023
42f689f
fix: only register valid active configs
patak-dev Dec 8, 2023
cc9c8e9
chore: merge main
patak-dev Jan 25, 2024
0104871
chore: update
patak-dev Jan 25, 2024
ab5bc15
chore: update
patak-dev Jan 25, 2024
93d0815
chore: merge main
patak-dev Jan 26, 2024
4b37e14
chore: merge main
patak-dev Jan 26, 2024
1ba78ab
fix: connectRootCacheToActiveRoots
patak-dev Jan 26, 2024
9aef77d
fix: use workspace root
patak-dev Jan 26, 2024
37dcc60
chore: simplify
patak-dev Jan 27, 2024
b3768f1
chore: test
patak-dev Jan 27, 2024
4c409f1
chore: enable back
patak-dev Jan 27, 2024
e11de80
fix: add file to cache servers sharing a root
patak-dev Jan 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 116 additions & 1 deletion packages/vite/src/node/fsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import path from 'node:path'
import type { FSWatcher } from 'dep-types/chokidar'
import type { ResolvedConfig } from './config'
import {
createDebugger,
isInNodeModules,
normalizePath,
safeRealpathSync,
tryStatSync,
} from './utils'
import { searchForWorkspaceRoot } from './server/searchRoot'

const debug = createDebugger('vite:fs')

export interface FsUtils {
existsSync: (path: string) => boolean
isDirectory: (path: string) => boolean
Expand Down Expand Up @@ -41,10 +44,22 @@ export const commonFsUtils: FsUtils = {
tryResolveRealFileOrType,
}

const activeResolvedConfigs = new Array<WeakRef<ResolvedConfig>>()
const registry = new FinalizationRegistry((fsUtils: FsUtils) => {
debug?.(`removing config`)
const i = activeResolvedConfigs.findIndex((r) => !r.deref())
activeResolvedConfigs.splice(i, 1)
})
function addActiveResolvedConfig(config: ResolvedConfig, fsUtils: FsUtils) {
activeResolvedConfigs.push(new WeakRef(config))
registry.register(config, fsUtils)
}

const cachedFsUtilsMap = new WeakMap<ResolvedConfig, FsUtils>()
export function getFsUtils(config: ResolvedConfig): FsUtils {
let fsUtils = cachedFsUtilsMap.get(config)
if (!fsUtils) {
debug?.(`resolving FsUtils for ${config.root}`)
if (config.command !== 'serve' || !config.server.fs.cachedChecks) {
// cached fsUtils is only used in the dev server for now, and only when the watcher isn't configured
// we can support custom ignored patterns later
Expand All @@ -56,6 +71,7 @@ export function getFsUtils(config: ResolvedConfig): FsUtils {
fsUtils = commonFsUtils
} else {
fsUtils = createCachedFsUtils(config)
addActiveResolvedConfig(config, fsUtils)
}
cachedFsUtilsMap.set(config, fsUtils)
}
Expand Down Expand Up @@ -118,6 +134,99 @@ function ensureFileMaybeSymlinkIsResolved(
isSymlink === undefined ? 'error' : isSymlink ? 'symlink' : 'file'
}

interface CachedFsUtilsMeta {
root: string
rootCache: DirentCache
}
const cachedFsUtilsMeta = new WeakMap<ResolvedConfig, CachedFsUtilsMeta>()

function expandUntilOtherRoot(
rootCache: DirentCache,
root: string,
otherRoot: string,
) {
// Start a parent Tree, and expand it to reach the otherRoot
if (!rootCache.dirents) {
rootCache.dirents = readDirCacheSync(root)
}
if (!rootCache.dirents) {
return
}
const parts = otherRoot.slice(root.length + 1).split('/')
const lastPart = parts.pop()!
let currentDirPath = root
let currentDirentCache = rootCache
while (parts.length) {
const nextDirentCache = (currentDirentCache.dirents as DirentsMap).get(
parts[0],
)
if (!nextDirentCache || nextDirentCache.type === 'file') {
return
}
if (nextDirentCache.type === 'symlink') {
// We don't support sharing trees with symlinks in the middle of the path
return
}
// We know it's a directory
currentDirPath += '/' + parts.shift()!
nextDirentCache.dirents = readDirCacheSync(currentDirPath)
if (!nextDirentCache.dirents) {
return
}
currentDirentCache = nextDirentCache
}
const lastDirents = currentDirentCache.dirents as DirentsMap
if (!lastDirents.has(lastPart)) {
return undefined
}
return { part: lastPart, dirents: lastDirents }
}

function findCompatibleRootCache(
config: ResolvedConfig,
): DirentCache | undefined {
const { root } = config
debug?.(`active configs: ${activeResolvedConfigs.length}`)
activeResolvedConfigs.forEach((otherConfigRef) => {
const otherConfig = otherConfigRef?.deref()
if (otherConfig) {
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
const otherRoot = otherConfig.root
const otherCachedFsUtilsMeta = cachedFsUtilsMeta.get(otherConfig)!
const otherRootCache = otherCachedFsUtilsMeta.rootCache
debug?.(
`Checking if ${root} can be connected to the cache for ${otherRoot}`,
)
if (otherRoot === root) {
debug?.(`FsUtils for ${root} sharing root cache with compatible cache`)
return otherRootCache
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
} else if (otherRoot.startsWith(root + '/')) {
const rootCache = { type: 'directory' } as DirentCache
const last = expandUntilOtherRoot(rootCache, root, otherRoot)
if (!last) {
return
}
last.dirents.set(last.part, otherRootCache)
debug?.(
`FsUtils for ${root} connected as a parent to the cache for ${otherRoot}`,
)
return rootCache
} else if (root.startsWith(otherRoot + '/')) {
const last = expandUntilOtherRoot(otherRootCache, otherRoot, root)
if (!last) {
return
}
debug?.(
`FsUtils for ${root} connected as a child to the cache for ${otherRoot}`,
)
return last.dirents.get(last.part)
}
}
})

debug?.(`FsUtils for ${root} started as an independent cache`)
return { type: 'directory' as DirentCacheType } // dirents will be computed lazily
}

function pathUntilPart(root: string, parts: string[], i: number): string {
let p = root
for (let k = 0; k < i; k++) p += '/' + parts[k]
Expand All @@ -127,7 +236,13 @@ function pathUntilPart(root: string, parts: string[], i: number): string {
export function createCachedFsUtils(config: ResolvedConfig): FsUtils {
const root = normalizePath(searchForWorkspaceRoot(config.root))
const rootDirPath = `${root}/`
const rootCache: DirentCache = { type: 'directory' } // dirents will be computed lazily

const rootCache = findCompatibleRootCache(config)
if (!rootCache) {
return commonFsUtils
}

cachedFsUtilsMeta.set(config, { root, rootCache })

const getDirentCacheSync = (parts: string[]): DirentCache | undefined => {
let direntCache: DirentCache = rootCache
Expand Down
Loading