Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1104,10 +1104,19 @@ jobs:
run: |
results="${{ needs.rust-lint.result }} ${{ needs.harness-unit.result }} ${{ needs.themes-parity.result }} ${{ needs.rust-build.result }} ${{ needs.swift-mac.result }} ${{ needs.gtk-build.result }} ${{ needs.iced-build-e2e.result }} ${{ needs.e2e-gtk.result }} ${{ needs.e2e-mac.result }}"
echo "job results: ${results}"
# Allowlist, not a denylist. A denylist of failure/cancelled let
# `abandoned` through — the status GitHub assigns when its own
# infrastructure kills a job — so a run where 7 of 9 jobs never
# executed reported this gate GREEN. Observed on PR #306 during a
# GitHub "Failed to resolve action download info" incident.
# Anything that is not an actual pass now fails the gate.
for r in ${results}; do
if [ "${r}" = "failure" ] || [ "${r}" = "cancelled" ]; then
echo "::error::a CI job failed (${results})"
exit 1
fi
case "${r}" in
success|skipped) ;;
*)
echo "::error::a required CI job did not pass: '${r}' (${results})"
exit 1
;;
esac
done
echo "all good"
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,19 @@ run-mac: bundle ## Launch the bundled Mac app

# ---- test -------------------------------------------------------------

.PHONY: test test-rust test-iced test-mac test-harness e2e e2e-gtk e2e-iced e2e-iced-clipboard e2e-mac e2e-gtk-ci e2e-iced-ci e2e-mac-ci smoke-gtk smoke-iced smoke-mac visual-parity smoke-mac-launch test-real-input test-iced-real-input test-iced-wayland-input check-iced
.PHONY: test test-rust test-iced test-mac test-harness e2e e2e-gtk e2e-iced e2e-iced-clipboard e2e-mac e2e-gtk-ci e2e-iced-ci e2e-mac-ci smoke-gtk smoke-iced smoke-mac visual-parity smoke-mac-launch test-real-input test-iced-real-input test-iced-wayland-input check-iced perf-refresh perf-render-stats

ICED_E2E_TESTS := tools/roosttest/test_smoke.py tools/roosttest/test_iced_walking_skeleton.py tools/roosttest/test_notifications.py tools/roosttest/test_provider.py tools/roosttest/test_sidebar_pixels.py tools/roosttest/test_tab_strip_pixels.py tools/roosttest/test_focus.py tools/roosttest/test_palette.py tools/roosttest/test_z_typography.py tools/roosttest/test_project_lifecycle.py tools/roosttest/test_sidebar_resize.py
ICED_CLIPBOARD_TESTS := tools/roosttest/test_selection.py tools/roosttest/test_osc52.py
test: test-rust test-mac test-harness ## All unit/integration tests (Rust + Swift + harness)

test-rust: ## cargo test --workspace
# roost-vt's tests/*.rs all start with `#![cfg(feature = "ffi")]`, so the
# `--workspace` run compiles and then silently skips every one of them. The
# second line mirrors CI's separate `cargo test -p roost-vt --features ffi`
# step (.github/workflows/ci.yml, rust job) so `make test` runs them too.
test-rust: ## cargo test --workspace (+ roost-vt ffi tests, cfg-gated out of the default run)
cargo test --workspace
cargo test -p roost-vt --features ffi

test-iced: ## Iced unit tests (renderer + input + adapter)
cargo test -p roost-iced
Expand Down Expand Up @@ -140,6 +145,12 @@ test-iced-real-input: build-iced ## Iced real clipboard input (self-contained L
test-iced-wayland-input: build-iced ## Iced system clipboard with cage + a real uinput seat
ROOST_REQUIRE_REAL_INPUT=1 uv run --group test python tools/input/linux/iced_wayland_clipboard_check.py

perf-refresh: ## In-crate refresh_snapshot perf harness (release, --ignored; see tools/perf/README.md)
cargo test -p roost-iced --release -- --ignored --nocapture

