Skip to content

Commit 2c72122

Browse files
[stable] [tool] Fix Windows hot reload mtime truncation issue (#191599)
### Issue Link: #191198 ### Impact Description: On Windows, file modification timestamps (mtime) reported by `dart:io` are truncated to whole-second precision. When files are edited rapidly (e.g. IDE format/save on change or quick successive edits within the same wall-clock second as the previous compilation), `ProjectFileInvalidator.findInvalidated` compares milliseconds against truncated seconds and falsely concludes that zero files have changed. Hot reload/restart silently skips compiling the changes, leaving running apps on stale code. ### Changelog Description: [flutter/191198] On Windows, truncate `lastCompiled` timestamp to whole seconds in `ProjectFileInvalidator` so rapid consecutive file modifications within the same second are not silently ignored by hot reload. ### Workaround: Wait at least 1-2 seconds after saving before triggering hot reload or hot restart. ### Risk: ### Test Coverage: ### Validation Steps: 1. On Windows, run a Flutter app with `flutter run`. 2. Save a change to `lib/main.dart` and trigger hot reload. 3. Immediately edit `lib/main.dart` again within < 1 second and trigger hot reload. 4. Verify that Flutter recompiles and applies the second change rather than reporting `Compiling dart to kernel with 0 updated files`.
1 parent aff2b56 commit 2c72122

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

‎packages/flutter_tools/lib/src/run_hot.dart‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,20 @@ class ProjectFileInvalidator {
14861486
if (_isNotInPubCache(uri)) uri,
14871487
];
14881488
final invalidatedFiles = <Uri>[];
1489+
1490+
final bool Function(DateTime) isInvalidated;
1491+
if (_platform.isWindows) {
1492+
// On Windows, FileStat.modified truncates to second precision (via GetFileAttributesExW).
1493+
// However, lastCompiled is recorded with millisecond precision.
1494+
final lastCompiledTruncated = DateTime.fromMillisecondsSinceEpoch(
1495+
(lastCompiled.millisecondsSinceEpoch ~/ 1000) * 1000,
1496+
isUtc: lastCompiled.isUtc,
1497+
);
1498+
isInvalidated = (DateTime updatedAt) => !updatedAt.isBefore(lastCompiledTruncated);
1499+
} else {
1500+
isInvalidated = (DateTime updatedAt) => updatedAt.isAfter(lastCompiled);
1501+
}
1502+
14891503
if (asyncScanning) {
14901504
final pool = Pool(_kMaxPendingStats);
14911505
final waitList = <Future<void>>[];
@@ -1502,7 +1516,7 @@ class ProjectFileInvalidator {
15021516
: _fileSystem.stat(uri.toFilePath(windows: _platform.isWindows)))
15031517
.then((FileStat stat) {
15041518
final DateTime updatedAt = stat.modified;
1505-
if (updatedAt.isAfter(lastCompiled)) {
1519+
if (isInvalidated(updatedAt)) {
15061520
invalidatedFiles.add(uri);
15071521
}
15081522
}),
@@ -1517,7 +1531,7 @@ class ProjectFileInvalidator {
15171531
final DateTime updatedAt = uri.hasScheme && uri.scheme != 'file'
15181532
? _fileSystem.file(uri).statSync().modified
15191533
: _fileSystem.statSync(uri.toFilePath(windows: _platform.isWindows)).modified;
1520-
if (updatedAt.isAfter(lastCompiled)) {
1534+
if (isInvalidated(updatedAt)) {
15211535
invalidatedFiles.add(uri);
15221536
}
15231537
}
@@ -1527,7 +1541,7 @@ class ProjectFileInvalidator {
15271541
final File packageFile = _fileSystem.file(packagesPath);
15281542
final Uri packageUri = packageFile.uri;
15291543
final DateTime updatedAt = packageFile.statSync().modified;
1530-
if (updatedAt.isAfter(lastCompiled)) {
1544+
if (isInvalidated(updatedAt)) {
15311545
invalidatedFiles.add(packageUri);
15321546
}
15331547

‎packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,85 @@ void main() {
132132
);
133133
},
134134
);
135+
136+
testWithoutContext(
137+
'File edited in same second as last compile is invalidated on Windows, asyncScanning: $asyncScanning',
138+
() async {
139+
final FileSystem fileSystem = MemoryFileSystem.test();
140+
final projectFileInvalidator = ProjectFileInvalidator(
141+
fileSystem: fileSystem,
142+
platform: FakePlatform(operatingSystem: 'windows'),
143+
logger: BufferLogger.test(),
144+
);
145+
146+
final lastCompiled = DateTime(2026, 8, 18, 3, 0, 0, 500); // 3:00:00.500
147+
final fileMtime = DateTime(2026, 8, 18, 3); // 3:00:00.000 (truncated)
148+
149+
final File file = fileSystem.file('foo.dart')
150+
..createSync(recursive: true)
151+
..writeAsStringSync('content');
152+
file.setLastModifiedSync(fileMtime);
153+
154+
fileSystem.file('.dart_tool/package_config.json')
155+
..createSync(recursive: true)
156+
..writeAsStringSync('''
157+
{
158+
"packages": [],
159+
"configVersion": 2
160+
}
161+
''');
162+
163+
expect(
164+
(await projectFileInvalidator.findInvalidated(
165+
lastCompiled: lastCompiled,
166+
urisToMonitor: <Uri>[file.uri],
167+
packagesPath: '.dart_tool/package_config.json',
168+
asyncScanning: asyncScanning,
169+
packageConfig: PackageConfig.empty,
170+
)).uris,
171+
contains(file.uri),
172+
);
173+
},
174+
);
175+
176+
testWithoutContext(
177+
'File edited in same second as last compile is NOT invalidated on non-Windows, asyncScanning: $asyncScanning',
178+
() async {
179+
final FileSystem fileSystem = MemoryFileSystem.test();
180+
final projectFileInvalidator = ProjectFileInvalidator(
181+
fileSystem: fileSystem,
182+
platform: FakePlatform(),
183+
logger: BufferLogger.test(),
184+
);
185+
186+
final lastCompiled = DateTime(2026, 8, 18, 3, 0, 0, 500); // 3:00:00.500
187+
final fileMtime = DateTime(2026, 8, 18, 3); // 3:00:00.000
188+
189+
final File file = fileSystem.file('foo.dart')
190+
..createSync(recursive: true)
191+
..writeAsStringSync('content');
192+
file.setLastModifiedSync(fileMtime);
193+
194+
fileSystem.file('.dart_tool/package_config.json')
195+
..createSync(recursive: true)
196+
..writeAsStringSync('''
197+
{
198+
"packages": [],
199+
"configVersion": 2
200+
}
201+
''');
202+
203+
expect(
204+
(await projectFileInvalidator.findInvalidated(
205+
lastCompiled: lastCompiled,
206+
urisToMonitor: <Uri>[file.uri],
207+
packagesPath: '.dart_tool/package_config.json',
208+
asyncScanning: asyncScanning,
209+
packageConfig: PackageConfig.empty,
210+
)).uris,
211+
isEmpty,
212+
);
213+
},
214+
);
135215
}
136216
}

0 commit comments

Comments
 (0)