fix(registry): one clock for association and reaping
FrameScope::candidates() filtered associable tracks on the tracker clock (now_) while reap_locked() retired them on the matcher's evidence watermark (evidence_through_). Because the tracker runs ahead of the matcher (backpressure turns channel depth into lag, AR-004), a track the registry had already reaped on the watermark could still be offered for association on the tracker clock; the matcher's later vote then landed on an erased id and was counted as a dropped vote, silently under-reporting presence. Rare live (small lag), routine in replay where the Python source drives the tracker far ahead of the matcher. candidates() now filters on the same clock reaping uses (awaits_evidence_ ? evidence_through_ : now_) with the same track_extinction_sec horizon, so the offered pool and the live pool are identical: nothing is offered past its reap horizon, nothing is reaped while still offerable. This also cannot reintroduce the older zombie-merge bug (offering on a looser horizon than the reap), because the horizon is now identical rather than looser. track_extinction_sec is the tracker's association window, so it must be evaluated on the horizon the registry retires tracks on.
This commit is contained in:
+31
-15
@@ -181,29 +181,45 @@ public:
|
|||||||
/// alone. There is no separate revival path (AR-008).
|
/// alone. There is no separate revival path (AR-008).
|
||||||
///
|
///
|
||||||
/// TRACES: AR-008, AR-013 | SR-002
|
/// TRACES: AR-008, AR-013 | SR-002
|
||||||
/// Filtered on the TRACKER's clock, deliberately, while reaping runs on
|
/// Association and reaping share ONE clock — the evidence watermark when a
|
||||||
/// the matcher's evidence watermark. The two answer different questions
|
/// matcher is attached, the tracker clock otherwise (they coincide when
|
||||||
/// and must not share an answer:
|
/// there is only one). `candidates()` and `reap_locked()` apply the SAME
|
||||||
|
/// `track_extinction_sec` horizon against that clock, so the offered pool
|
||||||
|
/// and the live pool are the same set:
|
||||||
///
|
///
|
||||||
/// "may this detection link to that track?" — a tracking question,
|
/// offered ⟺ (clock - last_seen) ≤ track_extinction_sec
|
||||||
/// asked now, about a box observed `track_extinction_sec` ago.
|
/// reaped/erased ⟺ (clock - last_seen) > track_extinction_sec
|
||||||
/// "is that track finished, so its claim can be emitted?" — a presence
|
|
||||||
/// question, which cannot be answered until every vote is in.
|
|
||||||
///
|
///
|
||||||
/// Conflating them makes the result depend on node speed in one
|
/// This closes two symmetric failures. (1) Offering on the tracker's clock
|
||||||
/// direction or the other. Reaping on the tracker's clock closed tracks
|
/// (ahead of the watermark) let a face associate onto a track the registry
|
||||||
/// before their votes arrived. Deferring association to the evidence
|
/// had ALREADY reaped on the watermark; the vote then landed on a dead id
|
||||||
/// clock — which is what deferring the erase alone did — left retired
|
/// and was dropped (record_vote → dropped_votes_). Rare live (small lag),
|
||||||
/// tracks in the pool for as long as the matcher lagged, so a new face
|
/// but replay runs the tracker far ahead of the matcher and lost ~0.3% of
|
||||||
|
/// votes. (2) Historically, offering on a LOOSER horizon than the reap left
|
||||||
|
/// retired tracks in the pool while the matcher lagged, so a new face
|
||||||
/// re-associated onto a long-dead track and two people merged into one
|
/// re-associated onto a long-dead track and two people merged into one
|
||||||
/// window. Measured: 5 actors / 16 windows at channel depth 32 against
|
/// window (measured: 5 actors/16 windows at depth 32 vs 3/5 at depth 10322).
|
||||||
/// 3 actors / 5 windows at depth 10322, from identical input.
|
/// A single clock and a single threshold make both impossible: nothing is
|
||||||
|
/// offered past its reap horizon, nothing is reaped while still offerable.
|
||||||
std::vector<Track*> candidates() {
|
std::vector<Track*> candidates() {
|
||||||
std::vector<Track*> out;
|
std::vector<Track*> out;
|
||||||
out.reserve(reg_.tracks_.size());
|
out.reserve(reg_.tracks_.size());
|
||||||
|
// Filter association on the SAME clock reaping uses (the evidence
|
||||||
|
// watermark when a matcher is attached, else the tracker clock). The
|
||||||
|
// two used to differ deliberately — the tracker offered on now_ while
|
||||||
|
// the registry reaped on evidence_through_ — but that let the tracker
|
||||||
|
// associate a face onto a track the registry had already reaped on the
|
||||||
|
// watermark, whose vote then landed on a dead id and was dropped
|
||||||
|
// (record_vote → dropped_votes_). In the live pipeline the lag is tiny
|
||||||
|
// so it rarely bit; in replay the Python source runs the tracker far
|
||||||
|
// ahead of the matcher and ~0.3% of votes were lost. One clock for both
|
||||||
|
// "may this associate?" and "is this reaped?" closes the race: a track
|
||||||
|
// past the horizon is neither offered nor reaped-out-from-under a vote.
|
||||||
|
const double clock =
|
||||||
|
reg_.awaits_evidence_ ? reg_.evidence_through_ : reg_.now_;
|
||||||
for (auto& [id, t] : reg_.tracks_) {
|
for (auto& [id, t] : reg_.tracks_) {
|
||||||
if (t.last_seen &&
|
if (t.last_seen &&
|
||||||
(reg_.now_ - *t.last_seen) > reg_.cfg_.track_extinction_sec)
|
(clock - *t.last_seen) > reg_.cfg_.track_extinction_sec)
|
||||||
continue; // retired from association; still awaiting evidence
|
continue; // retired from association; still awaiting evidence
|
||||||
out.push_back(&t);
|
out.push_back(&t);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user