Take the harvest off the unlock path too, not just the save

Jorijn ran the same block twice, split five held and five released, and the held
half kept costing about 2.7 seconds more per press in the client while the
daemon's own decide time stayed at 36 to 334 milliseconds either way. The gap was
not the sensor and not the matcher. It was this daemon.

The save was deferred until after the verdict was posted, for exactly the right
reason. The harvest was not, and the harvest is up to eight capture-and-fold
round trips. Holding a finger through the verdict keeps it fed to its frame
limit, so a held press folded eight frames and a released press folded one --
and every one of those folds sat between the match and the client hearing about
it.

Both now run after the event is posted. A finger held through the verdict is
still there a millisecond later, so the harvest loses nothing by waiting, and
the frame that actually matters was already folded at the moment it matched.
This commit is contained in:
Jorijn van der Graaf 2026-09-05 01:30:33 +02:00
commit 07516f2dc6

View file

@ -1574,9 +1574,19 @@ public:
SendCommand(app_, ta::Cmd::Cancel, {}); SendCommand(app_, ta::Cmd::Cancel, {});
out.cancelled = true; out.cancelled = true;
} }
// The matching frame was already folded, at the moment it matched. // NO HARVEST HERE. The matching frame was already folded, at the
// This picks up any further frames the finger stayed down for. // moment it matched, and that one costs a single command. Harvesting
if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames); // the REST of the press is up to eight more capture-and-fold round
// trips, and doing it before returning puts all of them on the
// client's latency path -- which is exactly what happened: a press the
// user held cost ~2.7 s more than one they released, entirely because
// holding kept the harvest fed to its frame limit. Measured
// 2026-09-05: held presses folded 8 frames, released presses folded 1.
//
// The save was already deferred for this reason. The harvest has to be
// deferred with it, and the worker runs both once the verdict is on
// its way. A finger held through the verdict is still there a
// millisecond later, so nothing is lost by waiting.
return out; return out;
} }
@ -1925,10 +1935,13 @@ private:
else if (o.matched) ev->status = "verify-match"; else if (o.matched) ev->status = "verify-match";
else ev->status = "verify-no-match"; else ev->status = "verify-no-match";
PostEvent(std::move(ev)); PostEvent(std::move(ev));
// AFTER the verdict is on its way to the client, never before: // AFTER the verdict is on its way to the client, never before.
// persisting a learned template is ~350 ms of gpfile and RPMB // BOTH of these are off the unlock path deliberately: the
// traffic and it must not sit on the unlock path. Stock defers // harvest is up to eight capture-and-fold round trips and the
// it the same way, with a timer. // save is ~350 ms of gpfile and RPMB traffic. Stock defers its
// save the same way, with a timer, and keeps harvesting while
// the finger is down after the framework has been told.
if (o.matched && g_learn) session_.HarvestTemplate(g_learnMaxFrames);
session_.FlushTemplate(); session_.FlushTemplate();
break; break;
} }