@@ -41,9 +41,15 @@ const _debugTokenPositions = bool.fromEnvironment('DEBUG_COVERAGE');
4141/// If [scopedOutput] is non-empty, coverage will be restricted so that only
4242/// scripts that start with any of the provided paths are considered.
4343///
44- /// if [isolateIds] is set, the coverage gathering will be restricted to only
44+ /// If [isolateIds] is set, the coverage gathering will be restricted to only
4545/// those VM isolates.
4646///
47+ /// If [coverableLineCache] is set, the collector will avoid recompiling
48+ /// libraries it has already seen (see VmService.getSourceReport's
49+ /// librariesAlreadyCompiled parameter). This is only useful when doing more
50+ /// than one [collect] call over the same libraries. Pass an empty map to the
51+ /// first call, and then pass the same map to all subsequent calls.
52+ ///
4753/// [serviceOverrideForTesting] is for internal testing only, and should not be
4854/// set by users.
4955Future <Map <String , dynamic >> collect (Uri serviceUri, bool resume,
@@ -52,6 +58,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
5258 Duration ? timeout,
5359 bool functionCoverage = false ,
5460 bool branchCoverage = false ,
61+ Map <String , Set <int >>? coverableLineCache,
5562 VmService ? serviceOverrideForTesting}) async {
5663 scopedOutput ?? = < String > {};
5764
@@ -92,7 +99,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
9299 }
93100
94101 return await _getAllCoverage (service, includeDart, functionCoverage,
95- branchCoverage, scopedOutput, isolateIds);
102+ branchCoverage, scopedOutput, isolateIds, coverableLineCache );
96103 } finally {
97104 if (resume) {
98105 await _resumeIsolates (service);
@@ -115,7 +122,8 @@ Future<Map<String, dynamic>> _getAllCoverage(
115122 bool functionCoverage,
116123 bool branchCoverage,
117124 Set <String >? scopedOutput,
118- Set <String >? isolateIds) async {
125+ Set <String >? isolateIds,
126+ Map <String , Set <int >>? coverableLineCache) async {
119127 scopedOutput ?? = < String > {};
120128 final vm = await service.getVM ();
121129 final allCoverage = < Map <String , dynamic >> [];
@@ -124,16 +132,22 @@ Future<Map<String, dynamic>> _getAllCoverage(
124132 final branchCoverageSupported = _versionCheck (version, 3 , 56 );
125133 final libraryFilters = _versionCheck (version, 3 , 57 );
126134 final fastIsoGroups = _versionCheck (version, 3 , 61 );
135+ final lineCacheSupported = _versionCheck (version, 4 , 13 );
136+
127137 if (branchCoverage && ! branchCoverageSupported) {
128138 branchCoverage = false ;
129139 stderr.writeln ('Branch coverage was requested, but is not supported'
130140 ' by the VM version. Try updating to a newer version of Dart' );
131141 }
142+
132143 final sourceReportKinds = [
133144 SourceReportKind .kCoverage,
134145 if (branchCoverage) SourceReportKind .kBranchCoverage,
135146 ];
136147
148+ final librariesAlreadyCompiled =
149+ lineCacheSupported ? coverableLineCache? .keys.toList () : null ;
150+
137151 // Program counters are shared between isolates in the same group. So we need
138152 // to make sure we're only gathering coverage data for one isolate in each
139153 // group, otherwise we'll double count the hits.
@@ -173,15 +187,24 @@ Future<Map<String, dynamic>> _getAllCoverage(
173187 late final SourceReport scriptReport;
174188 try {
175189 scriptReport = await service.getSourceReport (
176- isolateRef.id! , sourceReportKinds,
177- forceCompile: true ,
178- scriptId: script.id,
179- reportLines: reportLines ? true : null );
190+ isolateRef.id! ,
191+ sourceReportKinds,
192+ forceCompile: true ,
193+ scriptId: script.id,
194+ reportLines: reportLines ? true : null ,
195+ librariesAlreadyCompiled: librariesAlreadyCompiled,
196+ );
180197 } on SentinelException {
181198 continue ;
182199 }
183- final coverage = await _getCoverageJson (service, isolateRef,
184- scriptReport, includeDart, functionCoverage, reportLines);
200+ final coverage = await _processSourceReport (
201+ service,
202+ isolateRef,
203+ scriptReport,
204+ includeDart,
205+ functionCoverage,
206+ reportLines,
207+ coverableLineCache);
185208 allCoverage.addAll (coverage);
186209 }
187210 } else {
@@ -195,12 +218,19 @@ Future<Map<String, dynamic>> _getAllCoverage(
195218 libraryFilters: scopedOutput.isNotEmpty && libraryFilters
196219 ? List .from (scopedOutput.map ((filter) => 'package:$filter /' ))
197220 : null ,
221+ librariesAlreadyCompiled: librariesAlreadyCompiled,
198222 );
199223 } on SentinelException {
200224 continue ;
201225 }
202- final coverage = await _getCoverageJson (service, isolateRef,
203- isolateReport, includeDart, functionCoverage, reportLines);
226+ final coverage = await _processSourceReport (
227+ service,
228+ isolateRef,
229+ isolateReport,
230+ includeDart,
231+ functionCoverage,
232+ reportLines,
233+ coverableLineCache);
204234 allCoverage.addAll (coverage);
205235 }
206236 }
@@ -276,13 +306,14 @@ int? _getLineFromTokenPos(Script script, int tokenPos) {
276306}
277307
278308/// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs.
279- Future <List <Map <String , dynamic >>> _getCoverageJson (
309+ Future <List <Map <String , dynamic >>> _processSourceReport (
280310 VmService service,
281311 IsolateRef isolateRef,
282312 SourceReport report,
283313 bool includeDart,
284314 bool functionCoverage,
285- bool reportLines) async {
315+ bool reportLines,
316+ Map <String , Set <int >>? coverableLineCache) async {
286317 final hitMaps = < Uri , HitMap > {};
287318 final scripts = < ScriptRef , Script > {};
288319 final libraries = < LibraryRef > {};
@@ -333,7 +364,16 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
333364
334365 for (var range in report.ranges! ) {
335366 final scriptRef = report.scripts! [range.scriptIndex! ];
336- final scriptUri = Uri .parse (scriptRef.uri! );
367+ final scriptUriString = scriptRef.uri! ;
368+ final scriptUri = Uri .parse (scriptUriString);
369+
370+ // If we have a coverableLineCache, use it in the same way we use
371+ // SourceReportCoverage.misses: to add zeros to the coverage result for all
372+ // the lines that don't have a hit. Afterwards, add all the lines that were
373+ // hit or missed to the cache, so that the next coverage collection won't
374+ // need to compile this libarry.
375+ final coverableLines =
376+ coverableLineCache? .putIfAbsent (scriptUriString, () => < int > {});
337377
338378 // Not returned in scripts section of source report.
339379 if (scriptUri.scheme == 'evaluate' ) continue ;
@@ -379,7 +419,8 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
379419
380420 if (coverage == null ) continue ;
381421
382- void forEachLine (List <int > tokenPositions, void Function (int line) body) {
422+ void forEachLine (List <int >? tokenPositions, void Function (int line) body) {
423+ if (tokenPositions == null ) return ;
383424 for (final pos in tokenPositions) {
384425 final line = reportLines ? pos : _getLineFromTokenPos (script! , pos);
385426 if (line == null ) {
@@ -393,14 +434,22 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
393434 }
394435 }
395436
396- forEachLine (coverage.hits! , (line) {
437+ if (coverableLines != null ) {
438+ for (final line in coverableLines) {
439+ hits.lineHits.putIfAbsent (line, () => 0 );
440+ }
441+ }
442+
443+ forEachLine (coverage.hits, (line) {
397444 hits.lineHits.increment (line);
445+ coverableLines? .add (line);
398446 if (hits.funcNames != null && hits.funcNames! .containsKey (line)) {
399447 hits.funcHits! .increment (line);
400448 }
401449 });
402- forEachLine (coverage.misses! , (line) {
450+ forEachLine (coverage.misses, (line) {
403451 hits.lineHits.putIfAbsent (line, () => 0 );
452+ coverableLines? .add (line);
404453 });
405454 hits.funcNames? .forEach ((line, funcName) {
406455 hits.funcHits? .putIfAbsent (line, () => 0 );
@@ -409,10 +458,10 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
409458 final branchCoverage = range.branchCoverage;
410459 if (branchCoverage != null ) {
411460 hits.branchHits ?? = < int , int > {};
412- forEachLine (branchCoverage.hits! , (line) {
461+ forEachLine (branchCoverage.hits, (line) {
413462 hits.branchHits! .increment (line);
414463 });
415- forEachLine (branchCoverage.misses! , (line) {
464+ forEachLine (branchCoverage.misses, (line) {
416465 hits.branchHits! .putIfAbsent (line, () => 0 );
417466 });
418467 }
0 commit comments