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
+ /// for its sorting, so earlier phases are processed first in [_nextIdToLoad] .
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 = {};
@@ -75,11 +88,60 @@ class LibraryCycleGraphLoader {
75
88
76
89
/// Clears all data.
77
90
void clear () {
91
+ _runningAtPhases.clear ();
78
92
_assetDeps.clear ();
79
- _assetDepsToLoadByPhase.clear ();
80
- _newAssets.clear ();
93
+ _idsToLoad.clear ();
81
94
_cycles.clear ();
82
95
_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
+ }
83
145
}
84
146
85
147
/// Loads [id] and its transitive dependencies at all phases available to
@@ -88,85 +150,91 @@ class LibraryCycleGraphLoader {
88
150
/// Assets are loaded to [_assetDeps] .
89
151
///
90
152
/// 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.
93
155
///
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] .
96
158
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);
105
161
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);
108
165
109
166
// Nothing to do if deps were already loaded, unless they expire and
110
167
// [assetDepsLoader] is at a late enough phase to see the updated value.
111
168
final alreadyLoadedAssetDeps = _assetDeps[idToLoad];
112
169
if (alreadyLoadedAssetDeps != null &&
113
170
! alreadyLoadedAssetDeps.isExpiredAt (phase: assetDepsLoader.phase)) {
171
+ _removeIdToLoad (idToLoadPhase, idToLoad);
114
172
continue ;
115
173
}
116
174
117
- final assetDeps =
118
- _assetDeps[idToLoad] = await assetDepsLoader.load (idToLoad);
119
-
120
175
// First time seeing the asset, mark for computation of cycles and
121
176
// graphs given the initial state of the build.
122
177
if (alreadyLoadedAssetDeps == null ) {
123
- _newAssets .add (idToLoad);
178
+ (_graphsToComputeByPhase[ 0 ] ?? = {}) .add (idToLoad);
124
179
}
125
180
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
+
126
205
if (assetDeps.isComplete) {
127
206
// "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);
132
212
} else {
133
213
// It's a generated source that has not yet been generated. Mark it for
134
214
// loading later.
135
- (_assetDepsToLoadByPhase[assetDeps.values.last.expiresAfter! + 1 ] ?? =
136
- {})
137
- .add (idToLoad);
215
+ _loadAtPhase (assetDeps.values.last.expiresAfter! + 1 , idToLoad);
138
216
}
139
217
}
140
218
}
141
219
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] .
144
221
///
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.
147
223
///
148
224
/// 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.
152
227
void _buildCycles (int upToPhase) {
153
228
// Process phases that have work to do in ascending order.
154
229
while (true ) {
155
230
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)! ;
170
238
171
239
// Edges for strongly connected components computation.
172
240
Iterable <AssetId > edgesFromId (AssetId id) {
@@ -340,20 +408,41 @@ class LibraryCycleGraphLoader {
340
408
///
341
409
/// Previously computed state is used if possible, anything additional is
342
410
/// loaded using [assetDepsLoader] .
411
+ ///
412
+ /// See class note about recursive calls.
343
413
Future <PhasedValue <LibraryCycle >> libraryCycleOf (
344
414
AssetDepsLoader assetDepsLoader,
345
415
AssetId id,
346
416
) 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
+
347
426
await _load (assetDepsLoader, id);
348
427
_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;
350
437
}
351
438
352
439
/// Returns the [LibraryCycleGraph] of [id] at all phases before the
353
440
/// [assetDepsLoader] phase.
354
441
///
355
442
/// Previously computed state is used if possible, anything additional is
356
443
/// loaded using [assetDepsLoader] .
444
+ ///
445
+ /// See class note about recursive calls.
357
446
Future <PhasedValue <LibraryCycleGraph >> libraryCycleGraphOf (
358
447
AssetDepsLoader assetDepsLoader,
359
448
AssetId id,
@@ -374,6 +463,8 @@ class LibraryCycleGraphLoader {
374
463
///
375
464
/// Previously computed state is used if possible, anything additional is
376
465
/// loaded using [assetDepsLoader] .
466
+ ///
467
+ /// See class note about recursive calls.
377
468
Future <Iterable <AssetId >> transitiveDepsOf (
378
469
AssetDepsLoader assetDepsLoader,
379
470
AssetId id,
@@ -385,9 +476,9 @@ class LibraryCycleGraphLoader {
385
476
@override
386
477
String toString () => '''
387
478
LibraryCycleGraphLoader(
479
+ _runningAtPhases: $_runningAtPhases
388
480
_assetDeps: $_assetDeps ,
389
- _assetDepsToLoadByPhase: $_assetDepsToLoadByPhase ,
390
- _newAssets: $_newAssets ,
481
+ _idsToLoad: $_idsToLoad ,
391
482
_cycles: $_cycles ,
392
483
_graphs: $_graphs ,
393
484
_graphsToComputeByPhase: $_graphsToComputeByPhase ,
0 commit comments