Skip to content

Commit 803ef6a

Browse files
authored
Improve coverage speed by using new caching option for package:coverage (flutter#107395)
Speedup coverage test runs by using (new) coverage handle with caching. On a `flutter test --coverage` run on `packages/flutter` the runtime goes from ~828 seconds to ~617 seconds, or a ~25% reduction in time spent (testing without coverage takes ~236 seconds so the overhead of `--coverage` in this case goes from ~592 seconds to ~381 seconds, or a ~35% reduction).
1 parent c251856 commit 803ef6a

File tree

4 files changed

+216
-101
lines changed

4 files changed

+216
-101
lines changed

packages/flutter_tools/bin/fuchsia_tester.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ Future<void> run(List<String> args) async {
116116
// setting libraryNames to null.
117117
final Set<String> libraryNames = coverageDirectory != null ? null :
118118
<String>{FlutterProject.current().manifest.appName};
119+
final String packagesPath = globals.fs.path.normalize(globals.fs.path.absolute(argResults[_kOptionPackages] as String));
119120
collector = CoverageCollector(
120-
packagesPath: globals.fs.path.normalize(globals.fs.path.absolute(argResults[_kOptionPackages] as String)),
121-
libraryNames: libraryNames);
121+
packagesPath: packagesPath,
122+
libraryNames: libraryNames,
123+
resolver: await CoverageCollector.getResolver(packagesPath));
122124
if (!argResults.options.contains(_kOptionTestDirectory)) {
123125
throwToolExit('Use of --coverage requires setting --test-directory');
124126
}

packages/flutter_tools/lib/src/commands/test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
374374
collector = CoverageCollector(
375375
verbose: !machine,
376376
libraryNames: <String>{projectName},
377-
packagesPath: buildInfo.packagesPath
377+
packagesPath: buildInfo.packagesPath,
378+
resolver: await CoverageCollector.getResolver(buildInfo.packagesPath)
378379
);
379380
}
380381

packages/flutter_tools/lib/src/test/coverage_collector.dart

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import 'watcher.dart';
1919

