Merge remote-tracking branch 'origin/rustify' into worktree-agent-a16b22e34539b810e

# Conflicts:
#	iris/android-app/src/bench_client.rs
#	iris/android-app/src/bench_jni.rs
This commit is contained in:
iris committed 2026-09-06 01:05:18 -04:00
commit 4afc453faa
25 files changed
+838 -205

No files matched your search

@@ -1,8 +1,15 @@
package dev.iris.android.demo;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -68,4 +75,81 @@ public final class IrisView extends RustView {
scroll.addView(text);
activity.setContentView(scroll);
}
private static final String DIAGNOSTICS_OVERLAY_TAG = "iris-diagnostics-overlay";
/**
* The bench build's keyboard diagnostics capture
* (`bench_client.rs`'s `on_insets_changed` /
* `capture_keyboard_diagnostics`, via `bench_jni.rs`'s
* `PlatformHandle::show_diagnostics_overlay`): unlike
* `showRendererError` above, this adds a panel *over* this view
* (`MainActivity`'s `FrameLayout` still holds `IrisView` underneath,
* running) rather than replacing the activity's content, and gives it
* a Copy button and a Close that removes the panel -- so it draws
* (and can be read) whether or not iris itself is still putting
* anything on screen, without abandoning the session that produced
* it. Runs on the UI thread regardless of which thread calls it,
* since the call comes from a background task (a delayed capture
* after the keyboard opens), and touching the view tree off the UI
* thread is undefined.
*/
void showDiagnosticsOverlay(String report) {
Context context = getContext();
if (!(context instanceof Activity)) {
return;
}
Activity activity = (Activity) context;
activity.runOnUiThread(() -> {
ViewGroup parent = (ViewGroup) getParent();
if (parent == null) {
return;
}
View existing = parent.findViewWithTag(DIAGNOSTICS_OVERLAY_TAG);
if (existing != null) {
parent.removeView(existing);
}
float density = activity.getResources().getDisplayMetrics().density;
int pad = (int) (16 * density);
LinearLayout overlay = new LinearLayout(activity);
overlay.setTag(DIAGNOSTICS_OVERLAY_TAG);
overlay.setOrientation(LinearLayout.VERTICAL);
overlay.setBackgroundColor(0xEE000000);
overlay.setPadding(pad, pad, pad, pad);
TextView text = new TextView(activity);
text.setText(report);
text.setTextIsSelectable(true);
text.setTextColor(0xFFFFFFFF);
ScrollView scroll = new ScrollView(activity);
scroll.addView(text);
overlay.addView(scroll, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
LinearLayout buttonRow = new LinearLayout(activity);
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
buttonRow.setPadding(0, pad, 0, 0);
Button copy = new Button(activity);
copy.setText("Copy");
copy.setOnClickListener(v -> {
ClipboardManager clipboard =
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
clipboard.setPrimaryClip(ClipData.newPlainText("iris diagnostics", report));
}
});
Button close = new Button(activity);
close.setText("Close");
close.setOnClickListener(v -> parent.removeView(overlay));
buttonRow.addView(copy);
buttonRow.addView(close);
overlay.addView(buttonRow);
parent.addView(overlay, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
});
}
}
+114 -19
View File
@@ -114,10 +114,17 @@ pub struct BenchClient {
running: bool,
/// The keyboard phase's own confirmation channel -- updated from
/// `on_insets_changed` (the platform's own answer for whether the IME
/// is actually visible, per `LogicalInsets::ime_bottom`), read from the
/// is actually visible, per `WindowInsets::ime_bottom`), read from the
/// benchmark's spawned task via the shared `Arc<Mutex<_>>` rather than
/// `ctx.update`, since neither side needs the widget tree for this.
ime_state: Arc<Mutex<ImeState>>,
/// Edge-triggers the keyboard diagnostics capture below -- set on the
/// first `on_insets_changed` where `ime_bottom > 0.0`, cleared on the
/// first where it is not, so opening the keyboard fires this once
/// rather than on every insets update while it stays open (a rotation
/// or a status-bar change with the keyboard already up would otherwise
/// re-fire it).
keyboard_was_visible: bool,
}
/// See `BenchClient::ime_state`'s doc. `shown_events`/`hidden_events`
@@ -241,7 +248,7 @@ impl AndroidAppState for BenchClient {
let tree = (
top_bar,
content.height(rest(2)),
report_display.height(rest(1)).pad(8),
report_display.height(rest(1)).pad(dp(8)),
)
.span(Dir::DOWN)
.add_strong(rsc)
@@ -277,6 +284,7 @@ impl AndroidAppState for BenchClient {
last_report: None,
running: false,
ime_state: Arc::new(Mutex::new(ImeState::default())),
keyboard_was_visible: false,
};
let (backlog, stream_tail) = parse_fixture();
@@ -305,38 +313,93 @@ impl AndroidAppState for BenchClient {
/// field comment. Rebuilds the row rather than mutating a stored
/// `Padding` in place, since nothing here holds a handle to one.
///
/// Also the keyboard phase's own confirmation signal: `ime_bottom`
/// crossing 0<->positive is the platform's own answer for whether the
/// IME actually opened or closed, fired from the real insets
/// callback rather than assumed from having called `show_ime`/
/// `hide_ime` -- see `ime_state`'s doc.
/// Also two things downstream of the same `ime_bottom` transition:
/// **the keyboard phase's own confirmation signal** (`ime_state`'s
/// doc -- the platform's own answer for whether the IME actually
/// opened or closed, rather than assumed from having called
/// `show_ime`/`hide_ime`), and **the trigger for the keyboard
/// diagnostics capture** (RUST.md's P0 box): the IME resizing the
/// surface is exactly the case a previous commit found wiped text,
/// and Iris needs a way to get a report off the phone even if that
/// (or some other keyboard-triggered regression) is still happening
/// on the build she is holding -- `capture_keyboard_diagnostics`
/// below fires ~500ms after the keyboard becomes visible, once per
/// keyboard opening, and shows its report in a plain overlay view
/// that draws independently of whatever iris itself is doing.
fn on_insets_changed(
&mut self,
rsc: &mut AndroidRsc<Self>,
insets: iris::android::LogicalInsets,
insets: iris::android::WindowInsets,
) {
let controls = bench_controls(rsc, insets.top);
(self.top_bar)(rsc).set(controls);
let ime_visible = insets.ime_bottom > 0.0;
let mut ime = self.ime_state.lock().unwrap();
let now_visible = insets.ime_bottom > 0.0;
if now_visible && !ime.visible {
if ime_visible && !ime.visible {
ime.shown_events += 1;
}
if !now_visible && ime.visible {
if !ime_visible && ime.visible {
ime.hidden_events += 1;
}
ime.visible = now_visible;
ime.visible = ime_visible;
drop(ime);
if ime_visible && !self.keyboard_was_visible {
self.keyboard_was_visible = true;
let redraw = rsc.tasks.redraw_handle();
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);
});
redraw.request_redraw();
});
} else if !ime_visible {
self.keyboard_was_visible = false;
}
}
}
/// How long to wait after the keyboard becomes visible before capturing
/// diagnostics -- long enough that the resize, the reported wipe (if it is
/// still happening) and a couple of frames have all had time to land, per
/// AGENTS.md's "so that operations that finish in milliseconds have states
/// on the way that nothing can observe" reasoning applied the other way:
/// this wants to observe the state *after* the transition settles, not
/// mid-flight.
const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500;
type Rsc = AndroidRsc<BenchClient>;
/// `top_pad` is the status-bar inset in logical units (0.0 until
/// 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: UiColor = UiColor::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.
///
/// **Backed by an opaque rect the full size of the row, not just the three
/// buttons.** Iris's phone report (docs/RUST.md's P0 box, screenshots on
/// build a9232ac): "the header buttons have nothing behind them and
/// overlap the transcript text" -- before this, only each button's own
/// `rect(...)` painted anything, so the gaps between and around them (and
/// the status-bar strip above them) showed whatever was one layer back
/// (`CLEAR_COLOR`, black), and the row's true height was three
/// physical-pixel-sized (`abs`, not `dp`) button boxes rather than the
/// density-correct size the transcript below was already using post-P0 --
/// exactly what reads as "overlap" once the two disagree. Fixed two ways
/// together: a `HEADER_SURFACE` rect stacked behind the whole row (this
/// function), and every size below moved from a bare number (physical
/// pixels) to `dp(...)` (IRIS_TODO.md's density-independent length unit),
/// so the row's reserved height in the outer `Span::DOWN`
/// (`AndroidAppState::new`) matches what is actually painted.
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
let run_rect = rect(Color::rgb(40, 70, 40))
.on(
@@ -351,7 +414,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
wtext("Run benchmark").size(18).text_align(Align::CENTER),
)
.stack()
.pad(8)
.pad(dp(8))
.add(rsc);
let copy_rect = rect(Color::rgb(50, 50, 60))
@@ -367,7 +430,7 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
wtext("Copy report").size(18).text_align(Align::CENTER),
)
.stack()
.pad(8)
.pad(dp(8))
.add(rsc);
let diag_rect = rect(Color::rgb(60, 45, 70))
@@ -383,12 +446,14 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
wtext("Diagnostics").size(18).text_align(Align::CENTER),
)
.stack()
.pad(8)
.pad(dp(8))
.add(rsc);
(run, copy, diagnostics)
.span(Dir::RIGHT)
.height(56)
let buttons = (run, copy, diagnostics).span(Dir::RIGHT).add(rsc);
(rect(HEADER_SURFACE), buttons)
.stack()
.height(dp(56))
.pad(Padding::top(top_pad))
.add_strong(rsc)
.any()
@@ -428,6 +493,36 @@ impl BenchClient {
self.last_report = Some(report);
}
/// The keyboard's own diagnostics capture -- see `on_insets_changed`'s
/// doc comment. Reuses `show_diagnostics`'s exact report (so it is the
/// same text the on-screen `Diagnostics` button produces, plus the
/// per-frame log `FrameReport` already keeps around the resize --
/// `frame_report.report()` above covers "the frames around the
/// resize" without a second accounting mechanism), then does three
/// things the button does not: logs it (so a `logcat` pull gets it
/// even if nothing on screen does), copies it to the clipboard
/// unprompted, and shows it in the shell's plain overlay view, which
/// draws independently of iris's own renderer -- the whole point,
/// since the renderer is exactly what might be in the wiped state
/// this exists to report on.
fn capture_keyboard_diagnostics(&mut self, rsc: &mut Rsc) {
self.show_diagnostics(rsc);
let Some(report) = self.last_report.clone() else {
return;
};
log::info!("iris keyboard diagnostics:\n{report}");
let Some(platform) = &self.platform else {
log::info!("iris keyboard diagnostics: no platform handle, can't reach the shell");
return;
};
if platform.copy_to_clipboard("iris keyboard diagnostics", &report) {
log::info!("iris keyboard diagnostics: copied to clipboard");
} else {
log::info!("iris keyboard diagnostics: clipboard copy failed");
}
platform.show_diagnostics_overlay(&report);
}
fn copy_report(&mut self) {
let Some(report) = &self.last_report else {
log::info!("iris bench report: nothing to copy -- run the benchmark first");
+27
View File
@@ -222,4 +222,31 @@ impl PlatformHandle {
.ok()
}
}
/// Shows `report` in the shell's plain-view diagnostics overlay
/// (`IrisView.showDiagnosticsOverlay`) -- a real `TextView` plus Copy
/// and Close controls, added over whatever iris itself is drawing
/// rather than replacing it (unlike `android::view::show_renderer_error`,
/// which exists for the case the renderer can never recover from and
/// intentionally never returns). Called from a background task after
/// the keyboard-open delay (`bench_client.rs`'s `on_insets_changed`),
/// so the Java side hops onto the UI thread itself before touching the
/// view tree -- see that method's own comment.
pub fn show_diagnostics_overlay(&self, report: &str) -> bool {
self.try_show_diagnostics_overlay(report).is_some()
}
fn try_show_diagnostics_overlay(&self, report: &str) -> Option<()> {
let mut guard = self.vm.attach_current_thread().ok()?;
let env: &mut JNIEnv = &mut guard;
let jreport = env.new_string(report).ok()?;
env.call_method(
self.view.as_obj(),
"showDiagnosticsOverlay",
"(Ljava/lang/String;)V",
&[JValue::Object(jreport.as_ref())],
)
.ok()?;
Some(())
}
}