22// for details. All rights reserved. Use of this source code is governed by a
33// BSD-style license that can be found in the LICENSE file.
44
5+ import 'dart:collection' ;
56import 'dart:math' ;
67
78import 'package:graphs/graphs.dart' ;
@@ -39,27 +40,39 @@ import 'phased_value.dart';
3940/// Secondly, because the loader is for use _during_ the build, it might be that
4041/// not all files have been generated yet. So, results must be returned based on
4142/// incomplete data, as needed.
43+ ///
44+ /// There can be multiple concurrent computations running on top of the same
45+ /// state, as noted in the implementation. It is allowed to call the methods
46+ /// `libraryCycleOf` , `libraryCycleGraphOf` and `transitiveDepsOf` while a prior
47+ /// call to any of the methods is still running, provided each newer call is at
48+ /// an earlier phase. This happens when a load does a read that triggers a build
49+ /// of a generated file in an earlier phase.
4250class LibraryCycleGraphLoader {
51+ /// The phases at which evaluation is currently running.
52+ ///
53+ /// Used to check that recursive loads are always to an earlier phase.
54+ final List <int > _runningAtPhases = [];
55+
4356 /// The dependencies of loaded assets, as far as is known.
4457 ///
4558 /// Source files do not change during the build, so as soon as loaded
4659 /// their value is a [PhasedValue.fixed] that is valid for the whole build.
4760 ///
4861 /// A generated file that could not yet be loaded is a
4962 /// [PhasedValue.unavailable] specify the phase when it will be generated.
50- /// When to finish loading the asset is tracked in [_assetDepsToLoadByPhase ] .
63+ /// When to finish loading the asset is tracked in [_idsToLoad ] .
5164 ///
5265 /// A generated file that _has_ been loaded is a [PhasedValue.generated]
5366 /// specifying both the phase it was generated at and its parsed dependencies.
5467 final Map <AssetId , PhasedValue <AssetDeps >> _assetDeps = {};
5568
56- /// Generated assets that were loaded before they were generated .
69+ /// Assets to load .
5770 ///
58- /// The `key` is the phase at which they have been generated and can be read.
59- final Map <int , Set <AssetId >> _assetDepsToLoadByPhase = {};
71+ /// The `key` is the phase to load them at or after. A [SplayTreeMap] is used
72+ /// for its sorting, so earlier phases are processed first in [_nextIdToLoad] .
73+ final SplayTreeMap <int , List <AssetId >> _idsToLoad = SplayTreeMap ();
6074
61- /// Newly [_load] ed assets to process for the first time in [_buildCycles] .
62- Set <AssetId > _newAssets = {};
75+ final List <(int , AssetId )> _loadingIds = [];
6376
6477 /// All loaded library cycles, by asset.
6578 final Map <AssetId , PhasedValue <LibraryCycle >> _cycles = {};
@@ -75,11 +88,60 @@ class LibraryCycleGraphLoader {
7588
7689 /// Clears all data.
7790 void clear () {
91+ _runningAtPhases.clear ();
7892 _assetDeps.clear ();
79- _assetDepsToLoadByPhase.clear ();
80- _newAssets.clear ();
93+ _idsToLoad.clear ();
8194 _cycles.clear ();
8295 _graphs.clear ();
96+ _graphsToComputeByPhase.clear ();
97+ }
98+
99+ /// Marks asset with [id] for loading at [phase] .
100+ ///
101+ /// Any [_load] running at that phase or later will load it.
102+ void _loadAtPhase (int phase, AssetId id) {
103+ (_idsToLoad[phase] ?? = []).add (id);
104+ }
105+
106+ void _loadAllAtPhase (int phase, Iterable <AssetId > ids) {
107+ if (ids.isEmpty) return ;
108+ (_idsToLoad[phase] ?? = []).addAll (ids);
109+ }
110+
111+ /// Whether there are assets to load before or at [upToPhase] .
112+ bool _hasIdToLoad ({required int upToPhase}) =>
113+ _idsToLoad.keys.where ((key) => key <= upToPhase).isNotEmpty;
114+
115+ /// The phase and ID of the next asset to load before or at [upToPhase] .
116+ ///
117+ /// Earlier phases are processed first.
118+ ///
119+ /// Throws if not [_hasIdToLoad] at [upToPhase] .
120+ ///
121+ /// When done loading call [_removeIdToLoad] with the phase and ID.
122+ (int , AssetId ) _nextIdToLoad ({required int upToPhase}) {
123+ final entry =
124+ _idsToLoad.entries.where ((entry) => entry.key <= upToPhase).first;
125+ final result = entry.value.last;
126+ _loadingIds.add ((entry.key, result));
127+ return (entry.key, result);
128+ }
129+
130+ /// Removes from [_idsToLoad] .
131+ ///
132+ /// Pass a phase and ID from [_nextIdToLoad] .
133+ void _removeIdToLoad (int phase, AssetId id) {
134+ // A recursive load might have updated `_idsToLoad` since `_nextIdToLoad`
135+ // was called. If so it fully processed some phases: either `_idsToLoad` is
136+ // now empty at `phase`, in which case there is nothing to do, or it's
137+ // unchanged, in which case `id` is still the last ID.
138+ final ids = _idsToLoad[phase];
139+ if (ids != null ) {
140+ if (ids.removeLast () != id) {
141+ throw StateError ('$id should still be last in _idsToLoad[$phase ]' );
142+ }
143+ if (ids.isEmpty) _idsToLoad.remove (phase);
144+ }
83145 }
84146
85147 /// Loads [id] and its transitive dependencies at all phases available to
@@ -88,85 +150,91 @@ class LibraryCycleGraphLoader {
88150 /// Assets are loaded to [_assetDeps] .
89151 ///
90152 /// If assets are encountered that have not yet been generated, they are
91- /// added to [_assetDepsToLoadByPhase ] , and will be loaded eagerly by any
92- /// call to `_load` with an `assetDepsLoader` at a late enough phase.
153+ /// added to [_idsToLoad ] , and will be loaded eagerly by any call to `_load`
154+ /// with an `assetDepsLoader` at a late enough phase.
93155 ///
94- /// Newly seen assets are noted in [_newAssets] for further processing by
95- /// [_buildCycles] .
156+ /// Newly seen assets are noted in [_graphsToComputeByPhase] at phase 0
157+ /// for further processing by [_buildCycles] .
96158 Future <void > _load (AssetDepsLoader assetDepsLoader, AssetId id) async {
97- final idsToLoad = [id];
98- // Finish loading any assets that were `_load`ed before they were generated
99- // and have now been generated.
100- for (final phase in _assetDepsToLoadByPhase.keys.toList (growable: false )) {
101- if (phase <= assetDepsLoader.phase) {
102- idsToLoad.addAll (_assetDepsToLoadByPhase.remove (phase)! );
103- }
104- }
159+ // Mark [id] as an asset to load at any phase.
160+ _loadAtPhase (0 , id);
105161
106- while (idsToLoad.isNotEmpty) {
107- final idToLoad = idsToLoad.removeLast ();
162+ final phase = assetDepsLoader.phase;
163+ while (_hasIdToLoad (upToPhase: phase)) {
164+ final (idToLoadPhase, idToLoad) = _nextIdToLoad (upToPhase: phase);
108165
109166 // Nothing to do if deps were already loaded, unless they expire and
110167 // [assetDepsLoader] is at a late enough phase to see the updated value.
111168 final alreadyLoadedAssetDeps = _assetDeps[idToLoad];
112169 if (alreadyLoadedAssetDeps != null &&
113170 ! alreadyLoadedAssetDeps.isExpiredAt (phase: assetDepsLoader.phase)) {
171+ _removeIdToLoad (idToLoadPhase, idToLoad);
114172 continue ;
115173 }
116174
117- final assetDeps =
118- _assetDeps[idToLoad] = await assetDepsLoader.load (idToLoad);
119-
120175 // First time seeing the asset, mark for computation of cycles and
121176 // graphs given the initial state of the build.
122177 if (alreadyLoadedAssetDeps == null ) {
123- _newAssets .add (idToLoad);
178+ (_graphsToComputeByPhase[ 0 ] ?? = {}) .add (idToLoad);
124179 }
125180
181+ // If `idToLoad` is a generated asset from an earlier phase then the call
182+ // to `assetDepsLoader.load` causes it to be built if not yet build. This
183+ // in turn might cause a recursion into `LibraryCycleGraphLoader` and back
184+ // into this `_load` method.
185+ //
186+ // Only recursion with an earlier phase is possible: attempted reads to a
187+ // later phase return nothing instead of causing a build. This is also
188+ // enforced in `libraryCycleOf`.
189+ //
190+ // The earlier phase `_load` might need results that this `_load` was
191+ // going to produce. This is handled via the shared `_idsToLoad`: the
192+ // earlier phase `_load` will take all the pending loads up to its own
193+ // phase.
194+ //
195+ // This might include the current `idToLoad`, which is left in
196+ // `_idsToLoad` until the load completes for that reason.
197+ //
198+ // If a recursive `_load` happens then the associated cycles and graphs
199+ // are also fully computed before this `_load` continues: the work that
200+ // remains is only work for later phases.
201+ final assetDeps =
202+ _assetDeps[idToLoad] = await assetDepsLoader.load (idToLoad);
203+ _removeIdToLoad (idToLoadPhase, idToLoad);
204+
126205 if (assetDeps.isComplete) {
127206 // "isComplete" means it's a source file or a generated value that has
128- // already been generated. It has deps, so mark them for loading.
129- for (final dep in assetDeps.lastValue.deps) {
130- idsToLoad.add (dep);
131- }
207+ // already been generated, and its deps have been parsed. Mark them
208+ // for loading at any phase: if the `_load` that loads them is at a too
209+ // early phase to see generated output they will be queued for
210+ // processing by a later `_load`.
211+ _loadAllAtPhase (0 , assetDeps.lastValue.deps);
132212 } else {
133213 // It's a generated source that has not yet been generated. Mark it for
134214 // loading later.
135- (_assetDepsToLoadByPhase[assetDeps.values.last.expiresAfter! + 1 ] ?? =
136- {})
137- .add (idToLoad);
215+ _loadAtPhase (assetDeps.values.last.expiresAfter! + 1 , idToLoad);
138216 }
139217 }
140218 }
141219
142- /// Computes [_cycles] for all [_newAssets] at phase 0, then for all assets
143- /// with expiring graphs up to and including [upToPhase] .
220+ /// Computes [_cycles] then [_graphs] for all [_graphsToComputeByPhase] .
144221 ///
145- /// Call [_load] first so there are [_newAssets] assets to process. Clears
146- /// [_newAssets] of processed IDs.
222+ /// Call [_load] first so there are [_graphsToComputeByPhase] to process.
147223 ///
148224 /// Graphs which are still not complete--they have one or more assets that
149- /// expire after [upToPhase] --are added to [_graphsToComputeByPhase] to
150- /// be completed later.
151- /// [_graphsToComputeByPhase] .
225+ /// expire after [upToPhase] --are added to [_graphsToComputeByPhase] at
226+ /// the appropirate phase to be completed later.
152227 void _buildCycles (int upToPhase) {
153228 // Process phases that have work to do in ascending order.
154229 while (true ) {
155230 int phase;
156- Set <AssetId > idsToComputeCyclesFrom;
157- if (_newAssets.isNotEmpty) {
158- // New assets: work to do at phase 0, the initial build state.
159- phase = 0 ;
160- idsToComputeCyclesFrom = _newAssets;
161- _newAssets = {};
162- } else {
163- // Work through phases <= `upToPhase` at which graphs expire,
164- // so there are new values to compute.
165- if (_graphsToComputeByPhase.isEmpty) break ;
166- phase = _graphsToComputeByPhase.keys.reduce (min);
167- if (phase > upToPhase) break ;
168- idsToComputeCyclesFrom = _graphsToComputeByPhase.remove (phase)! ;
169- }
231+
232+ // Work through phases <= `upToPhase` at which graphs expire,
233+ // so there are new values to compute.
234+ if (_graphsToComputeByPhase.isEmpty) break ;
235+ phase = _graphsToComputeByPhase.keys.reduce (min);
236+ if (phase > upToPhase) break ;
237+ final idsToComputeCyclesFrom = _graphsToComputeByPhase.remove (phase)! ;
170238
171239 // Edges for strongly connected components computation.
172240 Iterable <AssetId > edgesFromId (AssetId id) {
@@ -340,20 +408,41 @@ class LibraryCycleGraphLoader {
340408 ///
341409 /// Previously computed state is used if possible, anything additional is
342410 /// loaded using [assetDepsLoader] .
411+ ///
412+ /// See class note about recursive calls.
343413 Future <PhasedValue <LibraryCycle >> libraryCycleOf (
344414 AssetDepsLoader assetDepsLoader,
345415 AssetId id,
346416 ) async {
417+ final phase = assetDepsLoader.phase;
418+ if (_runningAtPhases.isNotEmpty && phase >= _runningAtPhases.last) {
419+ throw StateError (
420+ 'Cannot recurse at later or equal phase $phase , already running at: '
421+ '$_runningAtPhases ' ,
422+ );
423+ }
424+ _runningAtPhases.add (assetDepsLoader.phase);
425+
347426 await _load (assetDepsLoader, id);
348427 _buildCycles (assetDepsLoader.phase);
349- return _cycles[id]! ;
428+ final result = _cycles[id]! ;
429+
430+ // A recursive call always finishes before the outer call resumes.
431+ final removedPhase = _runningAtPhases.removeLast ();
432+ if (removedPhase != phase) {
433+ throw StateError ('Removed phase $removedPhase , expected $phase .' );
434+ }
435+
436+ return result;
350437 }
351438
352439 /// Returns the [LibraryCycleGraph] of [id] at all phases before the
353440 /// [assetDepsLoader] phase.
354441 ///
355442 /// Previously computed state is used if possible, anything additional is
356443 /// loaded using [assetDepsLoader] .
444+ ///
445+ /// See class note about recursive calls.
357446 Future <PhasedValue <LibraryCycleGraph >> libraryCycleGraphOf (
358447 AssetDepsLoader assetDepsLoader,
359448 AssetId id,
@@ -374,6 +463,8 @@ class LibraryCycleGraphLoader {
374463 ///
375464 /// Previously computed state is used if possible, anything additional is
376465 /// loaded using [assetDepsLoader] .
466+ ///
467+ /// See class note about recursive calls.
377468 Future <Iterable <AssetId >> transitiveDepsOf (
378469 AssetDepsLoader assetDepsLoader,
379470 AssetId id,
@@ -385,9 +476,9 @@ class LibraryCycleGraphLoader {
385476 @override
386477 String toString () => '''
387478LibraryCycleGraphLoader(
479+ _runningAtPhases: $_runningAtPhases
388480 _assetDeps: $_assetDeps ,
389- _assetDepsToLoadByPhase: $_assetDepsToLoadByPhase ,
390- _newAssets: $_newAssets ,
481+ _idsToLoad: $_idsToLoad ,
391482 _cycles: $_cycles ,
392483 _graphs: $_graphs ,
393484 _graphsToComputeByPhase: $_graphsToComputeByPhase ,
0 commit comments