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, {});
out.cancelled = true;
}
// The matching frame was already folded, at the moment it matched.
// This picks up any further frames the finger stayed down for.
if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames);
// NO HARVEST HERE. The matching frame was already folded, at the
// moment it matched, and that one costs a single command. Harvesting
// 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;
}
@ -1925,10 +1935,13 @@ private:
else if (o.matched) ev->status = "verify-match";
else ev->status = "verify-no-match";
PostEvent(std::move(ev));
// AFTER the verdict is on its way to the client, never before:
// persisting a learned template is ~350 ms of gpfile and RPMB
// traffic and it must not sit on the unlock path. Stock defers
// it the same way, with a timer.
// AFTER the verdict is on its way to the client, never before.
// BOTH of these are off the unlock path deliberately: the
// harvest is up to eight capture-and-fold round trips and the
// 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();
break;
}