Skip to content

Commit 3494d50

Browse files
authored
Library cycle graph loader recursion support (#3966)
* Support recursive loads in `LibraryCycleGraphLoader`. * Address review comments.
1 parent 05adfea commit 3494d50

File tree

2 files changed

+324
-72
lines changed

2 files changed

+324
-72
lines changed

build/lib/src/library_cycle_graph/library_cycle_graph_loader.dart

Lines changed: 148 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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';
56
import 'dart:math';
67

78
import '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.
4250
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+
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() => '''
387478
LibraryCycleGraphLoader(
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

Comments
 (0)