2
2
// for details. All rights reserved. Use of this source code is governed by a
3
3
// BSD-style license that can be found in the LICENSE file.
4
4
5
+ import 'dart:collection' ;
5
6
import 'dart:math' ;
6
7
7
8
import 'package:graphs/graphs.dart' ;
@@ -39,27 +40,39 @@ import 'phased_value.dart';
39
40
/// Secondly, because the loader is for use _during_ the build, it might be that
40
41
/// not all files have been generated yet. So, results must be returned based on
41
42
/// 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.
42
50
class 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
+
43
56
/// The dependencies of loaded assets, as far as is known.
44
57
///
45
58
/// Source files do not change during the build, so as soon as loaded
46
59
/// their value is a [PhasedValue.fixed] that is valid for the whole build.
47
60
///
48
61
/// A generated file that could not yet be loaded is a
49
62
/// [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 ] .
51
64
///
52
65
/// A generated file that _has_ been loaded is a [PhasedValue.generated]
53
66
/// specifying both the phase it was generated at and its parsed dependencies.
54
67
final Map <AssetId , PhasedValue <AssetDeps >> _assetDeps = {};
55
68
56
- /// Generated assets that were loaded before they were generated .
69
+ /// Assets to load .
57
70
///
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
+ /// to keep the keys sorted so earlier phases can be processed first.
73
+ final SplayTreeMap <int , List <AssetId >> _idsToLoad = SplayTreeMap ();
60
74
61
- /// Newly [_load] ed assets to process for the first time in [_buildCycles] .
62
- Set <AssetId > _newAssets = {};
75
+ final List <(int , AssetId )> _loadingIds = [];
63
76
64
77
/// All loaded library cycles, by asset.
65
78
final Map <AssetId , PhasedValue <LibraryCycle >> _cycles = {};
@@ -76,10 +89,51 @@ class LibraryCycleGraphLoader {
76
89
/// Clears all data.
77
90
void clear () {
78
91
_assetDeps.clear ();
79
- _assetDepsToLoadByPhase.clear ();
80
- _newAssets.clear ();
92
+ _idsToLoad.clear ();
81
93
_cycles.clear ();
82
94
_graphs.clear ();
95
+ _graphsToComputeByPhase.clear ();
96
+ _runningAtPhases.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
+ /// Throws if not [_hasIdToLoad] at [upToPhase] .
118
+ ///
119
+ /// When done loading call [_removeIdToLoad] with the phase and ID.
120
+ (int , AssetId ) _nextIdToLoad ({required int upToPhase}) {
121
+ final entry =
122
+ _idsToLoad.entries.where ((entry) => entry.key <= upToPhase).first;
123
+ final result = entry.value.last;
124
+ _loadingIds.add ((entry.key, result));
125
+ return (entry.key, result);
126
+ }
127
+
128
+ /// Removes from [_idsToLoad] .
129
+ ///
130
+ /// Pass a phase and ID from [_nextIdToLoad] .
131
+ void _removeIdToLoad (int phase, AssetId id) {
132
+ final ids = _idsToLoad[phase];
133
+ if (ids != null ) {
134
+ ids.remove (id);
135
+ if (ids.isEmpty) _idsToLoad.remove (phase);
136
+ }
83
137
}
84
138
85
139
/// Loads [id] and its transitive dependencies at all phases available to
@@ -88,85 +142,91 @@ class LibraryCycleGraphLoader {
88
142
/// Assets are loaded to [_assetDeps] .
89
143
///
90
144
/// If assets are encountered that have not yet been generated, they are
91
- /// added to [_assetDepsToLoadByPhase ] , and will be loaded eagerly by any
145
+ /// added to [_idsToLoad ] , and will be loaded eagerly by any
92
146
/// call to `_load` with an `assetDepsLoader` at a late enough phase.
93
147
///
94
- /// Newly seen assets are noted in [_newAssets] for further processing by
95
- /// [_buildCycles] .
148
+ /// Newly seen assets are noted in [_graphsToComputeByPhase] at phase 0
149
+ /// for further processing by [_buildCycles] .
96
150
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
- }
151
+ // Mark [id] as an asset to load at any phase.
152
+ _loadAtPhase (0 , id);
105
153
106
- while (idsToLoad.isNotEmpty) {
107
- final idToLoad = idsToLoad.removeLast ();
154
+ final phase = assetDepsLoader.phase;
155
+ while (_hasIdToLoad (upToPhase: phase)) {
156
+ final (idToLoadPhase, idToLoad) = _nextIdToLoad (upToPhase: phase);
108
157
109
158
// Nothing to do if deps were already loaded, unless they expire and
110
159
// [assetDepsLoader] is at a late enough phase to see the updated value.
111
160
final alreadyLoadedAssetDeps = _assetDeps[idToLoad];
112
161
if (alreadyLoadedAssetDeps != null &&
113
162
! alreadyLoadedAssetDeps.isExpiredAt (phase: assetDepsLoader.phase)) {
163
+ _removeIdToLoad (idToLoadPhase, idToLoad);
114
164
continue ;
115
165
}
116
166
117
- final assetDeps =
118
- _assetDeps[idToLoad] = await assetDepsLoader.load (idToLoad);
119
-
120
167
// First time seeing the asset, mark for computation of cycles and
121
168
// graphs given the initial state of the build.
122
169
if (alreadyLoadedAssetDeps == null ) {
123
- _newAssets .add (idToLoad);
170
+ (_graphsToComputeByPhase[ 0 ] ?? = {}) .add (idToLoad);
124
171
}
125
172
173
+ // If `idToLoad` is a generated asset from an earlier phase then the call
174
+ // to `assetDepsLoader.load` causes it to be built if not yet build. This
175
+ // in turn might cause a recursion into `LibraryCycleGraphLoader` and back
176
+ // into this `_load` method.
177
+ //
178
+ // Only recursion with an earlier phase is possible: attempted reads to a
179
+ // later phase return nothing instead of causing a build. This is also
180
+ // enforced in `libraryCycleOf`.
181
+ //
182
+ // The earlier phase `_load` might need results that this `_load` was
183
+ // going to produce. This is handled via the shared `_idsToLoad`: the
184
+ // earlier phase `_load` will take all the pending loads up to its own
185
+ // phase.
186
+ //
187
+ // This might include the current `idToLoad`, which is left in
188
+ // `_idsToLoad` until the load completes for that reason.
189
+ //
190
+ // If a recursive `_load` happens then the associated cycles and graphs
191
+ // are also fully computed before this `_load` continues: the work that
192
+ // remains is only work for later phases.
193
+ final assetDeps =
194
+ _assetDeps[idToLoad] = await assetDepsLoader.load (idToLoad);
195
+ _removeIdToLoad (idToLoadPhase, idToLoad);
196
+
126
197
if (assetDeps.isComplete) {
127
198
// "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
- }
199
+ // already been generated, and its deps have been parsed. Mark them
200
+ // for loading at any phase: if the `_load` that loads them is at a too
201
+ // early phase to see generated output they will be queued for
202
+ // processing by a later `_load`.
203
+ _loadAllAtPhase (0 , assetDeps.lastValue.deps);
132
204
} else {
133
205
// It's a generated source that has not yet been generated. Mark it for
134
206
// loading later.
135
- (_assetDepsToLoadByPhase[assetDeps.values.last.expiresAfter! + 1 ] ?? =
136
- {})
137
- .add (idToLoad);
207
+ _loadAtPhase (assetDeps.values.last.expiresAfter! + 1 , idToLoad);
138
208
}
139
209
}
140
210
}
141
211
142
- /// Computes [_cycles] for all [_newAssets] at phase 0, then for all assets
143
- /// with expiring graphs up to and including [upToPhase] .
212
+ /// Computes [_cycles] then [_graphs] for all [_graphsToComputeByPhase] .
144
213
///
145
- /// Call [_load] first so there are [_newAssets] assets to process. Clears
146
- /// [_newAssets] of processed IDs.
214
+ /// Call [_load] first so there are [_graphsToComputeByPhase] to process.
147
215
///
148
216
/// 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] .
217
+ /// expire after [upToPhase] --are added to [_graphsToComputeByPhase] at
218
+ /// the appropirate phase to be completed later.
152
219
void _buildCycles (int upToPhase) {
153
220
// Process phases that have work to do in ascending order.
154
221
while (true ) {
155
222
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
- }
223
+
224
+ // Work through phases <= `upToPhase` at which graphs expire,
225
+ // so there are new values to compute.
226
+ if (_graphsToComputeByPhase.isEmpty) break ;
227
+ phase = _graphsToComputeByPhase.keys.reduce (min);
228
+ if (phase > upToPhase) break ;
229
+ final idsToComputeCyclesFrom = _graphsToComputeByPhase.remove (phase)! ;
170
230
171
231
// Edges for strongly connected components computation.
172
232
Iterable <AssetId > edgesFromId (AssetId id) {
@@ -340,20 +400,36 @@ class LibraryCycleGraphLoader {
340
400
///
341
401
/// Previously computed state is used if possible, anything additional is
342
402
/// loaded using [assetDepsLoader] .
403
+ ///
404
+ /// See class note about recursive calls.
343
405
Future <PhasedValue <LibraryCycle >> libraryCycleOf (
344
406
AssetDepsLoader assetDepsLoader,
345
407
AssetId id,
346
408
) async {
347
- await _load (assetDepsLoader, id);
348
- _buildCycles (assetDepsLoader.phase);
349
- return _cycles[id]! ;
409
+ if (_runningAtPhases.isNotEmpty &&
410
+ assetDepsLoader.phase >= _runningAtPhases.last) {
411
+ throw StateError (
412
+ 'Cannot recurse at later or equal phase ${assetDepsLoader .phase }, '
413
+ 'already running at : $_runningAtPhases ' ,
414
+ );
415
+ }
416
+ _runningAtPhases.add (assetDepsLoader.phase);
417
+ try {
418
+ await _load (assetDepsLoader, id);
419
+ _buildCycles (assetDepsLoader.phase);
420
+ return _cycles[id]! ;
421
+ } finally {
422
+ _runningAtPhases.removeLast ();
423
+ }
350
424
}
351
425
352
426
/// Returns the [LibraryCycleGraph] of [id] at all phases before the
353
427
/// [assetDepsLoader] phase.
354
428
///
355
429
/// Previously computed state is used if possible, anything additional is
356
430
/// loaded using [assetDepsLoader] .
431
+ ///
432
+ /// See class note about recursive calls.
357
433
Future <PhasedValue <LibraryCycleGraph >> libraryCycleGraphOf (
358
434
AssetDepsLoader assetDepsLoader,
359
435
AssetId id,
@@ -374,6 +450,8 @@ class LibraryCycleGraphLoader {
374
450
///
375
451
/// Previously computed state is used if possible, anything additional is
376
452
/// loaded using [assetDepsLoader] .
453
+ ///
454
+ /// See class note about recursive calls.
377
455
Future <Iterable <AssetId >> transitiveDepsOf (
378
456
AssetDepsLoader assetDepsLoader,
379
457
AssetId id,
@@ -386,8 +464,7 @@ class LibraryCycleGraphLoader {
386
464
String toString () => '''
387
465
LibraryCycleGraphLoader(
388
466
_assetDeps: $_assetDeps ,
389
- _assetDepsToLoadByPhase: $_assetDepsToLoadByPhase ,
390
- _newAssets: $_newAssets ,
467
+ _idsToLoad: $_idsToLoad ,
391
468
_cycles: $_cycles ,
392
469
_graphs: $_graphs ,
393
470
_graphsToComputeByPhase: $_graphsToComputeByPhase ,
0 commit comments