Skip to content

Commit ec5136c

Browse files
Fix perf regression when checking for changed content (#10234)
* Commit changes to mod time cache all at once This allows us to track changes in files that are both a context and content dependency in a way that preserves file mod checking optimizations * fixup
1 parent 7d8eb21 commit ec5136c

File tree

3 files changed

+49
-27
lines changed

3 files changed

+49
-27
lines changed

src/lib/content.js

+11-15
Original file line numberDiff line numberDiff line change
@@ -164,50 +164,46 @@ function resolvePathSymlinks(contentPath) {
164164
* @param {any} context
165165
* @param {ContentPath[]} candidateFiles
166166
* @param {Map<string, number>} fileModifiedMap
167-
* @returns {{ content: string, extension: string }[]}
167+
* @returns {[{ content: string, extension: string }[], Map<string, number>]}
168168
*/
169169
export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
170170
let changedContent = context.tailwindConfig.content.files
171171
.filter((item) => typeof item.raw === 'string')
172172
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
173173

174-
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
174+
let [changedFiles, mTimesToCommit] = resolveChangedFiles(candidateFiles, fileModifiedMap)
175+
176+
for (let changedFile of changedFiles) {
175177
let content = fs.readFileSync(changedFile, 'utf8')
176178
let extension = path.extname(changedFile).slice(1)
177179
changedContent.push({ content, extension })
178180
}
179181

180-
return changedContent
182+
return [changedContent, mTimesToCommit]
181183
}
182184

183185
/**
184186
*
185187
* @param {ContentPath[]} candidateFiles
186188
* @param {Map<string, number>} fileModifiedMap
187-
* @returns {Set<string>}
189+
* @returns {[Set<string>, Map<string, number>]}
188190
*/
189191
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
190192
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
193+
let mTimesToCommit = new Map()
191194

192195
let changedFiles = new Set()
193196
env.DEBUG && console.time('Finding changed files')
194197
let files = fastGlob.sync(paths, { absolute: true })
195198
for (let file of files) {
196-
let prevModified = fileModifiedMap.has(file) ? fileModifiedMap.get(file) : -Infinity
199+
let prevModified = fileModifiedMap.get(file) || -Infinity
197200
let modified = fs.statSync(file).mtimeMs
198201

199-
// This check is intentionally >= because we track the last modified time of context dependencies
200-
// earier in the process and we want to make sure we don't miss any changes that happen
201-
// when a context dependency is also a content dependency
202-
// Ideally, we'd do all this tracking at one time but that is a larger refactor
203-
// than we want to commit to right now, so this is a decent compromise.
204-
// This should be sufficient because file modification times will be off by at least
205-
// 1ms (the precision of fstat in Node) in most cases if they exist and were changed.
206-
if (modified >= prevModified) {
202+
if (modified > prevModified) {
207203
changedFiles.add(file)
208-
fileModifiedMap.set(file, modified)
204+
mTimesToCommit.set(file, modified)
209205
}
210206
}
211207
env.DEBUG && console.timeEnd('Finding changed files')
212-
return changedFiles
208+
return [changedFiles, mTimesToCommit]
213209
}

src/lib/setupContextUtils.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ export function getFileModifiedMap(context) {
632632

633633
function trackModified(files, fileModifiedMap) {
634634
let changed = false
635+
let mtimesToCommit = new Map()
635636

636637
for (let file of files) {
637638
if (!file) continue
@@ -652,10 +653,10 @@ function trackModified(files, fileModifiedMap) {
652653
changed = true
653654
}
654655

655-
fileModifiedMap.set(file, newModified)
656+
mtimesToCommit.set(file, newModified)
656657
}
657658

658-
return changed
659+
return [changed, mtimesToCommit]
659660
}
660661

661662
function extractVariantAtRules(node) {
@@ -1230,12 +1231,12 @@ export function getContext(
12301231
// If there's already a context in the cache and we don't need to
12311232
// reset the context, return the cached context.
12321233
if (existingContext) {
1233-
let contextDependenciesChanged = trackModified(
1234+
let [contextDependenciesChanged, mtimesToCommit] = trackModified(
12341235
[...contextDependencies],
12351236
getFileModifiedMap(existingContext)
12361237
)
12371238
if (!contextDependenciesChanged && !cssDidChange) {
1238-
return [existingContext, false]
1239+
return [existingContext, false, mtimesToCommit]
12391240
}
12401241
}
12411242

@@ -1270,7 +1271,7 @@ export function getContext(
12701271
userConfigPath,
12711272
})
12721273

1273-
trackModified([...contextDependencies], getFileModifiedMap(context))
1274+
let [, mtimesToCommit] = trackModified([...contextDependencies], getFileModifiedMap(context))
12741275

12751276
// ---
12761277

@@ -1285,5 +1286,5 @@ export function getContext(
12851286

12861287
contextSourcesMap.get(context).add(sourcePath)
12871288

1288-
return [context, true]
1289+
return [context, true, mtimesToCommit]
12891290
}

src/lib/setupTrackingContext.js

+31-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// @ts-check
2+
13
import fs from 'fs'
24
import LRU from 'quick-lru'
35

@@ -101,7 +103,7 @@ export default function setupTrackingContext(configOrPath) {
101103
}
102104
}
103105

104-
let [context] = getContext(
106+
let [context, , mTimesToCommit] = getContext(
105107
root,
106108
result,
107109
tailwindConfig,
@@ -110,6 +112,8 @@ export default function setupTrackingContext(configOrPath) {
110112
contextDependencies
111113
)
112114

115+
let fileModifiedMap = getFileModifiedMap(context)
116+
113117
let candidateFiles = getCandidateFiles(context, tailwindConfig)
114118

115119
// If there are no @tailwind or @apply rules, we don't consider this CSS file or it's
@@ -118,28 +122,49 @@ export default function setupTrackingContext(configOrPath) {
118122
// because it's impossible for a layer in one file to end up in the actual @tailwind rule
119123
// in another file since independent sources are effectively isolated.
120124
if (tailwindDirectives.size > 0) {
121-
let fileModifiedMap = getFileModifiedMap(context)
122-
123125
// Add template paths as postcss dependencies.
124126
for (let contentPath of candidateFiles) {
125127
for (let dependency of parseDependency(contentPath)) {
126128
registerDependency(dependency)
127129
}
128130
}
129131

130-
for (let changedContent of resolvedChangedContent(
132+
let [changedContent, contentMTimesToCommit] = resolvedChangedContent(
131133
context,
132134
candidateFiles,
133135
fileModifiedMap
134-
)) {
135-
context.changedContent.push(changedContent)
136+
)
137+
138+
for (let content of changedContent) {
139+
context.changedContent.push(content)
140+
}
141+
142+
// Add the mtimes of the content files to the commit list
143+
// We can overwrite the existing values because unconditionally
144+
// This is because:
145+
// 1. Most of the files here won't be in the map yet
146+
// 2. If they are that means it's a context dependency
147+
// and we're reading this after the context. This means
148+
// that the mtime we just read is strictly >= the context
149+
// mtime. Unless the user / os is doing something weird
150+
// in which the mtime would be going backwards. If that
151+
// happens there's already going to be problems.
152+
for (let [path, mtime] of contentMTimesToCommit.entries()) {
153+
mTimesToCommit.set(path, mtime)
136154
}
137155
}
138156

139157
for (let file of configDependencies) {
140158
registerDependency({ type: 'dependency', file })
141159
}
142160

161+
// "commit" the new modified time for all context deps
162+
// We do this here because we want content tracking to
163+
// read the "old" mtime even when it's a context dependency.
164+
for (let [path, mtime] of mTimesToCommit.entries()) {
165+
fileModifiedMap.set(path, mtime)
166+
}
167+
143168
return context
144169
}
145170
}

0 commit comments

Comments
 (0)