Skip to content

Commit d2ba83d

Browse files
authored
Use libraryFilters flag to speed up coverage collection (#104122)
* Use libraryFilters flag to speed up coverage collection * Allow libraryNames to be null * Unconditionally enable the reportLines flag * Fix analysis errors
1 parent d9f3acb commit d2ba83d

File tree

4 files changed

+294
-131
lines changed

4 files changed

+294
-131
lines changed

packages/flutter_tools/bin/fuchsia_tester.dart

+5-8
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,13 @@ Future<void> run(List<String> args) async {
112112
Directory testDirectory;
113113
CoverageCollector collector;
114114
if (argResults['coverage'] as bool) {
115+
// If we have a specified coverage directory then accept all libraries by
116+
// setting libraryNames to null.
117+
final Set<String> libraryNames = coverageDirectory != null ? null :
118+
<String>{FlutterProject.current().manifest.appName};
115119
collector = CoverageCollector(
116120
packagesPath: globals.fs.path.normalize(globals.fs.path.absolute(argResults[_kOptionPackages] as String)),
117-
libraryPredicate: (String libraryName) {
118-
// If we have a specified coverage directory then accept all libraries.
119-
if (coverageDirectory != null) {
120-
return true;
121-
}
122-
final String projectName = FlutterProject.current().manifest.appName;
123-
return libraryName.contains(projectName);
124-
});
121+
libraryNames: libraryNames);
125122
if (!argResults.options.contains(_kOptionTestDirectory)) {
126123
throwToolExit('Use of --coverage requires setting --test-directory');
127124
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
380380
final String projectName = flutterProject.manifest.appName;
381381
collector = CoverageCollector(
382382
verbose: !machine,
383-
libraryPredicate: (String libraryName) => libraryName.contains(projectName),
383+
libraryNames: <String>{projectName},
384384
packagesPath: buildInfo.packagesPath
385385
);
386386
}

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

+73-91
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.libraryPredicate, this.verbose = true, @required this.packagesPath});
22+
CoverageCollector({this.libraryNames, this.verbose = true, @required this.packagesPath});
2323

