Unverified Commit 83704b1d authored by James D. Lin's avatar James D. Lin Committed by GitHub

Make `ProjectFileInvalidator.findInvalidated` able to use the async `FileStat.stat` (#42028)

Empirical measurements indicate on the network file system we use
internally, using `FileStat.stat` on thousands of files is much
faster than using `FileStat.statSync`. (It can be slower for files
on a local SSD, however.)

Add a flag to `ProjectFileInvalidator.findInvalidated` to let it
use `FileStat.stat` instead of `FileStat.statSync` when scanning for
modified files.  This can be enabled by overriding `HotRunnerConfig`.

I considered creating a separate, asynchronous version of
`findInvalidated`, but that led to more code duplication than I
liked, and it would be harder to avoid drift between the versions.
parent 35adf72c
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:json_rpc_2/error_code.dart' as rpc_error_code; import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import 'base/async_guard.dart'; import 'base/async_guard.dart';
import 'base/common.dart'; import 'base/common.dart';
...@@ -29,6 +30,10 @@ import 'vmservice.dart'; ...@@ -29,6 +30,10 @@ import 'vmservice.dart';
class HotRunnerConfig { class HotRunnerConfig {
/// Should the hot runner assume that the minimal Dart dependencies do not change? /// Should the hot runner assume that the minimal Dart dependencies do not change?
bool stableDartDependencies = false; bool stableDartDependencies = false;
/// Whether the hot runner should scan for modified files asynchronously.
bool asyncScanning = false;
/// A hook for implementations to perform any necessary initialization prior /// A hook for implementations to perform any necessary initialization prior
/// to a hot restart. Should return true if the hot restart should continue. /// to a hot restart. Should return true if the hot restart should continue.
Future<bool> setupHotRestart() async { Future<bool> setupHotRestart() async {
...@@ -296,10 +301,11 @@ class HotRunner extends ResidentRunner { ...@@ -296,10 +301,11 @@ class HotRunner extends ResidentRunner {
// Picking up first device's compiler as a source of truth - compilers // Picking up first device's compiler as a source of truth - compilers
// for all devices should be in sync. // for all devices should be in sync.
final List<Uri> invalidatedFiles = ProjectFileInvalidator.findInvalidated( final List<Uri> invalidatedFiles = await ProjectFileInvalidator.findInvalidated(
lastCompiled: flutterDevices[0].devFS.lastCompiled, lastCompiled: flutterDevices[0].devFS.lastCompiled,
urisToMonitor: flutterDevices[0].devFS.sources, urisToMonitor: flutterDevices[0].devFS.sources,
packagesPath: packagesFilePath, packagesPath: packagesFilePath,
asyncScanning: hotRunnerConfig.asyncScanning,
); );
final UpdateFSReport results = UpdateFSReport(success: true); final UpdateFSReport results = UpdateFSReport(success: true);
for (FlutterDevice device in flutterDevices) { for (FlutterDevice device in flutterDevices) {
...@@ -1044,11 +1050,20 @@ class ProjectFileInvalidator { ...@@ -1044,11 +1050,20 @@ class ProjectFileInvalidator {
static const String _pubCachePathLinuxAndMac = '.pub-cache'; static const String _pubCachePathLinuxAndMac = '.pub-cache';
static const String _pubCachePathWindows = 'Pub/Cache'; static const String _pubCachePathWindows = 'Pub/Cache';
static List<Uri> findInvalidated({ // As of writing, Dart supports up to 32 asynchronous I/O threads per
// isolate. We also want to avoid hitting platform limits on open file
// handles/descriptors.
//
// This value was chosen based on empirical tests scanning a set of
// ~2000 files.
static const int _kMaxPendingStats = 8;
static Future<List<Uri>> findInvalidated({
@required DateTime lastCompiled, @required DateTime lastCompiled,
@required List<Uri> urisToMonitor, @required List<Uri> urisToMonitor,
@required String packagesPath, @required String packagesPath,
}) { bool asyncScanning = false,
}) async {
assert(urisToMonitor != null); assert(urisToMonitor != null);
assert(packagesPath != null); assert(packagesPath != null);
...@@ -1068,17 +1083,36 @@ class ProjectFileInvalidator { ...@@ -1068,17 +1083,36 @@ class ProjectFileInvalidator {
fs.file(packagesPath).uri, fs.file(packagesPath).uri,
]; ];
final List<Uri> invalidatedFiles = <Uri>[]; final List<Uri> invalidatedFiles = <Uri>[];
if (asyncScanning) {
final Pool pool = Pool(_kMaxPendingStats);
final List<Future<void>> waitList = <Future<void>>[];
for (final Uri uri in urisToScan) {
waitList.add(pool.withResource<void>(
() => fs
.stat(uri.toFilePath(windows: platform.isWindows))
.then((FileStat stat) {
final DateTime updatedAt = stat.modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(uri);
}
})
));
}
await Future.wait<void>(waitList);
} else {
for (final Uri uri in urisToScan) { for (final Uri uri in urisToScan) {
final DateTime updatedAt = fs.statSync( final DateTime updatedAt = fs.statSync(
uri.toFilePath(windows: platform.isWindows), uri.toFilePath(windows: platform.isWindows)).modified;
).modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(uri); invalidatedFiles.add(uri);
} }
} }
}
printTrace( printTrace(
'Scanned through ${urisToScan.length} files in ' 'Scanned through ${urisToScan.length} files in '
'${stopwatch.elapsedMilliseconds}ms', '${stopwatch.elapsedMilliseconds}ms'
'${asyncScanning ? " (async)" : ""}',
); );
return invalidatedFiles; return invalidatedFiles;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
import 'package:meta/meta.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -14,12 +15,21 @@ final DateTime inFuture = DateTime.now().add(const Duration(days: 100)); ...@@ -14,12 +15,21 @@ final DateTime inFuture = DateTime.now().add(const Duration(days: 100));
void main() { void main() {
group('ProjectFileInvalidator', () { group('ProjectFileInvalidator', () {
_testProjectFileInvalidator(asyncScanning: false);
});
group('ProjectFileInvalidator (async scanning)', () {
_testProjectFileInvalidator(asyncScanning: true);
});
}
void _testProjectFileInvalidator({@required bool asyncScanning}) {
testUsingContext('No last compile', () async { testUsingContext('No last compile', () async {
expect( expect(
ProjectFileInvalidator.findInvalidated( await ProjectFileInvalidator.findInvalidated(
lastCompiled: null, lastCompiled: null,
urisToMonitor: <Uri>[], urisToMonitor: <Uri>[],
packagesPath: '', packagesPath: '',
asyncScanning: asyncScanning,
), ),
isEmpty, isEmpty,
); );
...@@ -27,10 +37,11 @@ void main() { ...@@ -27,10 +37,11 @@ void main() {
testUsingContext('Empty project', () async { testUsingContext('Empty project', () async {
expect( expect(
ProjectFileInvalidator.findInvalidated( await ProjectFileInvalidator.findInvalidated(
lastCompiled: inFuture, lastCompiled: inFuture,
urisToMonitor: <Uri>[], urisToMonitor: <Uri>[],
packagesPath: '', packagesPath: '',
asyncScanning: asyncScanning,
), ),
isEmpty, isEmpty,
); );
...@@ -41,10 +52,11 @@ void main() { ...@@ -41,10 +52,11 @@ void main() {
testUsingContext('Non-existent files are ignored', () async { testUsingContext('Non-existent files are ignored', () async {
expect( expect(
ProjectFileInvalidator.findInvalidated( await ProjectFileInvalidator.findInvalidated(
lastCompiled: inFuture, lastCompiled: inFuture,
urisToMonitor: <Uri>[Uri.parse('/not-there-anymore'),], urisToMonitor: <Uri>[Uri.parse('/not-there-anymore'),],
packagesPath: '', packagesPath: '',
asyncScanning: asyncScanning,
), ),
isEmpty, isEmpty,
); );
...@@ -52,5 +64,4 @@ void main() { ...@@ -52,5 +64,4 @@ void main() {
FileSystem: () => MemoryFileSystem(), FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager(<FakeCommand>[]), ProcessManager: () => FakeProcessManager(<FakeCommand>[]),
}); });
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment