iris: redrawing one widget cost O(its own primitives squared)
Iris's report was that expanding a tool card holding a long,
horizontally-scrolling edit lags on her phone. The cause is not text
layout: shaping and rasterising a 51,200-glyph block is 20ms, and the
frame that drew it took 1.37 seconds.
A widget redrawn in place frees every primitive it owned and writes
fresh ones. Freeing compacts each layer's draw order with swap_remove,
so ~N primitives are renumbered, and finding the handle to renumber was
a linear scan of everything that widget drew -- O(N^2) in the widget's
own primitive count. A paragraph never notices; one text widget holding
a whole old_string and new_string is every glyph in the card.
The arena now records, per slot, where that slot's handle sits in its
owner's ActiveData::primitives, written at the one place a handle is
taken (Painter::own), and apply_free indexes straight to it.
50,000 glyphs, redrawn: before 636ms after 2.4ms
per glyph: before 12.7us after 0.043us, flat in N
benches/message_list.rs gains scenario (g) for it, reporting per-glyph
because flat is the pass condition and a total hides it. That file had
also stopped running entirely: scenarios (a) and (e) built a LazySpan
with no mask around it, which the span now asserts against, so the
benchmark panicked on its second line. Fixed here too.
Also, on Iris's instruction: the copied report no longer inlines a tail
of the app log. Dev Updater's Runtime tab reads the same ring through
devlog's provider, so it was the same lines twice; the diagnostics pane
still names the provider's authority to read them from.
This commit is contained in:
1 parent
4fdabc39d0
commit
1318e149f5
8 files changed
+241
-97
No files matched your search
+12
-73
@@ -9,14 +9,18 @@
|
||||
//! of whichever platform logger was already installed (`android_logger`,
|
||||
//! `env_logger`) rather than instead of it -- see [`RingLogger`].
|
||||
//!
|
||||
//! Two consumers, both reading the same ring rather than each keeping
|
||||
//! their own: the bench app's `Copy report`/`Diagnostics` (which reads
|
||||
//! [`LogRing::tail_text`] and [`LogRing::summary`]) and whatever hands the
|
||||
//! log out of the process -- on Android, the `DevLogProvider` Dev Updater
|
||||
//! queries, which reads [`LogRing::since`] and [`LogRing::newest_seq`].
|
||||
//! That is why reading does not consume: a line already handed over must
|
||||
//! still be in the report, and a report taken twice must say the same
|
||||
//! thing.
|
||||
//! Three consumers, all reading the same ring rather than each keeping
|
||||
//! their own: whatever hands the log out of the process -- on Android, the
|
||||
//! `DevLogProvider` Dev Updater queries, which reads [`LogRing::since`]
|
||||
//! and [`LogRing::newest_seq`] -- the bench app's diagnostics pane, which
|
||||
//! only counts it ([`LogRing::summary`]), and the panic hook
|
||||
//! ([`LogRing::try_tail_text`]). That is why reading does not consume: a
|
||||
//! line already handed over must still be readable, and a report taken
|
||||
//! twice must say the same thing.
|
||||
//!
|
||||
//! Nothing inlines the log into a copied report any more (2026-09-08):
|
||||
//! Dev Updater reads it directly, so a second copy on the clipboard was
|
||||
//! the same lines twice.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
@@ -32,15 +36,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
pub const DEFAULT_MAX_LINES: usize = 2000;
|
||||
pub const DEFAULT_MAX_BYTES: usize = 256 * 1024;
|
||||
|
||||
/// How many of the ring's newest lines [`LogRing::tail_text`] includes.
|
||||
/// Sized for a phone's share sheet rather than for the ring itself: 150
|
||||
/// lines of `HH:MM:SS.mmm LEVEL target: message` is a few KiB, comfortably
|
||||
/// short of whatever made pasting the full (up to 2000-line) ring into a
|
||||
/// chat's message box laggy on Iris's phone. The full ring is still
|
||||
/// reachable through `devlog`'s provider, so this only bounds what a
|
||||
/// report inlines.
|
||||
pub const COPY_REPORT_TAIL_LINES: usize = 150;
|
||||
|
||||
/// One recorded line. `seq` is assigned by the ring and only ever
|
||||
/// increases, so a reader that remembers where it got to can ask for what
|
||||
/// came after -- and a gap in the sequence is exactly the lines the bound
|
||||
@@ -249,35 +244,6 @@ impl LogRing {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// The newest `max_lines` lines, formatted, with a first line naming
|
||||
/// how many older ones were left out of *this* text when the ring held
|
||||
/// more than that -- what `Copy report` appends instead of
|
||||
/// [`Self::to_text`].
|
||||
///
|
||||
/// Iris's own report: pasting the full ring (over a thousand lines on
|
||||
/// a session that ran with tracing on) into a phone's message box was
|
||||
/// what "causes a lot of lag" meant (docs/IRIS_TODO.md, 2026-09-07
|
||||
/// night) -- nothing is actually lost, since `devlog`'s provider still
|
||||
/// hands Dev Updater's Runtime tab the whole ring; this only caps what
|
||||
/// gets inlined into a share.
|
||||
pub fn tail_text(&self, max_lines: usize) -> String {
|
||||
let lines = self.snapshot();
|
||||
if lines.len() <= max_lines {
|
||||
return lines
|
||||
.iter()
|
||||
.map(LogLine::format)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
}
|
||||
let omitted = lines.len() - max_lines;
|
||||
let tail = lines[lines.len() - max_lines..]
|
||||
.iter()
|
||||
.map(LogLine::format)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("{omitted} earlier lines omitted; full log in Dev Updater's Runtime tab\n{tail}")
|
||||
}
|
||||
|
||||
/// The newest `max_lines` lines, formatted, or `None` if the ring is
|
||||
/// locked at this instant.
|
||||
///
|
||||
@@ -591,33 +557,6 @@ mod tests {
|
||||
assert_eq!(ring.to_text().lines().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tail_text_is_the_whole_ring_untouched_when_under_the_cap() {
|
||||
let ring = LogRing::new(100, 1 << 20);
|
||||
fill(&ring, 5);
|
||||
assert_eq!(ring.tail_text(150), ring.to_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tail_text_trims_to_the_newest_lines_and_says_how_many_were_left_out() {
|
||||
let ring = LogRing::new(1000, 1 << 20);
|
||||
fill(&ring, 200);
|
||||
let tail = ring.tail_text(150);
|
||||
let mut lines = tail.lines();
|
||||
assert_eq!(
|
||||
lines.next().unwrap(),
|
||||
"50 earlier lines omitted; full log in Dev Updater's Runtime tab"
|
||||
);
|
||||
let rest: Vec<&str> = lines.collect();
|
||||
assert_eq!(rest.len(), 150, "exactly the cap, after the header line");
|
||||
assert!(
|
||||
rest[0].ends_with("line 50"),
|
||||
"the oldest line kept is the 50th, not line 0: {}",
|
||||
rest[0]
|
||||
);
|
||||
assert!(rest.last().unwrap().ends_with("line 199"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_tail_text_gives_the_newest_lines_with_no_header() {
|
||||
let ring = LogRing::new(1000, 1 << 20);
|
||||
|
||||
Reference in new issue
Block a user