2424
/// True when log messages should be emitted.
2525
final bool verbose;
@@ -31,9 +31,9 @@ class CoverageCollector extends TestWatcher {
3131
/// Map of file path to coverage hit map for that file.
3232
Map<String, coverage.HitMap> _globalHitmap;
3333

34-
/// Predicate function that returns true if the specified library URI should
35-
/// be included in the computed coverage.
36-
bool Function(String) libraryPredicate;
34+
/// The names of the libraries to gather coverage for. If null, all libraries
35+
/// will be accepted.
36+
Set<String> libraryNames;
3737

3838
@override
3939
Future<void> handleFinishedTest(TestDevice testDevice) async {
@@ -83,7 +83,7 @@ class CoverageCollector extends TestWatcher {
8383
Future<void> collectCoverageIsolate(Uri observatoryUri) async {
8484
assert(observatoryUri != null);
8585
_logMessage('collecting coverage data from $observatoryUri...');
86-
final Map<String, dynamic> data = await collect(observatoryUri, libraryPredicate);
86+
final Map<String, dynamic> data = await collect(observatoryUri, libraryNames);
8787
if (data == null) {
8888
throw Exception('Failed to collect coverage.');
8989
}
@@ -121,7 +121,7 @@ class CoverageCollector extends TestWatcher {
121121
final Future<void> collectionComplete = testDevice.observatoryUri
122122
.then((Uri observatoryUri) {
123123
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
124-
return collect(observatoryUri, libraryPredicate)
124+
return collect(observatoryUri, libraryNames)
125125
.then<void>((Map<String, dynamic> result) {
126126
if (result == null) {
127127
throw Exception('Failed to collect coverage.');
@@ -237,91 +237,106 @@ Future<FlutterVmService> _defaultConnect(Uri serviceUri) {
237237
serviceUri, compression: CompressionOptions.compressionOff, logger: globals.logger,);
238238
}
239239

240-
Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libraryPredicate, {
240+
Future<Map<String, dynamic>> collect(Uri serviceUri, Set<String> libraryNames, {
241241
bool waitPaused = false,
242242
String debugName,
243243
Future<FlutterVmService> Function(Uri) connector = _defaultConnect,
244244
@visibleForTesting bool forceSequential = false,
245245
}) async {
246246
final FlutterVmService vmService = await connector(serviceUri);
247-
final Map<String, dynamic> result = await _getAllCoverage(vmService.service, libraryPredicate, forceSequential);
247+
final Map<String, dynamic> result = await _getAllCoverage(vmService.service, libraryNames, forceSequential);
248248
await vmService.dispose();
249249
return result;
250250
}
251251

252252
Future<Map<String, dynamic>> _getAllCoverage(
253253
vm_service.VmService service,
254-
bool Function(String) libraryPredicate,
254+
Set<String> libraryNames,
255255
bool forceSequential,
256256
) async {
257257
final vm_service.Version version = await service.getVersion();
258-
final bool reportLines = (version.major == 3 && version.minor >= 51) || version.major > 3;
258+
final bool libraryFilters = (version.major == 3 && version.minor >= 57) || version.major > 3;
259259
final vm_service.VM vm = await service.getVM();
260260
final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
261+
bool libraryPredicate(String libraryName) {
262+
if (libraryNames == null) {
263+
return true;
264+
}
265+
final Uri uri = Uri.parse(libraryName);
266+
if (uri.scheme != 'package') {
267+
return false;
268+
}
269+
final String scope = uri.path.split('/').first;
270+
return libraryNames.contains(scope);
271+
}
261272
for (final vm_service.IsolateRef isolateRef in vm.isolates) {
262273
if (isolateRef.isSystemIsolate) {
263274
continue;
264275
}
265-
vm_service.ScriptList scriptList;
266-
try {
267-
scriptList = await service.getScripts(isolateRef.id);
268-
} on vm_service.SentinelException {
269-
continue;
270-
}
271-
272-
final List<Future<void>> futures = <Future<void>>[];
273-
final Map<String, vm_service.Script> scripts = <String, vm_service.Script>{};
274-
final Map<String, vm_service.SourceReport> sourceReports = <String, vm_service.SourceReport>{};
275-
// For each ScriptRef loaded into the VM, load the corresponding Script and
276-
// SourceReport object.
277-
278-
for (final vm_service.ScriptRef script in scriptList.scripts) {
279-
final String libraryUri = script.uri;
280-
if (!libraryPredicate(libraryUri)) {
281-
continue;
282-
}
283-
final String scriptId = script.id;
284-
final Future<void> getSourceReport = service.getSourceReport(
285-
isolateRef.id,
286-
<String>['Coverage'],
287-
scriptId: scriptId,
288-
forceCompile: true,
289-
reportLines: reportLines ? true : null,
290-
)
291-
.then((vm_service.SourceReport report) {
292-
sourceReports[scriptId] = report;
293-
});
294-
if (forceSequential) {
295-
await null;
296-
}
297-
futures.add(getSourceReport);
298-
if (reportLines) {
276+
if (libraryFilters) {
277+
final vm_service.SourceReport sourceReport = await service.getSourceReport(
278+
isolateRef.id,
279+
<String>['Coverage'],
280+
forceCompile: true,
281+
reportLines: true,
282+
libraryFilters: libraryNames == null ? null : List<String>.from(
283+
libraryNames.map((String name) => 'package:$name/')),
284+
);
285+
_buildCoverageMap(
286+
<String, vm_service.Script>{},
287+
<vm_service.SourceReport>[sourceReport],
288+
coverage,
289+
);
290+
} else {
291+
vm_service.ScriptList scriptList;
292+
try {
293+
scriptList = await service.getScripts(isolateRef.id);
294+
} on vm_service.SentinelException {
299295
continue;
300296
}
301-
final Future<void> getObject = service
302-
.getObject(isolateRef.id, scriptId)
303-
.then((vm_service.Obj response) {
304-
final vm_service.Script script = response as vm_service.Script;
305-
scripts[scriptId] = script;
297+
298+
final List<Future<void>> futures = <Future<void>>[];
299+
final Map<String, vm_service.Script> scripts = <String, vm_service.Script>{};
300+
final List<vm_service.SourceReport> sourceReports = <vm_service.SourceReport>[];
301+
302+
// For each ScriptRef loaded into the VM, load the corresponding Script
303+
// and SourceReport object.
304+
for (final vm_service.ScriptRef script in scriptList.scripts) {
305+
final String libraryUri = script.uri;
306+
if (!libraryPredicate(libraryUri)) {
307+
continue;
308+
}
309+
final String scriptId = script.id;
310+
final Future<void> getSourceReport = service.getSourceReport(
311+
isolateRef.id,
312+
<String>['Coverage'],
313+
scriptId: scriptId,
314+
forceCompile: true,
315+
reportLines: true,
316+
)
317+
.then((vm_service.SourceReport report) {
318+
sourceReports.add(report);
306319
});
307-
futures.add(getObject);
320+
if (forceSequential) {
321+
await null;
322+
}
323+
futures.add(getSourceReport);
324+
}
325+
await Future.wait(futures);
326+
_buildCoverageMap(scripts, sourceReports, coverage);
308327
}
309-
await Future.wait(futures);
310-
_buildCoverageMap(scripts, sourceReports, coverage, reportLines);
311328
}
312329
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': coverage};
313330
}
314331

315332
// Build a hitmap of Uri -> Line -> Hit Count for each script object.
316333
void _buildCoverageMap(
317334
Map<String, vm_service.Script> scripts,
318-
Map<String, vm_service.SourceReport> sourceReports,
335+
List<vm_service.SourceReport> sourceReports,
319336
List<Map<String, dynamic>> coverage,
320-
bool reportLines,
321337
) {
322338
final Map<String, Map<int, int>> hitMaps = <String, Map<int, int>>{};
323-
for (final String scriptId in sourceReports.keys) {
324-
final vm_service.SourceReport sourceReport = sourceReports[scriptId];
339+
for (final vm_service.SourceReport sourceReport in sourceReports) {
325340
for (final vm_service.SourceReportRange range in sourceReport.ranges) {
326341
final vm_service.SourceReportCoverage coverage = range.coverage;
327342
// Coverage reports may sometimes be null for a Script.
@@ -335,24 +350,14 @@ void _buildCoverageMap(
335350
final Map<int, int> hitMap = hitMaps[uri];
336351
final List<int> hits = coverage.hits;
337352
final List<int> misses = coverage.misses;
338-
final List<dynamic> tokenPositions = scripts[scriptRef.id]?.tokenPosTable;
339-
// The token positions can be null if the script has no lines that may be
340-
// covered. It will also be null if reportLines is true.
341-
if (tokenPositions == null && !reportLines) {
342-
continue;
343-
}
344353
if (hits != null) {
345-
for (final int hit in hits) {
346-
final int line =
347-
reportLines ? hit : _lineAndColumn(hit, tokenPositions)[0];
354+
for (final int line in hits) {
348355
final int current = hitMap[line] ?? 0;
349356
hitMap[line] = current + 1;
350357
}
351358
}
352359
if (misses != null) {
353-
for (final int miss in misses) {
354-
final int line =
355-
reportLines ? miss : _lineAndColumn(miss, tokenPositions)[0];
360+
for (final int line in misses) {
356361
hitMap[line] ??= 0;
357362
}
358363
}
@@ -363,29 +368,6 @@ void _buildCoverageMap(
363368
});
364369
}
365370

366-
// Binary search the token position table for the line and column which
367-
// corresponds to each token position.
368-
// The format of this table is described in https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#script
369-
List<int> _lineAndColumn(int position, List<dynamic> tokenPositions) {
370-
int min = 0;
371-
int max = tokenPositions.length;
372-
while (min < max) {
373-
final int mid = min + ((max - min) >> 1);
374-
final List<int> row = (tokenPositions[mid] as List<dynamic>).cast<int>();
375-
if (row[1] > position) {
376-
max = mid;
377-
} else {
378-
for (int i = 1; i < row.length; i += 2) {
379-
if (row[i] == position) {
380-
return <int>[row.first, row[i + 1]];
381-
}
382-
}
383-
min = mid + 1;
384-
}
385-
}
386-
throw StateError('Unreachable');
387-
}
388-
389371
// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
390372
Map<String, dynamic> _toScriptCoverageJson(String scriptUri, Map<int, int> hitMap) {
391373
final Map<String, dynamic> json = <String, dynamic>{};

0 commit comments

Comments
 (0)