Let a paged-in row wait its turn instead of standing up with the page
The layers took care of the steady state -- the transcript's content re-recorded fifteen times in a thirty-second scroll, and the frame's draw phase sits at 3.5ms. What is left is entirely spikes, and they are pages landing: one of those fifteen recordings took 99.9ms on its own, with the frame after it unable to start. The cause was a rule that read as caution and was not. A row with no measured height was retained whatever its distance, on the grounds that it had just been paged in and was about to be looked at -- but "no measured height" describes the *whole page*, not the near edge of it, so eight hundred events' worth of markdown was shaped inside the frame the page arrived in. A row that has not been measured now stands in at the average of those that have, so it is placed and judged by distance like every other row and is built when the reader comes near it. The average rather than a constant because these run from a one-line note to a screenful, and the average row in a conversation is a fair guess at the next one. Being wrong is cheap here and self-correcting: an estimate is only ever used above the viewport, and this layout hangs from its far end, so a correction up there moves nothing on screen. Checked by scrolling twenty-five swipes into history and back on a real transcript -- rows stand up as they are reached, and the position does not shift as the guesses are replaced by measurements. Second half of the diagnosis from the ai-app-2-6d session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e65c961e1e
commit
53735f8df4
1 file changed
+39
-8
@@ -98,9 +98,35 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun height(seq: Long, height: Int) {
|
internal fun height(seq: Long, height: Int) {
|
||||||
if (heights.put(seq, height) != height) topsStale = true
|
val had = heights.put(seq, height)
|
||||||
|
if (had == height) return
|
||||||
|
measuredTotal += height - (had ?: 0)
|
||||||
|
if (had == null) measuredCount++
|
||||||
|
topsStale = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var measuredTotal = 0L
|
||||||
|
private var measuredCount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How tall to assume a row is before anything has measured it.
|
||||||
|
*
|
||||||
|
* A row that has just been paged in has no height, and something has to stand in for one or it
|
||||||
|
* cannot be placed at all. This used to be answered by keeping every unmeasured row built --
|
||||||
|
* which meant a page of history standing up seventeen screens of markdown inside a single
|
||||||
|
* frame, measured on a Pixel 9 Pro XL as a hundred milliseconds in one go, with the frame after
|
||||||
|
* it unable to start. An estimate lets a paged-in row be a spacer like any other distant row,
|
||||||
|
* and be built when the reader actually comes near it.
|
||||||
|
*
|
||||||
|
* The average of what has been measured, because the average row in a conversation is a good
|
||||||
|
* guess at the next one and a constant is not: these run from a one-line note to a screenful.
|
||||||
|
* Being wrong is cheap and self-correcting -- the estimate is only used above the viewport,
|
||||||
|
* where the content hangs from its far end, so a correction there moves nothing on screen.
|
||||||
|
*/
|
||||||
|
private fun assumed(seq: Long): Int =
|
||||||
|
heights[seq]
|
||||||
|
?: if (measuredCount > 0) (measuredTotal / measuredCount).toInt() else ROW_GUESS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The running total of every row's top edge, recomputed at most once per frame and only after
|
* The running total of every row's top edge, recomputed at most once per frame and only after
|
||||||
* something has actually changed height.
|
* something has actually changed height.
|
||||||
@@ -115,7 +141,7 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
var y = padTop
|
var y = padTop
|
||||||
order.forEachIndexed { index, seq ->
|
order.forEachIndexed { index, seq ->
|
||||||
out[index] = y
|
out[index] = y
|
||||||
y += (heights[seq] ?: 0) + spacing
|
y += assumed(seq) + spacing
|
||||||
}
|
}
|
||||||
tops = out
|
tops = out
|
||||||
topsStale = false
|
topsStale = false
|
||||||
@@ -181,7 +207,7 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
var y = padTop
|
var y = padTop
|
||||||
for (s in order) {
|
for (s in order) {
|
||||||
if (s == seq) return y
|
if (s == seq) return y
|
||||||
y += (heights[s] ?: return null) + spacing
|
y += assumed(s) + spacing
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -225,12 +251,14 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
* of the height it was last measured at, so the transcript's total height does not change and
|
* of the height it was last measured at, so the transcript's total height does not change and
|
||||||
* nothing under the reader moves.
|
* nothing under the reader moves.
|
||||||
*
|
*
|
||||||
* A row that has never been measured has no height to stand in for it and is kept whatever its
|
* A row that has never been measured stands in at the average of those that have, so it is
|
||||||
* distance -- that is a row that has just been paged in, and it is about to be looked at.
|
* placed and judged like any other; see [assumed]. Keeping every unmeasured row instead is what
|
||||||
|
* made a page of history land as one hundred-millisecond frame -- seventeen screens of markdown
|
||||||
|
* shaped at once, because "not measured yet" described the whole page.
|
||||||
*/
|
*/
|
||||||
fun retains(seq: Long): Boolean {
|
fun retains(seq: Long): Boolean {
|
||||||
val index = rowIndex[seq] ?: return true
|
val index = rowIndex[seq] ?: return true
|
||||||
val height = heights[seq] ?: return true
|
val height = assumed(seq)
|
||||||
refreshTops()
|
refreshTops()
|
||||||
val top = tops.getOrNull(index) ?: return true
|
val top = tops.getOrNull(index) ?: return true
|
||||||
val viewportTop = scroll.maxValue - scroll.value
|
val viewportTop = scroll.maxValue - scroll.value
|
||||||
@@ -240,7 +268,7 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The height a row was last measured at, for the spacer that stands in for it. */
|
/** The height a row was last measured at, for the spacer that stands in for it. */
|
||||||
fun heightOf(seq: Long): Int = heights[seq] ?: 0
|
fun heightOf(seq: Long): Int = assumed(seq)
|
||||||
|
|
||||||
/** The height of the visible area, 0 until the first measurement. */
|
/** The height of the visible area, 0 until the first measurement. */
|
||||||
val viewport: Int
|
val viewport: Int
|
||||||
@@ -259,7 +287,7 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
for (s in order) {
|
for (s in order) {
|
||||||
if (y > top) break
|
if (y > top) break
|
||||||
found = s to y
|
found = s to y
|
||||||
y += (heights[s] ?: return null) + spacing
|
y += assumed(s) + spacing
|
||||||
}
|
}
|
||||||
return found?.let { (seq, rowTop) -> ScrollAnchor(seq, top - rowTop) }
|
return found?.let { (seq, rowTop) -> ScrollAnchor(seq, top - rowTop) }
|
||||||
}
|
}
|
||||||
@@ -429,3 +457,6 @@ private fun RetainedRow(
|
|||||||
|
|
||||||
/** How far either side of the screen a row stays built; see [TranscriptScroll.retains]. */
|
/** How far either side of the screen a row stays built; see [TranscriptScroll.retains]. */
|
||||||
private const val RETAIN_SCREENS = 8
|
private const val RETAIN_SCREENS = 8
|
||||||
|
|
||||||
|
/** What a row is assumed to be worth before any of them have been measured. */
|
||||||
|
private const val ROW_GUESS = 800
|
||||||
Reference in new issue
Block a user