Fetch a restore's history in one request, and let RUST_LOG through
Now that a scroll anchor is a seq, the restore knows exactly how far back
it has to reach, so it asks for that span in one request instead of
walking there a page at a time. ai-app-2 suggested it; the arithmetic is
theirs. `read_window` counts lines and a transcript numbers them one per
seq, so the distance to the anchor is the number of events to ask for --
and were seqs ever sparse, that difference is larger than the count,
which overshoots into older history rather than stopping short.
Capped at [RESTORE_PAGE_MAX], and the loop already there is what makes
the cap safe: a span past it comes back in several requests rather than
one, which is what every restore did until now. The bytes are the same
either way -- every row between the anchor and the newest end has to be
loaded for the list to be able to count to it -- so this only trades
round trips against response size.
Measured on a 16,133-event session, restoring to seq 2000 (14,133 events
back, the extreme case): **19 requests before, 5 after.** The realistic
case, a couple of thousand events back, is 4 before and 2 after. At
`--delay 150` the deep one puts the row on screen at 5.7s and the
moderate one at 2.5s, and in both the row does not move once it lands.
Also: `RUST_LOG` did nothing. `with_env_filter("info")` is a fixed
directive that never reads the environment, so the per-request page
diagnostics AGENTS.md tells you to turn on with `RUST_LOG=ai_server=debug`
printed nothing at all -- which reads as the code you are instrumenting
being wrong rather than as the switch being disconnected. It is a
fallback now, so the default is still `info`. Those diagnostics are what
the request counts above were measured with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6079ac15a2
commit
0dc248b9e4
2 files changed
+43
-5
No files matched your search
@@ -113,6 +113,20 @@ private const val HISTORY_SCREENS = 3
|
||||
*/
|
||||
private const val HISTORY_PAGE = 800
|
||||
|
||||
/**
|
||||
* The most events one request of a restore may ask for.
|
||||
*
|
||||
* A restore knows exactly how far back it has to reach, so it asks for that in one request rather
|
||||
* than walking there a page at a time. This bounds the request anyway, because "exactly how far" is
|
||||
* however far the reader had scrolled and there is no bound on that -- and a single response of
|
||||
* arbitrary size is the one shape a phone on a slow tunnel handles worst. At roughly 800 bytes an
|
||||
* event, measured on a real transcript, this is about three megabytes.
|
||||
*
|
||||
* Going past it costs another request rather than anything being missed, so the number only trades
|
||||
* round trips against response size.
|
||||
*/
|
||||
private const val RESTORE_PAGE_MAX = 4000
|
||||
|
||||
/**
|
||||
* Which row was asked to hold its top edge, and how tall it was when it last measured.
|
||||
*
|
||||
@@ -879,7 +893,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
* Reads `items` rather than `rows`: this runs in a coroutine, and `rows` is the composition's
|
||||
* value, which does not change under a running one.
|
||||
*/
|
||||
suspend fun loadOlderPage(): Boolean {
|
||||
suspend fun loadOlderPage(limit: Int = HISTORY_PAGE): Boolean {
|
||||
// The fetch *and* the fold, both off the thread that draws. Only the fetch used to be,
|
||||
// and the fold is the expensive half: `foldEvent` returns a new list per event, so a page
|
||||
// of [HISTORY_PAGE] events is that many copies of a list growing to that length -- around
|
||||
@@ -894,8 +908,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
// `items` read below happens back on the caller's thread, where the write does too.
|
||||
val page =
|
||||
withContext(Dispatchers.IO) {
|
||||
val older =
|
||||
fetchTranscript(settings, summary.id, before = oldestSeq, limit = HISTORY_PAGE)
|
||||
val older = fetchTranscript(settings, summary.id, before = oldestSeq, limit = limit)
|
||||
if (older.isEmpty()) return@withContext null
|
||||
// Folded oldest-first into a list of their own, then put in front: `foldEvent`
|
||||
// merges streaming text into the item before it, so replaying an older page
|
||||
@@ -999,7 +1012,22 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
// conversation every time an active session was reopened.
|
||||
var index = indexOfSeq(anchor.seq)
|
||||
while (moreHistory && (index == null || index >= listItemCount() - 1)) {
|
||||
if (!loadOlderPage()) break
|
||||
// The whole span in one request rather than a page at a time. `read_window`
|
||||
// counts *lines* and a transcript numbers them one per seq, so the distance
|
||||
// back to the anchor is the number of events to ask for -- and were seqs ever
|
||||
// sparse, that difference is larger than the count, which overshoots into
|
||||
// older history rather than stopping short. [HISTORY_PAGE] on top is the
|
||||
// cushion that keeps the anchor's row off the oldest edge, where it would
|
||||
// still grow.
|
||||
//
|
||||
// Capped, and the loop is what makes the cap safe: a span past it comes back
|
||||
// in several requests instead of one, which is what this did for every
|
||||
// restore until now -- thirteen sequential round trips to reopen a session
|
||||
// somebody had read a little way back into, and a spinner for all of them.
|
||||
// The bytes are the same either way, since every row between the anchor and
|
||||
// the newest end has to be there for the list to be able to count to it.
|
||||
val span = oldestSeq - anchor.seq + HISTORY_PAGE
|
||||
if (!loadOlderPage(span.coerceIn(1L, RESTORE_PAGE_MAX.toLong()).toInt())) break
|
||||
index = indexOfSeq(anchor.seq)
|
||||
}
|
||||
// Both writes before this coroutine yields, so the list's first measurement is
|
||||
|
||||
Reference in new issue
Block a user