use crate::android::bench_jni::PlatformHandle; use crate::client::transcript_fold::{TranscriptItem, fold_event}; use crate::ui::{self, TranscriptScreen}; use android_view::jni::{JavaVM, objects::GlobalRef}; use event_model::SeqEvent; use iris::android::{AndroidAppState, AndroidUiState}; use iris::prelude::*; use std::{ fs, mem, sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, mpsc, }, time::{Duration, Instant}, }; const STREAM_EVENTS_PER_SEC: u64 = 20; const STREAM_SECONDS: u64 = 20; const LEGACY_CYCLES: usize = 6; const FLING_VELOCITY_PX_S: f32 = 12_000.0; const FLING_COUNT: usize = 8; const FLING_SETTLE_CAP_MS: u64 = 3_000; const FLING_PAUSE_MS: u64 = 300; const TYPE_TEXT: &str = "Benchmarking this transcript screen requires unusually long, \ multisyllabic words so wrapping and reflow are properly exercised: internationalization, \ counterproductiveness, disproportionately, incomprehensibility, deinstitutionalization, \ uncharacteristically, overenthusiastically, misunderstanding, straightforwardness, \ telecommunications, and interdisciplinary collaboration all push a narrow composer field to \ wrap across several lines while the transcript above is pushed upward by the growing \ keyboard-adjacent box, which is exactly what a real reader typing a long message sees \ happening now!!!"; const TYPE_CHAR_MS: u64 = 50; const KEYBOARD_CYCLES: usize = 5; const KEYBOARD_WAIT_MS: u64 = 1_000; const POLL_MS: u64 = 16; const REPORT_MAX_HEIGHT_DP: f32 = 260.0; #[derive(AndroidUiState)] pub struct BenchClient { ui_state: AndroidUiState, content: WeakWidget, report_display: WeakWidget, top_bar: WeakWidget, screen: Option, items: Vec, stream_tail: Vec, platform: Option>, last_report: Option, running: bool, ime_state: Arc>, keyboard_was_visible: bool, last_top_pad: f32, } #[derive(Default)] struct ImeState { visible: bool, shown_events: u32, hidden_events: u32, } fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget { wtext(message.to_string()) .color(PaintId::WHITE) .overflow(TextOverflow::Wrap) .pad(16) .add_strong(rsc) .any() } fn process_cpu_ms() -> Option { // SAFETY: `rusage` is a plain-old-data struct `getrusage` fully // initialises on success; on failure it is never read. unsafe { let mut usage: libc::rusage = mem::zeroed(); if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 { return None; } let user_ms = usage.ru_utime.tv_sec as u64 * 1000 + usage.ru_utime.tv_usec as u64 / 1000; let sys_ms = usage.ru_stime.tv_sec as u64 * 1000 + usage.ru_stime.tv_usec as u64 / 1000; Some(user_ms + sys_ms) } } fn peak_rss_kb() -> Option { fs::read_to_string("/proc/self/status") .ok()? .lines() .find_map(|line| line.strip_prefix("VmHWM:")) .and_then(|rest| rest.trim().strip_suffix("kB")) .and_then(|n| n.trim().parse().ok()) } fn battery_line(samples: &[i32]) -> String { if samples.is_empty() { return " battery current: unavailable on this device".to_string(); } let mean = samples.iter().map(|&v| v as i64).sum::() / samples.len() as i64; let (Some(min), Some(max)) = (samples.iter().min(), samples.iter().max()) else { unreachable!("samples is non-empty, checked above"); }; format!( " battery current: mean {mean}\u{b5}A over {} samples (min {min}, max {max})", samples.len() ) } impl BenchClient { pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { crate::ui::register_fonts(&mut rsc.ui); let content = WidgetPtr::new().add(rsc); let loading = placeholder(rsc, "Loading fixture..."); content(rsc).set(loading); let report_display = wtext("") .editable(EditMode::MultiLine) .text_align(Align::LEFT) .overflow(TextOverflow::Wrap) .size(14) .color(PaintId::WHITE) .attr::(()) .label("Benchmark report") .add(rsc); let top_bar = WidgetPtr::new().add(rsc); let controls = bench_controls(rsc, 0.0); top_bar(rsc).set(controls); let tree = ( top_bar, report_display .pad(dp(8)) .max_height(dp(REPORT_MAX_HEIGHT_DP)), content.height(rest(1)), ) .span(Dir::DOWN) .add_strong(rsc) .any(); ui_state.set_root(rsc, tree); let font = rsc.ui.text.borrow_mut().font_diagnostics(); log::info!( "iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \ bold={:?} italic={:?} mono={:?}", font.families_found, font.default_family, font.default_mono_family, font.regular_resolved, font.bold_resolved, font.italic_resolved, font.mono_resolved, ); let mut client = Self { ui_state, content, report_display, top_bar, screen: None, items: Vec::new(), stream_tail: Vec::new(), platform: None, last_report: None, running: false, ime_state: Arc::new(Mutex::new(ImeState::default())), keyboard_was_visible: false, last_top_pad: 0.0, }; match ui::fixture::build_screen(rsc) { Ok((opened, tree)) => { client.items = opened.items; client.stream_tail = opened.stream_tail; (client.content)(rsc).set(tree); client.screen = Some(opened.screen); } Err(message) => { client.show_message(rsc, &format!("Couldn't fold the bench fixture: {message}")) } } client } } impl AndroidAppState for BenchClient { type Resources = StdRsc; fn platform_ready(&mut self, _rsc: &mut Self::Resources, vm: JavaVM, view: GlobalRef) { self.platform = Some(Arc::new(PlatformHandle::new(vm, view))); } fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool { false } fn on_insets_changed( &mut self, rsc: &mut Self::Resources, insets: iris::android::WindowInsets, ) { if insets.top != self.last_top_pad { self.last_top_pad = insets.top; let controls = bench_controls(rsc, insets.top); (self.top_bar)(rsc).set(controls); } if let Some(screen) = &self.screen { screen .composer .set_bottom_inset(rsc, insets.bottom.max(insets.ime_bottom)); } // The platform's own answer, not `ime_bottom > 0.0` -- see // `iris::android::WindowInsets::ime_bottom`. The height is still // climbing while the keyboard slides in, so a frame or two of a // real opening reads as "closed" when the boolean is inferred from // it, and `shown_events`/`hidden_events` below count transitions. let ime_visible = insets.ime_visible; let mut ime = self.ime_state.lock().unwrap(); if ime_visible && !ime.visible { ime.shown_events += 1; } if !ime_visible && ime.visible { ime.hidden_events += 1; } ime.visible = ime_visible; drop(ime); if ime_visible && !self.keyboard_was_visible { self.keyboard_was_visible = true; rsc.spawn_task(async move |mut ctx| { tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await; ctx.update(|state: &mut BenchClient, rsc| { state.capture_keyboard_diagnostics(rsc); }); }); } else if !ime_visible { self.keyboard_was_visible = false; } } } const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500; type Rsc = StdRsc; /// What a report says about the `iris::input`/`iris::frame` trace, from /// the flag read at the start of what is being reported and again at the /// end. /// /// Three answers rather than two. Those lines are default-off and the /// switch that turns them on is on screen while a benchmark runs, so /// "somebody moved it half way through" is a state that actually happens /// -- and reported as either "on" or "off" it is a confident sentence /// about a log that only covers part of the run. The "on" wording also /// says what it costs, because a traced run fills the ring in seconds and /// a reader looking at a log with nothing else in it should know why. fn trace_line(at_start: bool, at_end: bool) -> String { match (at_start, at_end) { (true, true) => "input/frame trace: on (iris::input and iris::frame lines are in \ the app log, and a traced run fills the ring in seconds)" .to_string(), (false, false) => "input/frame trace: off".to_string(), _ => "input/frame trace: switched during this run, so those lines cover only part \ of it" .to_string(), } } /// The header row's own backdrop -- see `bench_controls`'s doc comment on /// why it needs one at all. A dark neutral rather than pure black /// (`android::render::CLEAR_COLOR`) so the row reads as a distinct panel /// instead of a hole in the background the buttons happen to float in. const HEADER_SURFACE: Srgba8 = Srgba8::new(28, 28, 34, 255); /// `top_pad` is the status-bar inset in physical pixels (0.0 until /// `on_insets_changed` has run once) -- folded in here, rather than /// exposing the unadded builder for a caller to `.pad()` itself, because /// naming that builder's type at each call site is more machinery than a /// top-of-screen padding number is worth. const HEADER_TEXT: f32 = 18.0; const HEADER_ROW_HEIGHT_DP: f32 = 56.0; fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget { let run_rect = rect(Srgba8::rgb(40, 70, 40)) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| { ctx.state.start_benchmark(rsc); }, ) .label("Run benchmark"); let run = ( run_rect, wtext("Run benchmark") .size(HEADER_TEXT) .text_align(Align::CENTER), ) .stack() .pad(dp(8)) .add(rsc); let copy_rect = rect(Srgba8::rgb(50, 50, 60)) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| { ctx.state.copy_report(rsc); }, ) .label("Copy report"); let copy = ( copy_rect, wtext("Copy report") .size(HEADER_TEXT) .text_align(Align::CENTER), ) .stack() .pad(dp(8)) .add(rsc); let diag_rect = rect(Srgba8::rgb(60, 45, 70)) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| { ctx.state.show_diagnostics(rsc); }, ) .label("Diagnostics"); let diagnostics = ( diag_rect, wtext("Diagnostics") .size(HEADER_TEXT) .text_align(Align::CENTER), ) .stack() .pad(dp(8)) .add(rsc); // A switch rather than a button, so its own appearance says which // state it is in: the two `iris::input`/`iris::frame` targets are // default-off (`iris::diagnostics`'s module doc) because a 120Hz // session fills the 2000-line ring in seconds, so "is it on right // now" is the question somebody has while looking at a log that is // either full of trace or has none. let tracing = iris::diagnostics::trace_enabled(); let trace_rect = rect(if tracing { Srgba8::rgb(90, 70, 30) } else { Srgba8::rgb(50, 50, 60) }) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| { ctx.state.toggle_trace(rsc); }, ) .label("Trace input and frames"); let trace = ( trace_rect, wtext(if tracing { "Trace on" } else { "Trace off" }) .size(HEADER_TEXT) .text_align(Align::CENTER), ) .stack() .pad(dp(8)) .add(rsc); let row1 = (run, copy).span(Dir::RIGHT).add(rsc); let row2 = (diagnostics, trace).span(Dir::RIGHT).add(rsc); let buttons = (row1, row2).span(Dir::DOWN).add(rsc); (rect(HEADER_SURFACE), buttons) .stack() .height(dp(2.0 * HEADER_ROW_HEIGHT_DP)) .pad(Padding::top(top_pad)) .add_strong(rsc) .any() } impl BenchClient { fn show_message(&mut self, rsc: &mut Rsc, message: &str) { let widget = placeholder(rsc, message); (self.content)(rsc).set(widget); self.screen = None; } fn rebuild_transcript(&mut self, rsc: &mut Rsc) { let (screen, tree) = ui::build_tree(rsc, ui::fixture::rows(&self.items)); (self.content)(rsc).set(tree); self.screen = Some(screen); } fn show_diagnostics(&mut self, rsc: &mut Rsc) { let report = self.diagnostics_text(rsc); self.report_display(rsc).set(&report); self.last_report = Some(report); } /// Turns the `iris::input`/`iris::frame` trace on or off, redraws the /// switch that says so, and shows the pane that now reports it. fn toggle_trace(&mut self, rsc: &mut Rsc) { let on = !iris::diagnostics::trace_enabled(); iris::diagnostics::set_trace(on); log::info!( "iris diagnostics: input/frame trace {}", if on { "on" } else { "off" } ); let controls = bench_controls(rsc, self.last_top_pad); (self.top_bar)(rsc).set(controls); self.show_diagnostics(rsc); } /// The diagnostics report as text, with no side effect on what is on /// screen -- shared by the `Diagnostics` button (which shows it) and /// the keyboard-open capture (which only logs it), so the two can /// never drift into reporting different things. fn diagnostics_text(&self, rsc: &mut Rsc) -> String { let font = rsc.ui.text.borrow_mut().font_diagnostics(); let frame_report = match self.android_state().frame_report.report() { Some(stats) => format!("{stats}"), None => "no frames recorded yet".to_string(), }; let renderer = match &self.android_state().renderer { Some(renderer) => renderer.diagnostics_report(&font, &frame_report), None => "iris diagnostics: no renderer yet (no surface)".to_string(), }; // Insets must be visible without adb so a missing callback can be // distinguished from a callback reporting zero IME height. format!( "{renderer}\n{}\n{}\n{}\n{}", trace_line( iris::diagnostics::trace_enabled(), iris::diagnostics::trace_enabled() ), self.android_state().insets_report(), // Which server this build talks to, and what to do when the // answer is "none" -- the bench itself opens a checked-in // fixture and needs no server, so this pane is the only place // an enrolment can be seen to have taken. crate::android::enrollment::status_line(), crate::android::app_log::diagnostics_line() ) } fn capture_keyboard_diagnostics(&mut self, rsc: &mut Rsc) { let report = self.diagnostics_text(rsc); log::info!("iris keyboard diagnostics:\n{report}"); } fn copy_report(&mut self, rsc: &mut Rsc) { let Some(platform) = &self.platform else { log::info!("iris bench report: no platform handle, can't reach the clipboard"); return; }; let report = match self.last_report.clone() { Some(report) => report, None => format!( "no benchmark has run yet -- these are the diagnostics instead:\n\n{}", self.diagnostics_text(rsc) ), }; if platform.copy_to_clipboard("iris bench report", &report) { log::info!("iris bench report: copied to clipboard"); } else { log::info!("iris bench report: clipboard copy failed"); } } fn start_benchmark(&mut self, rsc: &mut Rsc) { if self.running { log::info!("iris bench report: already running"); return; } self.running = true; self.android_state_mut().frame_report.reset(); self.report_display(rsc).set("Running benchmark..."); let platform = self.platform.clone(); let stream_tail = self.stream_tail.clone(); let ime_state = self.ime_state.clone(); let platform_hz = platform.as_ref().and_then(|p| p.refresh_rate_hz()); let cpu_start = process_cpu_ms(); // Read at the start as well as the end, because the switch is on // screen while a run is going: a report that only asked afterwards // would say "on" about a run whose first half has no trace in it // -- the inferred answer presented as the measured one. let trace_at_start = iris::diagnostics::trace_enabled(); let run_started_at = Instant::now(); rsc.spawn_task(async move |mut ctx| { // The battery sampler runs for the whole run, once a second, // the benchmark's established battery-sampling cadence // -- via its own JNI-attached thread, not `ctx.update`, since // a sample needs no widget-tree access. let sampler_done = Arc::new(AtomicBool::new(false)); let samples = Arc::new(Mutex::new(Vec::::new())); let sampler = platform.clone().map(|platform| { let done = sampler_done.clone(); let samples = samples.clone(); tokio::spawn(async move { while !done.load(Ordering::Relaxed) { if let Some(value) = platform.battery_current_ua() { samples.lock().unwrap().push(value); } tokio::time::sleep(Duration::from_secs(1)).await; } }) }); let travel = run_fling_phase(&mut ctx).await; let (sent, total) = run_stream_phase(&mut ctx, stream_tail).await; run_type_phase(&mut ctx, &platform).await; let keyboard = run_keyboard_phase(&mut ctx, &platform, &ime_state).await; sampler_done.store(true, Ordering::Relaxed); if let Some(sampler) = sampler { let _ = sampler.await; } let battery = battery_line(&samples.lock().unwrap()); let cpu_line = match (cpu_start, process_cpu_ms()) { (Some(start), Some(end)) => { format!( " process CPU time over this run: {}ms", end.saturating_sub(start) ) } _ => " process CPU time over this run: unavailable".to_string(), }; let rss_line = match peak_rss_kb() { Some(kb) => format!(" peak RSS: {kb}kB"), None => " peak RSS: unavailable (/proc/self/status unreadable)".to_string(), }; let total_seconds = run_started_at.elapsed().as_secs_f64(); ctx.update(move |state: &mut BenchClient, rsc| { state.running = false; let now = Instant::now(); let drawn_hz = state.android_state().frame_report.sustained_frame_hz(); let refresh_hz = match (drawn_hz, platform_hz) { (Some(d), Some(p)) => d.max(p), (Some(d), None) => d, (None, Some(p)) => p, (None, None) => 60.0, }; let hz_line = match (drawn_hz, platform_hz) { (Some(d), Some(p)) if d > p + 5.0 => format!( " (sustained {d:.0}fps, so at least that; the display reported {p:.0}Hz)" ), (Some(d), Some(_)) => format!(" (as the display reports it; drew {d:.0}fps)"), (Some(d), None) => { format!(" (sustained {d:.0}fps; the display would not say)") } (None, Some(_)) => { " (as the display reports it; too few frames to measure)".to_string() } (None, None) => " (assumed -- neither measured nor reported)".to_string(), }; let phase_lines: String = state .android_state() .frame_report .phase_stats(now, refresh_hz) .iter() .map(|p| format!("{p}\n")) .collect(); let per_phase = if phase_lines.is_empty() { String::new() } else { format!("per phase:\n{phase_lines}\n") }; let frames_block = match state.android_state().frame_report.report() { Some(stats) => { let (late, late_pct) = state.android_state().frame_report.late_at_hz(refresh_hz); format!( "frames:\n {} frames over {:.1}s at {:.0}Hz{hz_line} ({:.1}ms \ budget)\n \ late: {late} ({late_pct:.1}%)\n total p50 {:.1}ms p90 {:.1}ms \ p99 {:.1}ms\n worst {:.1}ms\n build_p50 {:.1}ms acquire_p50 \ {:.1}ms submit_p50 {:.1}ms", stats.total_frames, total_seconds, refresh_hz, 1000.0 / refresh_hz as f64, stats.p50.as_secs_f64() * 1000.0, stats.p90.as_secs_f64() * 1000.0, stats.p99.as_secs_f64() * 1000.0, stats.worst.as_secs_f64() * 1000.0, stats.cpu_p50.as_secs_f64() * 1000.0, stats.acquire_p50.as_secs_f64() * 1000.0, stats.gpu_wait_p50.as_secs_f64() * 1000.0, ) } None => "frames:\n no frames recorded".to_string(), }; let scroll_line = format!( " scroll: {LEGACY_CYCLES} cycles ({} swipes, legacy tween), streamed \ {sent}/{total} fixture events", LEGACY_CYCLES * 4 ); let fling_line = format!( " fling: {FLING_COUNT} flings out + {FLING_COUNT} back at \ {FLING_VELOCITY_PX_S}px/s, travel {travel}" ); let type_line = format!( " type: {} characters inserted then deleted, one per {TYPE_CHAR_MS}ms", TYPE_TEXT.chars().count() ); let traced = trace_line(trace_at_start, iris::diagnostics::trace_enabled()); let report = format!( "iris bench report\n{traced}\n{per_phase}{frames_block}\n\nbench:\n\ {fling_line}\n{scroll_line}\n{type_line}\n{keyboard}\n{cpu_line}\n\ {rss_line}\n{battery}" ); log::info!("iris bench report: {report}"); state.report_display(rsc).set(&report); state.last_report = Some(report); }); }); } } /// Runs `f` against the real `BenchClient`/`Rsc` on the main thread (the /// same `ctx.update` every other mutation here goes through) and returns /// its result to the caller's async task. `ctx.update` wakes the UI thread, /// whose task callback drains Iris's update queue before checking whether the /// retained widget tree needs another frame. /// Polls rather than assuming one `POLL_MS` sleep is enough, since a /// slow device's frame callback can lag further than that. async fn read_from_state(ctx: &mut iris::task::TaskCtx, f: F) -> T where T: Send + 'static, F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static, { let (tx, rx) = mpsc::channel(); ctx.update(move |state: &mut BenchClient, rsc| { let _ = tx.send(f(state, rsc)); }); loop { if let Ok(value) = rx.try_recv() { return value; } tokio::time::sleep(Duration::from_millis(POLL_MS)).await; } } async fn run_fling_phase(ctx: &mut iris::task::TaskCtx) -> String { ctx.update(|state: &mut BenchClient, _rsc| { state.android_state_mut().frame_report.mark_phase("fling"); }); ctx.update(|state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.list)(rsc).jump_to_end(); } }); // Lets the next frame's `repair_anchor` resolve `jump_to_end`'s // `anchor = None` into a real slot before `start` is read. tokio::time::sleep(Duration::from_millis(POLL_MS * 2)).await; let start = read_anchor_position(ctx).await; for _ in 0..FLING_COUNT { ctx.update(|state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.list)(rsc).fling(FLING_VELOCITY_PX_S); } }); wait_for_fling_settle(ctx).await; tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await; } let outward = read_anchor_position(ctx).await; for _ in 0..FLING_COUNT { ctx.update(|state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.list)(rsc).fling(-FLING_VELOCITY_PX_S); animate_scroll(screen.list, rsc); } }); wait_for_fling_settle(ctx).await; tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await; } let end = read_anchor_position(ctx).await; format!("start={start} outward={outward} end={end} ticked=frame-loop") } async fn read_anchor_position(ctx: &mut iris::task::TaskCtx) -> String { read_from_state(ctx, |state, rsc| match &state.screen { Some(screen) => (screen.list)(rsc).anchor_position_display(), None => "idx=none".to_string(), }) .await } async fn wait_for_fling_settle(ctx: &mut iris::task::TaskCtx) { let cap = Duration::from_millis(FLING_SETTLE_CAP_MS); let started = Instant::now(); while started.elapsed() < cap { let still_scrolling = read_from_state(ctx, |state, rsc| match &state.screen { Some(screen) => (screen.list)(rsc).is_scrolling(), None => false, }) .await; if !still_scrolling { return; } tokio::time::sleep(Duration::from_millis(POLL_MS)).await; } } async fn run_stream_phase( ctx: &mut iris::task::TaskCtx, stream_tail: Vec, ) -> (usize, usize) { ctx.update(|state: &mut BenchClient, _rsc| { state.android_state_mut().frame_report.mark_phase("stream"); }); ctx.update(|state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.list)(rsc).jump_to_end(); } }); let total = (STREAM_EVENTS_PER_SEC * STREAM_SECONDS) as usize; let mut sent = 0usize; for event in stream_tail.into_iter().take(total) { ctx.update(move |state: &mut BenchClient, rsc| { let old_items = state.items.clone(); state.items = fold_event(&state.items, &event); match state.screen.as_mut() { Some(screen) => screen.apply(rsc, &old_items, &state.items), None => state.rebuild_transcript(rsc), } }); sent += 1; tokio::time::sleep(Duration::from_millis(1000 / STREAM_EVENTS_PER_SEC)).await; } tokio::time::sleep(Duration::from_millis(300)).await; (sent, total) } async fn run_type_phase( ctx: &mut iris::task::TaskCtx, platform: &Option>, ) { ctx.update(|state: &mut BenchClient, _rsc| { state.android_state_mut().frame_report.mark_phase("type"); }); ctx.update(|state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.list)(rsc).jump_to_end(); state.set_focus(Some(screen.composer.field)); } }); if let Some(p) = platform { p.show_ime(); } tokio::time::sleep(Duration::from_millis(300)).await; let mut typed = String::new(); for ch in TYPE_TEXT.chars() { typed.push(ch); let text = typed.clone(); ctx.update(move |state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.composer.field)(rsc).set(&text); } }); tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; } tokio::time::sleep(Duration::from_millis(200)).await; while !typed.is_empty() { typed.pop(); let text = typed.clone(); ctx.update(move |state: &mut BenchClient, rsc| { if let Some(screen) = &state.screen { (screen.composer.field)(rsc).set(&text); } }); tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; } } async fn run_keyboard_phase( ctx: &mut iris::task::TaskCtx, platform: &Option>, ime_state: &Arc>, ) -> String { ctx.update(|state: &mut BenchClient, _rsc| { state .android_state_mut() .frame_report .mark_phase("keyboard"); }); let mut shown = 0; let mut hidden = 0; for _ in 0..KEYBOARD_CYCLES { let before_shown = ime_state.lock().unwrap().shown_events; if let Some(p) = platform { p.show_ime(); } tokio::time::sleep(Duration::from_millis(KEYBOARD_WAIT_MS)).await; if ime_state.lock().unwrap().shown_events > before_shown { shown += 1; } let before_hidden = ime_state.lock().unwrap().hidden_events; if let Some(p) = platform { p.hide_ime(); } tokio::time::sleep(Duration::from_millis(KEYBOARD_WAIT_MS)).await; if ime_state.lock().unwrap().hidden_events > before_hidden { hidden += 1; } } if shown == 0 { format!(" keyboard: could not be shown ({KEYBOARD_CYCLES} attempts, 0 confirmed visible)") } else { format!( " keyboard: shown {shown}/{KEYBOARD_CYCLES}, hidden {hidden}/{KEYBOARD_CYCLES} \ (confirmed via on_insets_changed)" ) } } #[cfg(test)] mod tests { use super::TYPE_TEXT; #[test] fn type_text_is_exactly_600_characters() { assert_eq!(TYPE_TEXT.chars().count(), 600); } }