2020
/// A class that collects code coverage data during test runs.
2121
class CoverageCollector extends TestWatcher {
22-
CoverageCollector({this.libraryNames, this.verbose = true, required this.packagesPath});
22+
CoverageCollector({this.libraryNames, this.verbose = true, required this.packagesPath, this.resolver});
2323

2424
/// True when log messages should be emitted.
2525
final bool verbose;
@@ -35,6 +35,19 @@ class CoverageCollector extends TestWatcher {
3535
/// will be accepted.
3636
Set<String>? libraryNames;
3737

38+
final coverage.Resolver? resolver;
39+
final Map<String, List<List<int>>> _ignoredLinesInFilesCache = <String, List<List<int>>>{};
40+
41+
static Future<coverage.Resolver> getResolver(String? packagesPath) async {
42+
try {
43+
return await coverage.Resolver.create(packagesPath: packagesPath);
44+
} on FileSystemException {
45+
// When given a bad packages path (as for instance done in some tests)
46+
// just ignore it and return one without a packages path.
47+
return coverage.Resolver.create();
48+
}
49+
}
50+
3851
@override
3952
Future<void> handleFinishedTest(TestDevice testDevice) async {
4053
_logMessage('Starting coverage collection');
@@ -104,7 +117,7 @@ class CoverageCollector extends TestWatcher {
104117
/// has been run to completion so that all coverage data has been recorded.
105118
///
106119
/// The returned [Future] completes when the coverage is collected.
107-
Future<void> collectCoverage(TestDevice testDevice) async {
120+
Future<void> collectCoverage(TestDevice testDevice, {@visibleForTesting Future<FlutterVmService> Function(Uri?)? connector}) async {
108121
assert(testDevice != null);
109122

110123
Map<String, dynamic>? data;
@@ -119,7 +132,7 @@ class CoverageCollector extends TestWatcher {
119132
final Future<void> collectionComplete = testDevice.observatoryUri
120133
.then((Uri? observatoryUri) {
121134
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
122-
return collect(observatoryUri, libraryNames)
135+
return collect(observatoryUri, libraryNames, connector: connector ?? _defaultConnect)
123136
.then<void>((Map<String, dynamic> result) {
124137
if (result == null) {
125138
throw Exception('Failed to collect coverage.');
@@ -133,11 +146,12 @@ class CoverageCollector extends TestWatcher {
133146
assert(data != null);
134147

135148
_logMessage('Merging coverage data...');
136-
_addHitmap(await coverage.HitMap.parseJson(
137-
data!['coverage'] as List<Map<String, dynamic>>,
138-
packagePath: packageDirectory,
139-
checkIgnoredLines: true,
140-
));
149+
final Map<String, coverage.HitMap> hitmap = coverage.HitMap.parseJsonSync(
150+
data!['coverage'] as List<Map<String, dynamic>>,
151+
checkIgnoredLines: true,
152+
resolver: resolver ?? await CoverageCollector.getResolver(packageDirectory),
153+
ignoredLinesInFilesCache: _ignoredLinesInFilesCache);
154+
_addHitmap(hitmap);
141155
_logMessage('Done merging coverage data into global coverage map.');
142156
}
143157

@@ -154,13 +168,13 @@ class CoverageCollector extends TestWatcher {
154168
return null;
155169
}
156170
if (formatter == null) {
157-
resolver ??= await coverage.Resolver.create(packagesPath: packagesPath);
171+
final coverage.Resolver usedResolver = resolver ?? this.resolver ?? await CoverageCollector.getResolver(packagesPath);
158172
final String packagePath = globals.fs.currentDirectory.path;
159173
final List<String> reportOn = coverageDirectory == null
160174
? <String>[globals.fs.path.join(packagePath, 'lib')]
161175
: <String>[coverageDirectory.path];
162176
formatter = (Map<String, coverage.HitMap> hitmap) => hitmap
163-
.formatLcov(resolver!, reportOn: reportOn, basePath: packagePath);
177+
.formatLcov(usedResolver, reportOn: reportOn, basePath: packagePath);
164178
}
165179
final String result = formatter(_globalHitmap!);
166180
_globalHitmap = null;

packages/flutter_tools/test/general.shard/coverage_collector_test.dart

Lines changed: 186 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:convert' show jsonEncode;
6+
import 'dart:io' show Directory, File;
7+
8+
import 'package:coverage/src/hitmap.dart';
59
import 'package:flutter_tools/src/test/coverage_collector.dart';
10+
import 'package:flutter_tools/src/test/test_device.dart' show TestDevice;
11+
import 'package:stream_channel/stream_channel.dart' show StreamChannel;
612
import 'package:vm_service/vm_service.dart';
713

814
import '../src/common.dart';
@@ -138,94 +144,7 @@ void main() {
138144
});
139145

140146
testWithoutContext('Coverage collector with null libraryNames accepts all libraries', () async {
141-
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
142-
requests: <VmServiceExpectation>[
143-
FakeVmServiceRequest(
144-
method: 'getVersion',
145-
jsonResponse: Version(major: 3, minor: 51).toJson(),
146-
),
147-
FakeVmServiceRequest(
148-
method: 'getVM',
149-
jsonResponse: (VM.parse(<String, Object>{})!
150-
..isolates = <IsolateRef>[
151-
IsolateRef.parse(<String, Object>{
152-
'id': '1',
153-
})!,
154-
]
155-
).toJson(),
156-
),
157-
FakeVmServiceRequest(
158-
method: 'getScripts',
159-
args: <String, Object>{
160-
'isolateId': '1',
161-
},
162-
jsonResponse: ScriptList(scripts: <ScriptRef>[
163-
ScriptRef(uri: 'package:foo/foo.dart', id: '1'),
164-
ScriptRef(uri: 'package:bar/bar.dart', id: '2'),
165-
]).toJson(),
166-
),
167-
FakeVmServiceRequest(
168-
method: 'getSourceReport',
169-
args: <String, Object>{
170-
'isolateId': '1',
171-
'reports': <Object>['Coverage'],
172-
'scriptId': '1',
173-
'forceCompile': true,
174-
'reportLines': true,
175-
},
176-
jsonResponse: SourceReport(
177-
ranges: <SourceReportRange>[
178-
SourceReportRange(
179-
scriptIndex: 0,
180-
startPos: 0,
181-
endPos: 0,
182-
compiled: true,
183-
coverage: SourceReportCoverage(
184-
hits: <int>[1, 3],
185-
misses: <int>[2],
186-
),
187-
),
188-
],
189-
scripts: <ScriptRef>[
190-
ScriptRef(
191-
uri: 'package:foo/foo.dart',
192-
id: '1',
193-
),
194-
],
195-
).toJson(),
196-
),
197-
FakeVmServiceRequest(
198-
method: 'getSourceReport',
199-
args: <String, Object>{
200-
'isolateId': '1',
201-
'reports': <Object>['Coverage'],
202-
'scriptId': '2',
203-
'forceCompile': true,
204-
'reportLines': true,
205-
},
206-
jsonResponse: SourceReport(
207-
ranges: <SourceReportRange>[
208-
SourceReportRange(
209-
scriptIndex: 0,
210-
startPos: 0,
211-
endPos: 0,
212-
compiled: true,
213-
coverage: SourceReportCoverage(
214-
hits: <int>[47, 21],
215-
misses: <int>[32, 86],
216-
),
217-
),
218-
],
219-
scripts: <ScriptRef>[
220-
ScriptRef(
221-
uri: 'package:bar/bar.dart',
222-
id: '2',
223-
),
224-
],
225-
).toJson(),
226-
),
227-
],
228-
);
147+
final FakeVmServiceHost fakeVmServiceHost = createFakeVmServiceHostWithFooAndBar();
229148

230149
final Map<String, Object?> result = await collect(
231150
null,
@@ -417,4 +336,183 @@ void main() {
417336
});
418337
expect(fakeVmServiceHost.hasRemainingExpectations, false);
419338
});
339+
340+
testWithoutContext('Coverage collector caches read files', () async {
341+
Directory? tempDir;
342+
try {
343+
tempDir = Directory.systemTemp.createTempSync('flutter_coverage_collector_test.');
344+
final File file = File('${tempDir.path}/packages.json');
345+
file.writeAsStringSync(jsonEncode(<String, dynamic>{
346+
'configVersion': 2,
347+
'packages': <Map<String, String>>[
348+
<String, String>{
349+
'name': 'foo',
350+
'rootUri': 'foo',
351+
},
352+
<String, String>{
353+
'name': 'bar',
354+
'rootUri': 'bar',
355+
},
356+
],
357+
}));
358+
final Directory fooDir = Directory('${tempDir.path}/foo/');
359+
fooDir.createSync();
360+
final File fooFile = File('${fooDir.path}/foo.dart');
361+
fooFile.writeAsStringSync('hit\nnohit but ignored // coverage:ignore-line\nhit\n');
362+
363+
final String packagesPath = file.path;
364+
final CoverageCollector collector = CoverageCollector(
365+
libraryNames: <String>{'foo', 'bar'},
366+
verbose: false,
367+
packagesPath: packagesPath,
368+
resolver: await CoverageCollector.getResolver(packagesPath)
369+
);
370+
await collector.collectCoverage(TestTestDevice(), connector: (Uri? uri) async {
371+
return createFakeVmServiceHostWithFooAndBar().vmService;
372+
});
373+
374+
Future<void> getHitMapAndVerify() async {
375+
final Map<String, HitMap> gottenHitmap = <String, HitMap>{};
376+
await collector.finalizeCoverage(formatter: (Map<String, HitMap> hitmap) {
377+
gottenHitmap.addAll(hitmap);
378+
return '';
379+
});
380+
expect(gottenHitmap.keys.toList()..sort(), <String>['package:bar/bar.dart', 'package:foo/foo.dart']);
381+
expect(gottenHitmap['package:foo/foo.dart']!.lineHits, <int, int>{1: 1, /* 2: 0, is ignored in file */ 3: 1});
382+
expect(gottenHitmap['package:bar/bar.dart']!.lineHits, <int, int>{21: 1, 32: 0, 47: 1, 86: 0});
383+
}
384+
385+
Future<void> verifyHitmapEmpty() async {
386+
final Map<String, HitMap> gottenHitmap = <String, HitMap>{};
387+
await collector.finalizeCoverage(formatter: (Map<String, HitMap> hitmap) {
388+
gottenHitmap.addAll(hitmap);
389+
return '';
390+
});
391+
expect(gottenHitmap.isEmpty, isTrue);
392+
}
393+
394+
// Get hit map the first time.
395+
await getHitMapAndVerify();
396+
397+
// Getting the hitmap clears it so we now doesn't get any data.
398+
await verifyHitmapEmpty();
399+
400+
// Collecting again gets us the same data even though the foo file has been deleted.
401+
// This means that the fact that line 2 was ignored has been cached.
402+
fooFile.deleteSync();
403+
await collector.collectCoverage(TestTestDevice(), connector: (Uri? uri) async {
404+
return createFakeVmServiceHostWithFooAndBar().vmService;
405+
});
406+
await getHitMapAndVerify();
407+
} finally {
408+
tempDir?.deleteSync(recursive: true);
409+
}
410+
});
411+
}
412+
413+
FakeVmServiceHost createFakeVmServiceHostWithFooAndBar() {
414+
return FakeVmServiceHost(
415+
requests: <VmServiceExpectation>[
416+
FakeVmServiceRequest(
417+
method: 'getVersion',
418+
jsonResponse: Version(major: 3, minor: 51).toJson(),
419+
),
420+
FakeVmServiceRequest(
421+
method: 'getVM',
422+
jsonResponse: (VM.parse(<String, Object>{})!
423+
..isolates = <IsolateRef>[
424+
IsolateRef.parse(<String, Object>{
425+
'id': '1',
426+
})!,
427+
]
428+
).toJson(),
429+
),
430+
FakeVmServiceRequest(
431+
method: 'getScripts',
432+
args: <String, Object>{
433+
'isolateId': '1',
434+
},
435+
jsonResponse: ScriptList(scripts: <ScriptRef>[
436+
ScriptRef(uri: 'package:foo/foo.dart', id: '1'),
437+
ScriptRef(uri: 'package:bar/bar.dart', id: '2'),
438+
]).toJson(),
439+
),
440+
FakeVmServiceRequest(
441+
method: 'getSourceReport',
442+
args: <String, Object>{
443+
'isolateId': '1',
444+
'reports': <Object>['Coverage'],
445+
'scriptId': '1',
446+
'forceCompile': true,
447+
'reportLines': true,
448+
},
449+
jsonResponse: SourceReport(
450+
ranges: <SourceReportRange>[
451+
SourceReportRange(
452+
scriptIndex: 0,
453+
startPos: 0,
454+
endPos: 0,
455+
compiled: true,
456+
coverage: SourceReportCoverage(
457+
hits: <int>[1, 3],
458+
misses: <int>[2],
459+
),
460+
),
461+
],
462+
scripts: <ScriptRef>[
463+
ScriptRef(
464+
uri: 'package:foo/foo.dart',
465+
id: '1',
466+
),
467+
],
468+
).toJson(),
469+
),
470+
FakeVmServiceRequest(
471+
method: 'getSourceReport',
472+
args: <String, Object>{
473+
'isolateId': '1',
474+
'reports': <Object>['Coverage'],
475+
'scriptId': '2',
476+
'forceCompile': true,
477+
'reportLines': true,
478+
},
479+
jsonResponse: SourceReport(
480+
ranges: <SourceReportRange>[
481+
SourceReportRange(
482+
scriptIndex: 0,
483+
startPos: 0,
484+
endPos: 0,
485+
compiled: true,
486+
coverage: SourceReportCoverage(
487+
hits: <int>[47, 21],
488+
misses: <int>[32, 86],
489+
),
490+
),
491+
],
492+
scripts: <ScriptRef>[
493+
ScriptRef(
494+
uri: 'package:bar/bar.dart',
495+
id: '2',
496+
),
497+
],
498+
).toJson(),
499+
),
500+
],
501+
);
502+
}
503+
504+
class TestTestDevice extends TestDevice {
505+
@override
506+
Future<void> get finished => Future<void>.delayed(const Duration(seconds: 1));
507+
508+
@override
509+
Future<void> kill() => Future<void>.value();
510+
511+
@override
512+
Future<Uri?> get observatoryUri => Future<Uri?>.value();
513+
514+
@override
515+
Future<StreamChannel<String>> start(String entrypointPath) {
516+
throw UnimplementedError();
517+
}
420518
}

0 commit comments

Comments
 (0)