Skip to content

Commit b240361

Browse files
committed
Support recursive loads in LibraryCycleGraphLoader.
1 parent ec6d434 commit b240361

File tree

2 files changed

+311
-73
lines changed

2 files changed

+311
-73
lines changed

build/lib/src/library_cycle_graph/library_cycle_graph_loader.dart

Lines changed: 135 additions & 58 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+
/// to keep the keys sorted so earlier phases can be processed first.
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 = {};
@@ -76,10 +89,51 @@ class LibraryCycleGraphLoader {
7689
/// Clears all data.
7790
void clear() {
7891
_assetDeps.clear();
79-
_assetDepsToLoadByPhase.clear();
80-
_newAssets.clear();
92+
_idsToLoad.clear();
8193
_cycles.clear();
8294
_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+
}
83137
}
84138

85139
/// Loads [id] and its transitive dependencies at all phases available to
@@ -88,85 +142,91 @@ class LibraryCycleGraphLoader {
88142
/// Assets are loaded to [_assetDeps].
89143
///
90144
/// 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
92146
/// call to `_load` with an `assetDepsLoader` at a late enough phase.
93147
///
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].
96150
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);
105153

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);
108157

109158
// Nothing to do if deps were already loaded, unless they expire and
110159
// [assetDepsLoader] is at a late enough phase to see the updated value.
111160
final alreadyLoadedAssetDeps = _assetDeps[idToLoad];
112161
if (alreadyLoadedAssetDeps != null &&
113162
!alreadyLoadedAssetDeps.isExpiredAt(phase: assetDepsLoader.phase)) {
163+
_removeIdToLoad(idToLoadPhase, idToLoad);
114164
continue;
115165
}
116166

117-
final assetDeps =
118-
_assetDeps[idToLoad] = await assetDepsLoader.load(idToLoad);
119-
120167
// First time seeing the asset, mark for computation of cycles and
121168
// graphs given the initial state of the build.
122169
if (alreadyLoadedAssetDeps == null) {
123-
_newAssets.add(idToLoad);
170+
(_graphsToComputeByPhase[0] ??= {}).add(idToLoad);
124171
}
125172

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+
126197
if (assetDeps.isComplete) {
127198
// "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);
132204
} else {
133205
// It's a generated source that has not yet been generated. Mark it for
134206
// loading later.
135-
(_assetDepsToLoadByPhase[assetDeps.values.last.expiresAfter! + 1] ??=
136-
{})
137-
.add(idToLoad);
207+
_loadAtPhase(assetDeps.values.last.expiresAfter! + 1, idToLoad);
138208
}
139209
}
140210
}
141211

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].
144213
///
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.
147215
///
148216
/// 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.
152219
void _buildCycles(int upToPhase) {
153220
// Process phases that have work to do in ascending order.
154221
while (true) {
155222
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)!;
170230

171231
// Edges for strongly connected components computation.
172232
Iterable<AssetId> edgesFromId(AssetId id) {
@@ -340,20 +400,36 @@ class LibraryCycleGraphLoader {
340400
///
341401
/// Previously computed state is used if possible, anything additional is
342402
/// loaded using [assetDepsLoader].
403+
///
404+
/// See class note about recursive calls.
343405
Future<PhasedValue<LibraryCycle>> libraryCycleOf(
344406
AssetDepsLoader assetDepsLoader,
345407
AssetId id,
346408
) 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+
}
350424
}
351425

352426
/// Returns the [LibraryCycleGraph] of [id] at all phases before the
353427
/// [assetDepsLoader] phase.
354428
///
355429
/// Previously computed state is used if possible, anything additional is
356430
/// loaded using [assetDepsLoader].
431+
///
432+
/// See class note about recursive calls.
357433
Future<PhasedValue<LibraryCycleGraph>> libraryCycleGraphOf(
358434
AssetDepsLoader assetDepsLoader,
359435
AssetId id,
@@ -374,6 +450,8 @@ class LibraryCycleGraphLoader {
374450
///
375451
/// Previously computed state is used if possible, anything additional is
376452
/// loaded using [assetDepsLoader].
453+
///
454+
/// See class note about recursive calls.
377455
Future<Iterable<AssetId>> transitiveDepsOf(
378456
AssetDepsLoader assetDepsLoader,
379457
AssetId id,
@@ -386,8 +464,7 @@ class LibraryCycleGraphLoader {
386464
String toString() => '''
387465
LibraryCycleGraphLoader(
388466
_assetDeps: $_assetDeps,
389-
_assetDepsToLoadByPhase: $_assetDepsToLoadByPhase,
390-
_newAssets: $_newAssets,
467+
_idsToLoad: $_idsToLoad,
391468
_cycles: $_cycles,
392469
_graphs: $_graphs,
393470
_graphsToComputeByPhase: $_graphsToComputeByPhase,

0 commit comments

Comments
 (0)