Close the FX reclaimer's undo span at the state-load hook, and expose the drain's direction
A Ctrl-Z mid-span used to close the pre-undo block over post-undo state; it now terminates at BeginLoadProjectState and stops the drain where it stands. fxReclaimActivity reports Online/Offline/Idle; fxReclaimPending is unchanged.
This commit is contained in:
@@ -59,6 +59,14 @@ struct FakeHost {
|
||||
int invalidateAfterWrite = -1;
|
||||
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
||||
|
||||
// A Ctrl-Z arriving through the same pump: what fxReclaimAbandonForStateLoad does,
|
||||
// fired on the Nth successful write. The direction probe rides the same hook because
|
||||
// the property it checks is the same one — what a caller re-entering mid-drain sees.
|
||||
int abandonAfterWrite = -1;
|
||||
int probeActivityAfterWrite = -1;
|
||||
int spanClosesMidDrain = 0;
|
||||
FxReclaimActivity activityMidDrain = FxReclaimActivity::Idle;
|
||||
|
||||
bool rolling = false; // the transport gate's input
|
||||
|
||||
// Stands in for the shell's one span instance: fx_reclaim.cpp opens it on the first
|
||||
@@ -70,7 +78,10 @@ struct FakeHost {
|
||||
double nowMs() const { return clock; }
|
||||
bool transportRolling() const { return rolling; }
|
||||
|
||||
// Both host entry points refuse once abandoned, exactly as fx_reclaim.cpp's do: this
|
||||
// one ends every REMAINING track, writeOffline below ends the one being written.
|
||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||
if (reentrant && reentrant->abandoned()) return false;
|
||||
auto it = chains.find(guid);
|
||||
if (it == chains.end()) return false;
|
||||
out.clear();
|
||||
@@ -79,6 +90,7 @@ struct FakeHost {
|
||||
}
|
||||
|
||||
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||
if (reentrant && reentrant->abandoned()) return false;
|
||||
auto it = chains.find(guid);
|
||||
if (it == chains.end()) return false;
|
||||
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
|
||||
@@ -97,6 +109,12 @@ struct FakeHost {
|
||||
reentrant->discard();
|
||||
reentrant->invalidate();
|
||||
}
|
||||
if (reentrant && static_cast<int>(writeLog.size()) == probeActivityAfterWrite)
|
||||
activityMidDrain = reentrant->activity(clock, rolling);
|
||||
if (reentrant && static_cast<int>(writeLog.size()) == abandonAfterWrite) {
|
||||
if (span && span->end()) ++spanClosesMidDrain; // closeSpan, then the stop
|
||||
reentrant->abandon();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1197,6 +1215,160 @@ static void testADrainNoSwitchOpenedMintsOnePointOfItsOwn() {
|
||||
CHECK(!span.open());
|
||||
}
|
||||
|
||||
// -- 11. the state-load terminator ------------------------------------------------------
|
||||
|
||||
static void testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack() {
|
||||
// The contrast with testAReentrantInvalidationReachesNothingTheDrainIsUsing above,
|
||||
// which finishes its track's remaining three writes: an ordinary invalidation only
|
||||
// says the model moved, while a state load says the PROJECT is being replaced under
|
||||
// us and the block covering these writes has already closed. So a step is atomic only
|
||||
// up to the abandon — the half-parked chain that leaves is the accepted cost.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}},
|
||||
{"{T2}", {"{FX-C}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
host.chains["{T2}"] = onlineChain({"{FX-C}"});
|
||||
host.stepCostMs = 1.0; // cheap enough that only the abandon can stop the tick
|
||||
|
||||
UndoSpan span; // what a real switch left open
|
||||
CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9));
|
||||
host.span = &span;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"});
|
||||
q.run(host); // observes both parked at t=0
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
host.reentrant = &q;
|
||||
host.abandonAfterWrite = 1;
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
|
||||
CHECK(host.writeLog == std::vector<std::string>{"{T1}:0=off"});
|
||||
CHECK(host.offlineAt("{T1}", 0));
|
||||
CHECK(!host.offlineAt("{T1}", 1)); // the same track, mid-chain — the partial park
|
||||
CHECK(!host.offlineAt("{T2}", 0)); // and every track after it, untouched
|
||||
|
||||
CHECK(host.spanClosesMidDrain == 1);
|
||||
CHECK(!span.open());
|
||||
CHECK(r.aborted);
|
||||
CHECK(!r.settled);
|
||||
CHECK(spanEndsAfter(r)); // the tick's own close still fires...
|
||||
CHECK(!span.end()); // ...and finds nothing: the terminator got there first
|
||||
|
||||
// The wakeup stands and the work is owed again: going idle here would strand the
|
||||
// half-parked chain for the rest of the session.
|
||||
CHECK(q.stale());
|
||||
CHECK(q.pending());
|
||||
|
||||
// ...and the stop belonged to that tick alone. The reloaded project's own drain writes.
|
||||
host.abandonAfterWrite = -1;
|
||||
q.adopt(derive(vm, {"{T1}", "{T2}"}));
|
||||
CHECK(q.run(host).writes == 2); // {T1}'s remaining slot, and {T2}'s whole chain
|
||||
CHECK(host.offlineAt("{T1}", 1));
|
||||
CHECK(host.offlineAt("{T2}", 0));
|
||||
}
|
||||
|
||||
// -- 12. the direction the drain is moving ----------------------------------------------
|
||||
|
||||
static void testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold() {
|
||||
// The panel bug this surface exists for: `pending` is true from the instant of the
|
||||
// switch, but for the whole kParkHoldMs the only thing eligible is the RETURNING
|
||||
// chains' online work — an indicator driven off the queue's mere contents would name
|
||||
// parking while the drain is doing the opposite.
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{Z-back}", kArrangeModeId); // active; owed an online
|
||||
vm.membership().tag("{A-park}", kDesignModeId); // parked; owed an offline
|
||||
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
|
||||
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
|
||||
|
||||
FakeHost host;
|
||||
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
|
||||
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
|
||||
host.stepCostMs = 1.0;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"});
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online);
|
||||
|
||||
CHECK(q.run(host).writes == 1); // the online item only — the offline is still held
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).settled);
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
}
|
||||
|
||||
static void testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures() {
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
// Idle, not Offline: nothing may move, and the wait is the reason the span is allowed
|
||||
// to stay open across it. Reporting Offline here is what would light the indicator for
|
||||
// 3s before a single plugin moves.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
CHECK(!q.run(host).settled); // the hold's origin is set by this tick's observe
|
||||
CHECK(host.writeLog.empty());
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
}
|
||||
|
||||
static void testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline() {
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{Z-back}", kArrangeModeId);
|
||||
vm.membership().tag("{A-park}", kDesignModeId);
|
||||
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
|
||||
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
|
||||
|
||||
FakeHost host;
|
||||
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
|
||||
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
|
||||
host.stepCostMs = 1.0;
|
||||
host.rolling = true;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"});
|
||||
// Online is not transport-gated, so it both drains and reads through a rolling
|
||||
// transport — the second half of the panel bug: this work had no indicator at all.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
// The offline half is eligible now and still may not move, so it reads Idle rather
|
||||
// than naming a direction nothing is travelling in.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
CHECK(q.run(host).transportDeferred);
|
||||
CHECK(host.writeLog.size() == 1);
|
||||
|
||||
host.rolling = false;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
}
|
||||
|
||||
static void testTheDirectionHoldsThroughTheDrainsOwnMessagePump() {
|
||||
// A write pumps, and a repaint landing in there polls this. The queue object holds
|
||||
// NOTHING for the drain's whole duration (itemsSeenMidDrain == 0, asserted above), so
|
||||
// without the latch the indicator would read Idle at exactly the moment the plugin
|
||||
// loads the user is waiting through are happening.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
host.stepCostMs = 1.0;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}"});
|
||||
q.run(host);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
host.reentrant = &q;
|
||||
host.probeActivityAfterWrite = 1;
|
||||
CHECK(q.run(host).settled);
|
||||
|
||||
CHECK(host.activityMidDrain == FxReclaimActivity::Offline);
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); // latch released
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||
|
||||
@@ -1240,6 +1412,13 @@ int main() {
|
||||
testOneSwitchAndItsMultiTickParkingAreOneSpan();
|
||||
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
|
||||
|
||||
testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack();
|
||||
|
||||
testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold();
|
||||
testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures();
|
||||
testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline();
|
||||
testTheDirectionHoldsThroughTheDrainsOwnMessagePump();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user