perf-render-stats: ## Render-path counters for a running Iced UI (tools/perf/render-stats.sh iced <duration>; args for other targets)
tools/perf/render-stats.sh iced

# ---- code quality -----------------------------------------------------

.PHONY: fmt fmt-check clippy themes-check check
Expand Down
53 changes: 46 additions & 7 deletions crates/roost-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
//! roostctl project {list,create,rename,delete,reorder}
//! roostctl palette {open,state,query,activate,dismiss}
//! roostctl screenshot [--out PATH] [--scale 1|2]
//! roostctl render-stats [--reset]
//! roostctl claude-hook EVENT
//! roostctl claude install [--force]
//!
Expand All @@ -45,13 +46,14 @@ use clap::{Parser, Subcommand, ValueEnum};
use roost_agent::claude::{canonical_hook_event, claude_event_to_reports, CLAUDE_HOOK_EVENTS};
use roost_ipc::messages::ops;
use roost_ipc::messages::{
IdentifyParams, IdentifyResult, NotificationCreateParams, PaletteActivateParams,
PaletteItemView, PaletteOpenParams, PalettePresentParams, PalettePresentResult,
PaletteQueryParams, PaletteStateResult, ProjectCreateParams, ProjectCreateResult,
ProjectDeleteParams, ProjectRenameParams, ProjectReorderParams, ScreenshotParams,
ScreenshotResult, TabClearNotificationParams, TabCloseParams, TabDumpParams, TabDumpResult,
TabFocusParams, TabListResult, TabOpenParams, TabOpenResult, TabReorderParams, TabResizeParams,
TabSetStateParams, TabSetTitleParams, TabState, TabWriteParams,
AppRenderStatsParams, AppRenderStatsResult, IdentifyParams, IdentifyResult,
NotificationCreateParams, PaletteActivateParams, PaletteItemView, PaletteOpenParams,
PalettePresentParams, PalettePresentResult, PaletteQueryParams, PaletteStateResult,
ProjectCreateParams, ProjectCreateResult, ProjectDeleteParams, ProjectRenameParams,
ProjectReorderParams, ScreenshotParams, ScreenshotResult, TabClearNotificationParams,
TabCloseParams, TabDumpParams, TabDumpResult, TabFocusParams, TabListResult, TabOpenParams,
TabOpenResult, TabReorderParams, TabResizeParams, TabSetStateParams, TabSetTitleParams,
TabState, TabWriteParams,
};
use roost_ipc::paths::BundleProfileKind;
use roost_ipc::target::{ResolvedTarget, TargetError, TargetSelector};
Expand Down Expand Up @@ -169,6 +171,16 @@ enum Cmd {
#[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u32).range(1..=2))]
scale: u32,
},
/// Read the running UI's render-path counters — refresh/draw call
/// counts, elapsed nanos, rows and cells walked, `fill_text` calls.
/// The only way to measure the real draw path: it needs a live
/// renderer no unit test can construct.
RenderStats {
/// Zero the counters after reading them, so the next read is a
/// clean delta over whatever ran in between.
#[arg(long)]
reset: bool,
},
/// Claude Code hook entry point. Reads the JSON event payload
/// from stdin (Claude's contract), dispatches state +
/// notification ops to the running UI, and ALWAYS exits 0 with
Expand Down Expand Up @@ -881,6 +893,33 @@ async fn main() -> Result<()> {
}
}
}
Cmd::RenderStats { reset } => {
let stats: AppRenderStatsResult = client
.call(ops::APP_RENDER_STATS, AppRenderStatsParams { reset })
.await?;
let per = |total: i64, calls: i64| {
if calls > 0 {
(total / calls).to_string()
} else {
"-".to_string()
}
};
println!("refresh_calls {}", stats.refresh_calls);
println!("refresh_nanos {}", stats.refresh_nanos);
println!("rows_rebuilt {}", stats.rows_rebuilt);
println!("cells_walked {}", stats.cells_walked);
println!("draw_calls {}", stats.draw_calls);
println!("draw_nanos {}", stats.draw_nanos);
println!("fill_text_calls {}", stats.fill_text_calls);
println!(
"ns_per_refresh {}",
per(stats.refresh_nanos, stats.refresh_calls)
);
println!(
"ns_per_draw {}",
per(stats.draw_nanos, stats.draw_calls)
);
}
Cmd::Palette(PaletteCmd::Open { kind, json }) => {
let state: PaletteStateResult = client
.call(ops::PALETTE_OPEN, PaletteOpenParams { kind })
Expand Down
46 changes: 36 additions & 10 deletions crates/roost-engine/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ use std::sync::Arc;
use roost_ipc::agent::{self, TabAgentReportParams};
use roost_ipc::messages::{
ops, AppActivateParams, AppActiveTerminalFocusedParams, AppActiveTerminalFocusedResult,
AppCursorShapeParams, AppCursorShapeResult, AppSelectedTabIdParams, AppSelectedTabIdResult,
AppSetWindowFocusParams, ClipboardDumpParams, ClipboardDumpResult, ClipboardWriteParams,
IdentifyParams, IdentifyResult, NotificationCreateParams, PaletteActivateParams,
PaletteDismissParams, PaletteOpenParams, PalettePresentParams, PalettePresentResult,
PaletteQueryParams, PaletteStateParams, PaletteStateResult, ProjectCreateParams,
ProjectCreateResult, ProjectDeleteParams, ProjectRenameParams, ProjectReorderParams,
ResolvedCell, ScreenshotParams, ScreenshotResult, SelectionClearParams, SelectionDumpParams,
SelectionDumpResult, SelectionSetParams, SidebarDumpParams, SidebarDumpResult,
SidebarSetWidthParams, TabAgentReportResult, TabCapturePtyInputParams,
TabCapturePtyInputResult, TabClearNotificationParams, TabCloseParams,
AppCursorShapeParams, AppCursorShapeResult, AppRenderStatsParams, AppRenderStatsResult,
AppSelectedTabIdParams, AppSelectedTabIdResult, AppSetWindowFocusParams, ClipboardDumpParams,
ClipboardDumpResult, ClipboardWriteParams, IdentifyParams, IdentifyResult,
NotificationCreateParams, PaletteActivateParams, PaletteDismissParams, PaletteOpenParams,
PalettePresentParams, PalettePresentResult, PaletteQueryParams, PaletteStateParams,
PaletteStateResult, ProjectCreateParams, ProjectCreateResult, ProjectDeleteParams,
ProjectRenameParams, ProjectReorderParams, ResolvedCell, ScreenshotParams, ScreenshotResult,
SelectionClearParams, SelectionDumpParams, SelectionDumpResult, SelectionSetParams,
SidebarDumpParams, SidebarDumpResult, SidebarSetWidthParams, TabAgentReportResult,
TabCapturePtyInputParams, TabCapturePtyInputResult, TabClearNotificationParams, TabCloseParams,
TabDispatchMouseEventParams, TabDumpCursor, TabDumpParams, TabDumpResolvedParams,
TabDumpResolvedResult, TabDumpResult, TabExpandSelectionAtParams, TabExpandSelectionAtResult,
TabFeedPtyBytesParams, TabFocusParams, TabFocusResult, TabListResult, TabOpenParams,
Expand Down Expand Up @@ -66,6 +66,12 @@ type WindowMetricsReply = tokio::sync::oneshot::Sender<Result<WindowMetricsResul
/// `Ok`, matching `WindowMetricsReply`.
type SidebarDumpReply = tokio::sync::oneshot::Sender<Result<SidebarDumpResult, String>>;

/// Reply for [`UiRequest::AppRenderStats`]: the UI's render-path
/// counters. Read-only; always answers `Ok`, matching
/// `WindowMetricsReply`. A UI with no instrumentation answers with a
/// zeroed struct rather than an error — see the GTK arm.
type RenderStatsReply = tokio::sync::oneshot::Sender<Result<AppRenderStatsResult, String>>;

/// Reply for a [`UiRequest::Dump`]: the viewport text on success, an
/// error message (e.g. tab not found / no live terminal) on failure.
type DumpReply = tokio::sync::oneshot::Sender<Result<DumpData, String>>;
Expand Down Expand Up @@ -268,6 +274,15 @@ pub enum UiRequest {
/// collapsed flag (logical points). Backs the sidebar-holds-width
/// regression suite. Ungated (read-only).
WindowMetrics { reply: WindowMetricsReply },
/// `app.render_stats` — read the UI's render-path counters, and
/// zero them afterward when `reset`. Ungated. Not read-only: with
/// `reset` it reads and then clears. The counters are the only way
/// to measure the real draw path, which needs a live renderer no
/// unit test can construct.
AppRenderStats {
reset: bool,
reply: RenderStatsReply,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// `app.sidebar_dump` — read the sidebar's last-rendered agent rows
/// per project, plus the agents-visible toggle. Ungated (read-only);
/// reads `ProjectUi::rendered_agents`, the same cache the sidebar
Expand Down Expand Up @@ -681,6 +696,17 @@ async fn dispatch(
.map_err(|m| HandlerError::new("internal", m))?;
encode(&result)
}
ops::APP_RENDER_STATS => {
let p: AppRenderStatsParams = decode(params)?;
let result = h
.ui_call(|reply| UiRequest::AppRenderStats {
reset: p.reset,
reply,
})
.await?
.map_err(|m| HandlerError::new("internal", m))?;
encode(&result)
}
ops::SIDEBAR_DUMP => {
let _p: SidebarDumpParams = decode(params)?;
let result = h
Expand Down
18 changes: 12 additions & 6 deletions crates/roost-iced/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use roost_engine::{
};
use roost_ipc::agent;
use roost_ipc::messages::{
PaletteItemView, PalettePresentResult, PaletteStateResult, Project, SidebarDumpAgentRow,
SidebarDumpProject, SidebarDumpResult, WindowMetricsResult,
AppRenderStatsResult, PaletteItemView, PalettePresentResult, PaletteStateResult, Project,
SidebarDumpAgentRow, SidebarDumpProject, SidebarDumpResult, WindowMetricsResult,
};
use roost_ipc::paths::BundleProfile;
use roost_ipc::IpcServer;
Expand All @@ -47,9 +47,9 @@ use roost_ui_model::{
};
use roost_url::HoverUrl;
use roost_vt::{
key_action, mouse_action, mouse_button, KeyEncoder, KeyEvent, MouseEncoder, MouseEvent,
PageDirection, PageRoute, RenderState, ScrollDirection, ScrollRoute, Terminal, TerminalOptions,
TerminalScroll, TerminalSelection,
key_action, mouse_action, mouse_button, ColorRgb, KeyEncoder, KeyEvent, MouseEncoder,
MouseEvent, PageDirection, PageRoute, RenderState, ScrollDirection, ScrollRoute, Terminal,
TerminalOptions, TerminalScroll, TerminalSelection,
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

Expand All @@ -60,7 +60,7 @@ use crate::palette_scroll::Visibility;
use crate::sidebar_resize::SidebarResizeGrip;
use crate::strip_reorder::{ReorderStrip, StripEvent};
use crate::terminal_widget::{
resolve_colors, DrawCell, TerminalMetrics, TerminalPointerEvent, TerminalSnapshot,
DrawCell, RenderedRow, TerminalMetrics, TerminalPointerEvent, TerminalSnapshot,
TerminalWheelEvent, TerminalWidget, TERMINAL_PADDING,
};
use crate::Message;
Expand All @@ -73,6 +73,12 @@ mod interactions;
mod palettes;
mod servicing;
mod terminal_tab;
// The in-crate `#[ignore]`d perf harness — see `tools/perf/README.md` for
// how to run it. Gated on `cfg(test)` like `terminal_tab`'s test-only
// `attach_test_terminal` fixture it depends on; it carries no production
// code, so the whole module (not just an inner `mod tests`) is test-only.
#[cfg(test)]
mod perf_bench;

pub(crate) use self::interactions::RenameTarget;
use self::interactions::{
Expand Down
Loading
Loading