Skip to content

Commit 4beaab8

Browse files
committed
Something that looks correct.
1 parent ec6d434 commit 4beaab8

File tree

2 files changed

+206
-88
lines changed

2 files changed

+206
-88
lines changed

build/lib/src/library_cycle_graph/library_cycle_graph_loader.dart

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ import 'phased_value.dart';
4040
/// not all files have been generated yet. So, results must be returned based on
4141
/// incomplete data, as needed.
4242
class LibraryCycleGraphLoader {
43+
final List<int> _runningAtPhases = [];
44+
4345
/// The dependencies of loaded assets, as far as is known.
4446
///
4547
/// Source files do not change during the build, so as soon as loaded
4648
/// their value is a [PhasedValue.fixed] that is valid for the whole build.
4749
///
4850
/// A generated file that could not yet be loaded is a
4951
/// [PhasedValue.unavailable] specify the phase when it will be generated.
50-
/// When to finish loading the asset is tracked in [_assetDepsToLoadByPhase].
52+
/// When to finish loading the asset is tracked in [_idsToLoad].
5153
///
5254
/// A generated file that _has_ been loaded is a [PhasedValue.generated]
5355
/// specifying both the phase it was generated at and its parsed dependencies.
@@ -56,7 +58,7 @@ class LibraryCycleGraphLoader {
5658
/// Generated assets that were loaded before they were generated.
5759
///
5860
/// The `key` is the phase at which they have been generated and can be read.
59-
final Map<int, Set<AssetId>> _assetDepsToLoadByPhase = {};
61+
final Map<int, List<AssetId>> _idsToLoad = {};
6062

6163
/// Newly [_load]ed assets to process for the first time in [_buildCycles].
6264
Set<AssetId> _newAssets = {};
@@ -76,7 +78,7 @@ class LibraryCycleGraphLoader {
7678
/// Clears all data.
7779
void clear() {
7880
_assetDeps.clear();
79-
_assetDepsToLoadByPhase.clear();
81+
_idsToLoad.clear();
8082
_newAssets.clear();
8183
_cycles.clear();
8284
_graphs.clear();
@@ -88,55 +90,67 @@ class LibraryCycleGraphLoader {
8890
/// Assets are loaded to [_assetDeps].
8991
///
9092
/// If assets are encountered that have not yet been generated, they are
91-
/// added to [_assetDepsToLoadByPhase], and will be loaded eagerly by any
93+
/// added to [_idsToLoad], and will be loaded eagerly by any
9294
/// call to `_load` with an `assetDepsLoader` at a late enough phase.
9395
///
9496
/// Newly seen assets are noted in [_newAssets] for further processing by
9597
/// [_buildCycles].
9698
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-
}
99+
final thisPhaseIdsToLoad = _idsToLoad[assetDepsLoader.phase] ??= [];
100+
thisPhaseIdsToLoad.add(id);
105101

106-
while (idsToLoad.isNotEmpty) {
107-
final idToLoad = idsToLoad.removeLast();
102+
while (_idsToLoad.entries
103+
.where((entry) => entry.key <= assetDepsLoader.phase)
104+
.any((entry) => entry.value.isNotEmpty)) {
105+
final firstPhase = (_idsToLoad.keys..toList().sort()).firstWhere(
106+
(key) => key <= assetDepsLoader.phase && _idsToLoad[key]!.isNotEmpty);
107+
final idToLoad = _idsToLoad[firstPhase]!.last;
108+
print('($_runningAtPhases, $id) take $idToLoad at phase $firstPhase');
108109

109110
// Nothing to do if deps were already loaded, unless they expire and
110111
// [assetDepsLoader] is at a late enough phase to see the updated value.
111112
final alreadyLoadedAssetDeps = _assetDeps[idToLoad];
112113
if (alreadyLoadedAssetDeps != null &&
113114
!alreadyLoadedAssetDeps.isExpiredAt(phase: assetDepsLoader.phase)) {
115+
_idsToLoad[firstPhase]!.remove(idToLoad);
114116
continue;
115117
}
116118

119+
print(
120+
'($_runningAtPhases, $id) Load $idToLoad at ${assetDepsLoader.phase}');
117121
final assetDeps =
118122
_assetDeps[idToLoad] = await assetDepsLoader.load(idToLoad);
119123

120124
// First time seeing the asset, mark for computation of cycles and
121125
// graphs given the initial state of the build.
122126
if (alreadyLoadedAssetDeps == null) {
127+
print('For phase $firstPhase, Add to new assets: $idToLoad');
123128
_newAssets.add(idToLoad);
124129
}
125130

126131
if (assetDeps.isComplete) {
127132
// "isComplete" means it's a source file or a generated value that has
128133
// already been generated. It has deps, so mark them for loading.
134+
print(
135+
'($_runningAtPhases, $id) Completed $idToLoad: ${assetDeps.lastValue.deps}');
129136
for (final dep in assetDeps.lastValue.deps) {
130-
idsToLoad.add(dep);
137+
final phaseIdsToLoad = _idsToLoad[0] ??= [];
138+
phaseIdsToLoad.add(dep);
139+
print('($_runningAtPhases, $id) add $dep to _idsToLoad at 0');
131140
}
141+
print('Ehm: $thisPhaseIdsToLoad --- $_idsToLoad');
132142
} else {
133143
// It's a generated source that has not yet been generated. Mark it for
134144
// loading later.
135-
(_assetDepsToLoadByPhase[assetDeps.values.last.expiresAfter! + 1] ??=
136-
{})
145+
print('($_runningAtPhases, $id) Add $idToLoad to IDs to load later');
146+
(_idsToLoad[assetDeps.values.last.expiresAfter! + 1] ??= [])
137147
.add(idToLoad);
138148
}
149+
150+
_idsToLoad[firstPhase]!.remove(idToLoad);
139151
}
152+
153+
print('($_runningAtPhases, $id) returning: $_idsToLoad');
140154
}
141155

142156
/// Computes [_cycles] for all [_newAssets] at phase 0, then for all assets
@@ -170,7 +184,9 @@ class LibraryCycleGraphLoader {
170184

171185
// Edges for strongly connected components computation.
172186
Iterable<AssetId> edgesFromId(AssetId id) {
187+
print('Edges for $id');
173188
final deps = _assetDeps[id]!.valueAt(phase: phase).deps;
189+
return deps;
174190

175191
// Check edge against cycles that have already been computed
176192
// at the current `phase`. Newly discovered assets at the same phase
@@ -181,28 +197,29 @@ class LibraryCycleGraphLoader {
181197
);
182198
}
183199

200+
print('Up to $upToPhase, compute $phase');
201+
184202
// Do the strongly connected components computation and convert
185203
// from its output, a list of lists of IDs, to a list of [LibraryCycle].
186204
final newComponentLists = stronglyConnectedComponents(
187205
idsToComputeCyclesFrom,
188206
edgesFromId,
189207
);
190-
final newCycles =
191-
newComponentLists.map((list) {
192-
// Compare to the library cycle computed at `phase - 1`. If the
193-
// cycles are the same size then they must have the same contents,
194-
// because cycles only change by growing as phases progress. In that
195-
// case, reuse the existing [LibraryCycle].
196-
final maybePhasedCycle = _cycles[list.first];
197-
if (maybePhasedCycle != null) {
198-
final value = maybePhasedCycle.valueAt(phase: phase - 1);
199-
if (value.ids.length == list.length) {
200-
return value;
201-
}
202-
}
203-
// The cycle is new or has changed, return a new value.
204-
return LibraryCycle((b) => b..ids.replace(list));
205-
}).toList();
208+
final newCycles = newComponentLists.map((list) {
209+
// Compare to the library cycle computed at `phase - 1`. If the
210+
// cycles are the same size then they must have the same contents,
211+
// because cycles only change by growing as phases progress. In that
212+
// case, reuse the existing [LibraryCycle].
213+
final maybePhasedCycle = _cycles[list.first];
214+
if (maybePhasedCycle != null) {
215+
final value = maybePhasedCycle.valueAt(phase: phase - 1);
216+
if (value.ids.length == list.length) {
217+
return value;
218+
}
219+
}
220+
// The cycle is new or has changed, return a new value.
221+
return LibraryCycle((b) => b..ids.replace(list));
222+
}).toList();
206223

207224
// Build graphs from cycles.
208225
_buildGraphs(phase, newCycles: newCycles);
@@ -212,10 +229,9 @@ class LibraryCycleGraphLoader {
212229
// it gets a new dep that leads back to the cycle then that whole path
213230
// joins the cycle. Get this expirey phase from the graph built by
214231
// `_buildGraphs`.
215-
final expiresAfter =
216-
_graphs[cycle.ids.first]!
217-
.expiringValueAt(phase: phase)
218-
.expiresAfter;
232+
final expiresAfter = _graphs[cycle.ids.first]!
233+
.expiringValueAt(phase: phase)
234+
.expiresAfter;
219235

220236
// Merge the computed cycle into any existing phased value for each ID.
221237
// The phased value can differ by ID: they are in the same cycle at this
@@ -224,21 +240,19 @@ class LibraryCycleGraphLoader {
224240
// Nevertheless, the case in which the phased values are the same is a
225241
// common one, so use a temporary map from old value to new value to
226242
// avoid creating many equal but not identical phased values.
227-
final updatedValueByOldValue =
228-
Map<
229-
PhasedValue<LibraryCycle>?,
230-
PhasedValue<LibraryCycle>
231-
>.identity();
243+
final updatedValueByOldValue = Map<PhasedValue<LibraryCycle>?,
244+
PhasedValue<LibraryCycle>>.identity();
232245

233246
for (final id in cycle.ids) {
234247
final existingCycle = _cycles[id];
235248
_cycles[id] = updatedValueByOldValue.putIfAbsent(existingCycle, () {
236249
if (existingCycle == null) {
237250
return PhasedValue.of(cycle, expiresAfter: expiresAfter);
238251
}
239-
return existingCycle.followedBy(
240-
ExpiringValue<LibraryCycle>(cycle, expiresAfter: expiresAfter),
241-
);
252+
final newValue =
253+
ExpiringValue<LibraryCycle>(cycle, expiresAfter: expiresAfter);
254+
if (existingCycle.values.contains(newValue)) return existingCycle;
255+
return existingCycle.followedBy(newValue);
242256
});
243257
}
244258
}
@@ -315,21 +329,19 @@ class LibraryCycleGraphLoader {
315329
// Nevertheless, the case in which the phased values are the same is a
316330
// common one, so use a temporary map from old value to new value to
317331
// avoid creating many equal but not identical phased values.
318-
final updatedValueByOldValue =
319-
Map<
320-
PhasedValue<LibraryCycleGraph>?,
321-
PhasedValue<LibraryCycleGraph>
322-
>.identity();
332+
final updatedValueByOldValue = Map<PhasedValue<LibraryCycleGraph>?,
333+
PhasedValue<LibraryCycleGraph>>.identity();
323334

324335
for (final idToUpdate in root.ids) {
325336
final oldValue = _graphs[idToUpdate];
326337
_graphs[idToUpdate] = updatedValueByOldValue.putIfAbsent(oldValue, () {
327338
if (oldValue == null) {
328339
return PhasedValue.of(graph.build(), expiresAfter: expiresAfter);
329340
}
330-
return oldValue.followedBy(
331-
ExpiringValue(graph.build(), expiresAfter: expiresAfter),
332-
);
341+
final newValue =
342+
ExpiringValue(graph.build(), expiresAfter: expiresAfter);
343+
if (oldValue.values.contains(newValue)) return oldValue;
344+
return oldValue.followedBy(newValue);
333345
});
334346
}
335347
}
@@ -344,9 +356,20 @@ class LibraryCycleGraphLoader {
344356
AssetDepsLoader assetDepsLoader,
345357
AssetId id,
346358
) async {
347-
await _load(assetDepsLoader, id);
348-
_buildCycles(assetDepsLoader.phase);
349-
return _cycles[id]!;
359+
if (_runningAtPhases.isNotEmpty &&
360+
assetDepsLoader.phase >= _runningAtPhases.last) {
361+
throw StateError('Cannot recurse at phase ${assetDepsLoader.phase}, '
362+
'already running at an equal or earlier phase. '
363+
'Already running at : $_runningAtPhases');
364+
}
365+
_runningAtPhases.add(assetDepsLoader.phase);
366+
try {
367+
await _load(assetDepsLoader, id);
368+
_buildCycles(assetDepsLoader.phase);
369+
return _cycles[id]!;
370+
} finally {
371+
_runningAtPhases.removeLast();
372+
}
350373
}
351374

352375
/// Returns the [LibraryCycleGraph] of [id] at all phases before the
@@ -386,7 +409,7 @@ class LibraryCycleGraphLoader {
386409
String toString() => '''
387410
LibraryCycleGraphLoader(
388411
_assetDeps: $_assetDeps,
389-
_assetDepsToLoadByPhase: $_assetDepsToLoadByPhase,
412+
_assetDepsToLoadByPhase: $_idsToLoad,
390413
_newAssets: $_newAssets,
391414
_cycles: $_cycles,
392415
_graphs: $_graphs,

0 commit comments

Comments
 (0)