gallery/src/nblog.rs
Greg Pomerantz 5a8284e7a9 Port the gallery from Slint to egui (egui 0.31 / egui_glow / winit 0.30 / glow 0.16)
Slint-on-Mali was conclusively janky at its base level (sltest: 0.33-0.83%
slot drops on a scene with no app logic); egui on the same phone measured
3-7x cleaner (egtest: 0.12%, same referee/scene).

Architecture (see HANDOFF §8.11-8.12):
- winit 0.30 ApplicationHandler on android-activity 0.6, raw EGL (ES3),
  egui_glow::EguiGlow. winit 0.30 never delivers RedrawRequested on
  Android → the frame driver is app-owned: World::tick runs every loop
  iteration from about_to_wait (liveness), render when dirty or
  animating, park ~8 ms (one vblank) at rest.
- Virtualized grid painter (3 cols, visible rows ±4), GPU-resident
  textures, turbojpeg worker pool decoding off-thread.
- CRITICAL pixel-pipeline fix: RGB8→Vec<Color32> conversion happens on
  the WORKER (egui 0.31's from_rgba_unmultiplied is O(n) per-pixel,
  8-60 ms/2 MB on the UI thread → 9.57% drops + phone OOM crash).
  UI thread only MOVES the vector into ColorImage.
- Fling: per-event EMA velocity (frame-based dies at slow touch sampling),
  clamp 14000 px/s; decay exp(-7.42 dt).
- Painter::image uv is NORMALIZED (0..1) — a screen-space uv made every
  cell a solid rectangle (the emulator's solid-color test images hid it).
- Memory caps for a personal phone: full cache 6 entries / 16 MP.

Verified on emulator: grid, scroll, fling, viewer, back-key. Phone
deploy: thumbnails render, 8580 files. Pending: user finger test +
fr_capture phone run (per-pixel fix expected to cut render cost
18.8 ms → <8.3 ms).

Deletes: vendor/ (instrumented Slint tree), shim/, build.rs, gputex,
vkshim, baseline60, main.slint — the Slint zero-jank campaign is
concluded in docs/BUGS/.
2026-10-31 13:50:46 -04:00

114 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Non-blocking logger (Android). `log` calls on the UI thread must NEVER
//! block: logd-pipe backpressure during bursts was observed to block the
//! UI thread 10–40 ms (docs/BUGS/scroll-jank-120hz.md). Records are
//! formatted at the call site, pushed into a bounded channel; a drain
//! thread does the real `__android_log_write` and DROPS records when
//! saturated.
//!
//! `framelog`: FR lines are also mirrored to
//! /sdcard/Download/.gallery_perf.log — the logcat ring buffer rotates
//! every few seconds, which destroys the gap analysis. tools/fr_capture.py
//! pulls this file.
#[cfg(target_os = "android")]
pub mod android {
use std::io::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use log::{Level, Log, Metadata, Record};
extern "C" {
fn __android_log_write(prio: i32, tag: *const u8, msg: *const u8) -> i32;
}
const TAG: &[u8] = b"gallery\0";
const PERF_PATH: &str = "/sdcard/Download/.gallery_perf.log";
fn perf_line(msg: &str) -> bool {
msg.starts_with("FR ") || msg.starts_with("PERF ")
|| msg.starts_with("gallery: [W")
}
pub struct Logger {
tx: mpsc::SyncSender<(i32, String)>,
dropped: AtomicUsize,
}
pub fn init() {
let (tx, rx) = mpsc::sync_channel::<(i32, String)>(4096);
std::thread::Builder::new()
.name("log-drain".into())
.spawn(move || {
// Opened lazily (MANAGE_EXTERNAL_STORAGE may be granted
// later); retried until it works.
let mut perf: Option<std::io::BufWriter<std::fs::File>> = None;
let mut n = 0usize;
for (prio, msg) in rx {
if cfg!(feature = "framelog") && perf_line(&msg) {
if perf.is_none() {
if let Ok(f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(PERF_PATH)
{
perf = Some(std::io::BufWriter::new(f));
}
}
if let Some(w) = perf.as_mut() {
let _ = w.write_all(msg.as_bytes());
let _ = w.write_all(b"\n");
n += 1;
if n >= 32 {
let _ = w.flush();
n = 0;
}
}
}
let b = msg.into_bytes();
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
let mut safe: Vec<u8> = b[..end].iter().copied().filter(|&c| c != b'\n').collect();
safe.push(0);
unsafe {
__android_log_write(prio, TAG.as_ptr(), safe.as_ptr());
}
}
})
.ok();
let logger = Logger { tx, dropped: AtomicUsize::new(0) };
let _ = log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(log::LevelFilter::Info));
}
impl Logger {
fn prio(l: Level) -> i32 {
match l {
Level::Error => 6,
Level::Warn => 5,
Level::Info => 4,
_ => 3,
}
}
}
impl Log for Logger {
fn enabled(&self, m: &Metadata) -> bool {
m.level() <= Level::Info
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let mut s = String::with_capacity(96);
use std::fmt::Write as _;
let _ = write!(&mut s, "{}", record.args());
if self.tx.try_send((Self::prio(record.level()), s)).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
fn flush(&self) {}
}
}
#[cfg(not(target_os = "android"))]
pub fn init() {
// Desktop: the binary installs its own minimal logger.
}