Skip to content

perf(desktop-gpui): reduce long-menu CPU time by 9–40× - #3220

Merged
richiemcilroy merged 2 commits into
CapSoftware:mainfrom
kartikkabadi:perf/gpui-menu-virtualization
Oct 25, 2026
Merged

richiemcilroy merged 2 commits into
CapSoftware:mainfrom
kartikkabadi:perf/gpui-menu-virtualization

Conversation

@kartikkabadi

@kartikkabadi kartikkabadi commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Long dropdowns, including the editor font picker, currently construct and lay out every row on each redraw. This virtualizes overflowing fixed-height menus while preserving their intrinsic width, row identities and selection indices.

 redraw long menu
-  build and lay out every row
+  reuse widest-label measurement when labels and text style match
+  mount visible rows with overscan and top/bottom spacers

For synthetic 200, 500 and 1,000-entry catalogs, keyboard, hover and wheel redraws use approximately 9×, 23 to 25× and 38 to 40× less main/UI-thread CPU time, respectively. These numbers measure the component's CPU path, not native-font speed, GPU presentation, FPS or whole-editor latency.

The change is confined to apps/desktop-gpui/src/ui/menu.rs: +370 / -53, including 193 lines of regression tests. Production code is +177 / -53, net +124. No dependency or public API changes.

Behavior preserved

  • Small, empty, non-overflowing and extremely short menus retain eager rows.
  • Multiline labels and inherited line heights taller than a row retain eager rendering, because their text can extend beyond the row box.
  • Width still comes from the widest actual row, even when that row is offscreen. The measurement cache compares exact labels and the effective text style, including font properties, rather than a probabilistic hash.
  • The intrinsic sizing row is clipped to zero height and has a separate identity from interactive rows.
  • A shared ScrollHandle preserves offsets across eager and virtualized paths. Visible ranges clamp stale offsets and overscan fractional boundaries.
  • A held left-button press temporarily retains every row so GPUI's pending click state survives scrolling the pressed row offscreen and back. Inside and outside release clear this state.
  • Existing callbacks use absolute item indices. Selection/check state, anchor placement, themes and keyboard contracts are retained. Keyboard navigation's existing lack of automatic scrolling to the highlighted row is unchanged.

Evidence

Validated against upstream 69caa302a5e087b688a3f83ab369799264930c73, with candidate f9fb06fb6b126d14983c0cd7006cbd6d45f7ff73. Upstream was refreshed immediately before publication and still points to that base. The candidate source is unchanged from the reviewed input.

Main/UI-thread CPU time

Representative keyboard dispatch/update/draw/cleanup operations:

Entries Before After Median paired speedup Range across three pairs
200 2.698 ms 0.280 ms 9.6× 8.5 to 10.0×
500 7.356 ms 0.295 ms 25.2× 22.8 to 25.4×
1,000 13.825 ms 0.362 ms 40.0× 36.7 to 41.7×

The latency columns are medians of the three process medians. Speedups are medians of paired ratios, so the quotient of displayed latencies can differ slightly. Across keyboard, hover and wheel operations, the case-median ranges are 8.6 to 9.8× at 200 entries, 22.7 to 25.2× at 500, and 38.0 to 40.0× at 1,000. First-open and reopen at 1,000 entries have paired median ratios of 12.5× and 13.7×, including fresh per-open measurement-cache construction.

Complete CPU results, all 30 cases
Operation Entries Before, ms After, ms Median paired ratio Pair range
first_open 4 0.1075 0.1086 0.951× 0.947 to 1.001×
first_open 12 0.2458 0.2341 1.050× 0.939 to 1.115×
first_open 50 0.7571 0.3257 2.363× 2.073 to 2.428×
first_open 200 3.1224 0.4940 5.786× 5.662 to 6.576×
first_open 500 7.2551 0.8801 8.243× 7.528 to 9.726×
first_open 1000 16.2872 1.2728 12.487× 12.076 to 12.797×
hover 4 0.0769 0.0818 0.952× 0.881 to 1.029×
hover 12 0.2028 0.1958 1.025× 0.936 to 1.079×
hover 50 0.6960 0.2458 2.815× 2.550 to 3.134×
hover 200 2.8295 0.2921 9.799× 8.759 to 10.094×
hover 500 7.0414 0.3059 22.741× 22.546 to 25.210×
hover 1000 13.8611 0.3826 37.970× 35.661 to 39.443×
key_down_up 4 0.0784 0.0803 1.002× 0.954 to 1.032×
key_down_up 12 0.1990 0.1938 1.001× 0.988 to 1.083×
key_down_up 50 0.7006 0.2475 2.769× 2.545 to 3.024×
key_down_up 200 2.6984 0.2798 9.645× 8.540 to 9.973×
key_down_up 500 7.3557 0.2947 25.239× 22.836 to 25.409×
key_down_up 1000 13.8249 0.3620 39.958× 36.708 to 41.674×
reopen 4 0.0751 0.0771 0.974× 0.953 to 1.048×
reopen 12 0.2127 0.1964 1.062× 1.054 to 1.188×
reopen 50 0.7010 0.2770 2.546× 2.418 to 2.569×
reopen 200 2.9272 0.4118 6.606× 6.422 to 7.168×
reopen 500 7.0094 0.7254 9.498× 9.399 to 10.986×
reopen 1000 15.6355 1.1019 13.687× 13.314 to 14.331×
scroll 4 0.0783 0.0809 0.969× 0.948 to 1.022×
scroll 12 0.1989 0.2082 1.005× 0.946 to 1.024×
scroll 50 0.7323 0.2653 2.783× 2.570 to 2.841×
scroll 200 2.7221 0.3151 8.640× 7.937 to 10.114×
scroll 500 6.8176 0.3003 22.704× 22.273 to 23.065×
scroll 1000 14.0578 0.3601 39.088× 38.605 to 41.694×

Values below 1× mean the candidate used more CPU time. Small-menu results are within the variation of unchanged-binary controls; this does not make every menu faster. Some four-entry cases add several microseconds.

Measurement method and controls

Host: Apple M1 Pro, MacBookPro18,3, 8 logical CPUs, 16 GB RAM, macOS 27.0.1. Compiler: Rust 1.95.0. Both binaries use opt-level 3, thin LTO, one codegen unit and stripped symbols, with identical flags, dependency inputs and Cap's prepared GPUI checkout. GPUI is pinned at 5d1f83d9f27a19bec1fb241dc33b42238af9cf8d with the repository's six patches applied.

The primary timer is POSIX CLOCK_THREAD_CPUTIME_ID on the main/UI thread. It excludes OS descheduling and CPU work on other threads. A sleep control advanced wall time by approximately 55 ms while consuming approximately 46 µs of thread CPU time.

The fixture uses real Menu source, GPUI TestApp input dispatch, layout, CPU scene generation and old-frame release, with NoopTextSystem and the production check.svg. It constructs synthetic unique font-style labels rather than enumerating this host's font catalog. Opening-time and render-time item preparation are included. A fresh TestApp is used per first-open iteration; blank-window setup, fixture generation and teardown are outside the timed interval. Reopen excludes the preceding close but includes opening preparation and two draws, with a new per-open layout cache. Native font enumeration, shaping and GPU presentation are excluded from this timer.

Each series uses six catalog sizes, five operations, seven samples per case and three alternating fresh-process baseline/candidate pairs: 1,260 raw sample rows. CPU samples target 20 ms and use eight warmup operations. Raw sample arithmetic, pair medians and render counts were independently recomputed for all 30 cases. Dispatch verification separately checks that input events actually caused the expected redraws. The source and executable hashes were checked before each series.

Unchanged-binary CPU controls use the same baseline executable in both positions: three process pairs, five catalog sizes, five operations and seven samples, 1,050 rows. Individual paired ratios range from 0.79 to 1.29×; case-median ratios range from 0.88 to 1.05×. This limits the precision of small effects but is far smaller than the long-menu reductions. Other host workloads, clock frequencies and thermals were uncontrolled. No CPU affinity was applied, and task-owned builds/tests were stopped during timing.

Two wall-clock series were also retained: the original local 20 ms sample target and a 100 ms target with eight warmup operations. Both show substantial scheduling noise on this shared host. Unchanged-binary wall-clock controls produced apparent individual ratios from 0.46 to 3.67×. They do not support a precise local wall-clock headline. The title uses the separate main-thread CPU series rather than selecting the largest noisy wall-clock result. No samples were discarded. The three measured series contain 3,780 rows in total, in addition to control rows.

Retained wall-clock results, all 60 cases

Original local 20 ms series:

Operation Entries Before, ms After, ms Median paired ratio Pair range
first_open 4 0.1168 0.1153 1.022× 0.471 to 2.661×
first_open 12 0.2607 0.3233 0.800× 0.689 to 1.287×
first_open 50 0.7780 0.5069 1.735× 1.215 to 2.319×
first_open 200 3.2917 0.5525 5.787× 4.341 to 6.718×
first_open 500 7.2269 1.0834 6.661× 5.756 to 8.924×
first_open 1000 15.8454 1.7168 10.637× 8.456 to 11.768×
hover 4 0.0831 0.1297 0.884× 0.516 to 0.901×
hover 12 0.2142 0.3257 0.690× 0.359 to 0.913×
hover 50 0.7346 0.2863 2.495× 1.601 to 2.566×
hover 200 2.6994 0.3205 8.427× 6.994 to 8.600×
hover 500 7.0922 0.3215 21.675× 12.978 to 26.234×
hover 1000 22.7599 0.4633 49.128× 48.256 to 51.689×
key_down_up 4 0.1382 0.0934 1.667× 0.842 to 2.063×
key_down_up 12 0.2125 0.3605 0.615× 0.540 to 0.878×
key_down_up 50 0.7969 0.3232 2.507× 1.226 to 2.509×
key_down_up 200 2.6831 0.3151 8.515× 0.477 to 10.167×
key_down_up 500 7.4219 0.3461 24.140× 3.893 to 26.823×
key_down_up 1000 21.9629 0.4522 43.531× 41.763 to 62.862×
reopen 4 0.0855 0.1160 0.737× 0.595 to 2.091×
reopen 12 0.2285 0.2859 0.849× 0.783 to 0.904×
reopen 50 0.7378 0.4390 2.427× 1.306 to 2.430×
reopen 200 2.8552 0.4852 5.569× 0.923 to 7.322×
reopen 500 7.3309 0.7569 10.263× 5.745 to 11.224×
reopen 1000 16.0188 1.5294 9.385× 8.658 to 15.867×
scroll 4 0.1057 0.0951 1.210× 0.701 to 1.250×
scroll 12 0.2007 0.2797 0.718× 0.476 to 0.934×
scroll 50 0.7569 0.3338 2.268× 1.202 to 2.409×
scroll 200 2.6913 0.3161 8.513× 7.659 to 9.246×
scroll 500 8.1719 0.3171 22.737× 14.645 to 27.292×
scroll 1000 17.6113 0.4591 41.667× 38.360 to 57.623×

Longer local 100 ms series:

Operation Entries Before, ms After, ms Median paired ratio Pair range
first_open 4 0.1361 0.1235 1.059× 0.945 to 1.101×
first_open 12 0.3465 0.2546 1.238× 1.099 to 2.345×
first_open 50 0.8396 0.4930 1.648× 1.614 to 2.804×
first_open 200 3.1738 0.5363 5.918× 4.114 to 8.065×
first_open 500 9.6650 0.8551 11.303× 9.873 to 15.480×
first_open 1000 34.7894 1.4522 19.075× 12.002 to 33.221×
hover 4 0.0842 0.0918 0.985× 0.917 to 1.143×
hover 12 0.2243 0.2571 0.884× 0.873 to 0.941×
hover 50 0.8671 0.3021 2.852× 2.549 to 6.256×
hover 200 3.2859 0.3378 9.727× 8.138 to 11.318×
hover 500 23.8810 0.2978 80.565× 26.111 to 96.450×
hover 1000 22.8426 0.3738 58.374× 45.256 to 158.144×
key_down_up 4 0.0923 0.0975 0.947× 0.831 to 1.028×
key_down_up 12 0.2509 0.2855 0.879× 0.841 to 0.914×
key_down_up 50 0.9732 0.3065 3.175× 2.986 to 7.670×
key_down_up 200 3.3032 0.3147 11.831× 9.951 to 11.846×
key_down_up 500 11.2928 0.3033 33.995× 27.555 to 157.813×
key_down_up 1000 18.3264 0.3693 46.960× 43.302 to 113.065×
reopen 4 0.0862 0.1033 0.834× 0.554 to 1.014×
reopen 12 0.2857 0.3085 0.967× 0.926 to 1.663×
reopen 50 1.7212 0.3526 3.478× 2.325 to 6.879×
reopen 200 3.5493 0.4209 8.434× 5.720 to 9.415×
reopen 500 10.7431 0.7791 13.487× 12.994 to 16.071×
reopen 1000 41.9991 1.1953 32.469× 13.789 to 43.675×
scroll 4 0.1057 0.0859 1.036× 0.955 to 2.304×
scroll 12 0.2210 0.3593 0.601× 0.601 to 0.615×
scroll 50 0.7853 0.3042 2.583× 2.478 to 2.602×
scroll 200 3.7151 0.2946 12.610× 9.544 to 14.797×
scroll 500 8.8947 0.3261 27.989× 25.736 to 83.864×
scroll 1000 22.8117 0.3724 57.708× 43.472 to 241.758×

These tables are included for transparency. Their largest ratios are not the basis of the performance claim.

Tests and native rendering

The complete cap-gpui app test target passed on the refreshed base:

test result: ok. 1428 passed; 0 failed; 24 ignored; 0 measured; 0 filtered out; finished in 62.64s

The three new tests ran in that suite:

  • menu_width_uses_the_widest_actual_row
  • a_menu_click_survives_scrolling_away_and_back_while_held
  • releasing_a_held_menu_click_outside_cancels_it_without_breaking_the_next_click

The focused release harness also passed 25 tests. Those overlap app tests and are not added to 1,428 as unique coverage. Every one of 1,682 baseline/candidate headless geometry and interaction records matched exactly. Each baseline/candidate timing executable passed 16 dispatch checks.

Coverage includes empty/small/threshold menus, tiny/narrow viewports, all anchors, offscreen widest labels, fractional scroll offsets, bottom clamping, CJK/RTL/tab/multiline text, tall-line-height fallback, font/style/theme changes, checked-state changes, reordered/shrunk/resized menus, absolute callback indices, keyboard commit/dismiss, reopening, held presses and outside releases.

Native validation used real macOS fonts and Metal captures in an isolated offscreen component fixture, with the production SVG and 192 installed font names. The matrix is eight list sizes, three menu styles and five states: initial, fractional scroll, bottom, font/theme change and shrink. Each image is 560×420.

  • 95 of 120 image pairs are byte-identical.
  • The remaining 25 differ at 1 to 9 pixels, by at most one channel level out of 255. This is a tiny deterministic color difference, not a claim of pixel identity.
  • Repeating both unchanged binaries produced zero image differences across 120 repeat captures per variant.
  • All 1,236 native panel and visible-row bounds matched exactly. The passive bounds wrapper delegates the existing element's layout ID without inserting a layout node. Its images match every original uninstrumented capture for the respective variant.

An earlier canvas-based measurement attempt changed scroll extent and was rejected as intrusive instrumentation. Its results are excluded. The accepted bounds comparison is independently checked against uninstrumented images.

Real editor font-picker/sidebar/toolbar, settings and screenshot callers were reviewed. This validation does not establish a full signed-in app workflow, physical-pointer/IME behavior, local Windows/Linux execution, end-user FPS or workspace-wide integration coverage. The 24 ignored tests remain opt-in paths involving physical devices, audio/GPU scenarios, external fixtures or uploads.

Build, lint and formatting

The normal app test target builds successfully with the standard prepared GPUI and native dependencies. Baseline and candidate production/test Clippy runs both exit 0 and report 49 identical existing diagnostics, zero new diagnostics, none in menu.rs. This is not a claim that the workspace passes -D warnings.

Changed-file Rust formatting and git diff --check pass. Workspace formatting reports differences in four untouched upstream files: feeds.rs, main.rs, onboarding_window.rs and settings_window.rs. Those files are byte-identical to the validated base; this PR does not include unrelated formatting changes. The initial sandboxed build could not access Xcode DerivedData. Repeating it with the standard cache access succeeded, without source changes.

Overlap check

Checked complete file inventories for all 162 open PRs, including every new or changed head after the initial 159-PR audit, plus historical menu/font-picker/dropdown searches and recently closed PRs. No equivalent published change was found at publication time. #2587 overlaps this file as part of a broader design-system rewrite but still eagerly renders its menu rows. It may cause future conflicts. #3125 leaves font-picker virtualization as separate work. #3103 borrows the presets store in editor_window.rs, and #3110 restores Settings focus; neither virtualizes these rows.

Reproduction

App checks, using Rust 1.95.0 and the repository's normal GPUI/native-dependency setup:

bash scripts/prepare-gpui-dependency.sh
node scripts/setup.js
export CARGO_INCREMENTAL=0
export CARGO_PROFILE_DEV_DEBUG=0
export DYLD_LIBRARY_PATH="$PWD/target/native-deps/lib:$PWD/target/native-deps/onnxruntime/lib"
export DYLD_FRAMEWORK_PATH="$PWD/target/Frameworks"
cargo +1.95.0 test --manifest-path apps/desktop-gpui/Cargo.toml \
  -p cap-desktop-gpui --bin cap-gpui --locked -j 3 -- --test-threads=1
cargo +1.95.0 clippy --manifest-path apps/desktop-gpui/Cargo.toml \
  -p cap-desktop-gpui --bin cap-gpui --tests --locked -j 3
rustfmt +1.95.0 --edition 2024 --check apps/desktop-gpui/src/ui/menu.rs
git diff --check

Recorded app runs used cached dependencies with --offline and a separate shared CARGO_TARGET_DIR. To compare Clippy warnings, run the same command on the base and candidate, retaining JSON compiler diagnostics rather than changing lint settings.

Standalone CPU benchmark source and runner

The complete fixture, build helper and paired runner are below so this measurement can be reproduced without adding benchmark files to the PR's code diff. The full local raw-data/image/log archive is kept local. This description contains all 90 summary cases, the controls and their limitations.

Create a standalone directory next to the checked-out Cap repository, with a src subdirectory. Copy the following files into it. Obtain the production snapshots from the repository:

git -C /path/to/Cap show 69caa302a5e087b688a3f83ab369799264930c73:apps/desktop-gpui/src/ui/menu.rs > baseline-menu.rs
git -C /path/to/Cap show f9fb06fb6b126d14983c0cd7006cbd6d45f7ff73:apps/desktop-gpui/src/ui/menu.rs > candidate-menu.rs
git -C /path/to/Cap show 69caa302a5e087b688a3f83ab369799264930c73:apps/desktop-gpui/src/theme.rs > baseline-theme.rs
git -C /path/to/Cap show 69caa302a5e087b688a3f83ab369799264930c73:apps/desktop-gpui/assets/icons/check.svg > check.svg
mkdir -p src

Use the following manifest, adapting only the GPUI path to the checkout prepared by the repository script. The extra direct libc dependency is confined to this standalone timing fixture and uses libc 0.2.189 in the measured build. It is not a Cap dependency change.

[package]
name = "cap-menu-draw-probe"
version = "0.0.0"
edition = "2024"
publish = false

[workspace]

[features]
candidate = []
geometry = []

[dependencies]
gpui = { path = "/path/to/zed-cap/crates/gpui", features = ["test-support"] }

libc = "0.2"

[profile.release]
opt-level = 3
debug = 0
lto = "thin"
codegen-units = 1

strip = "symbols"

source-provenance.json:

{
  "baseline_commit": "ec15b98403441aee7780ff7734fef93d2e1a323f",
  "files": {
    "baseline-menu.rs": {
      "source": "apps/desktop-gpui/src/ui/menu.rs",
      "sha256": "de1425ea4177ab97be69012ee68bb498ddef4c18ccb16945b24965044ea04852"
    },
    "baseline-theme.rs": {
      "source": "apps/desktop-gpui/src/theme.rs",
      "sha256": "604d7dc9c674dbb6e75c2c5fb2ee3d6a12861873ea6390f01898acb18e4c3da4"
    },
    "check.svg": {
      "source": "apps/desktop-gpui/assets/icons/check.svg",
      "sha256": "3751b37796055d12e3b955b9afb117878d82ba26d35629afd460d7b070885c17"
    }
  }
}

src/main.rs:

#![allow(dead_code)]

use std::{
    borrow::Cow,
    hint::black_box,
    sync::{Arc, atomic::{AtomicUsize, Ordering}},
    time::Duration,
};

use gpui::{
    Bounds, Context, FocusHandle, InteractiveElement, IntoElement, KeyDownEvent,
    Modifiers, NoopTextSystem, ParentElement, Render, ScrollDelta, ScrollWheelEvent, Styled,
    TestApp, TestAppWindow, TouchPhase, Window, WindowBounds, WindowOptions, div, point,
    prelude::FluentBuilder, px, size,
};

#[cfg(all(not(feature = "candidate"), not(feature = "geometry")))]
#[path = "../baseline-menu.rs"]
mod menu;
#[cfg(all(feature = "candidate", not(feature = "geometry")))]
#[path = "../candidate-menu.rs"]
mod menu;
#[cfg(all(not(feature = "candidate"), feature = "geometry"))]
#[path = "../geometry-baseline-menu.rs"]
mod menu;
#[cfg(all(feature = "candidate", feature = "geometry"))]
#[path = "../geometry-candidate-menu.rs"]
mod menu;

#[path = "../baseline-theme.rs"]
mod theme;

#[cfg(feature = "geometry")]
mod geometry;

mod ui {
    pub mod button {
        pub type ClickHandler =
            Box<dyn Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static>;
    }
}

mod i18n {
    #[derive(Clone, Copy)]
    pub enum Locale {
        En,
        Ja,
        ZhCn,
        Hi,
    }
    pub fn current() -> Locale {
        Locale::En
    }
}

mod platform {
    #[derive(Clone, Copy, Debug, PartialEq)]
    pub enum MaterialKind {
        LiquidGlass,
        Vibrancy,
    }
    pub enum ForcedAppearance {
        System,
        Light,
        Dark,
    }
    pub fn apply_window_theme(_: &gpui::Window, _: ForcedAppearance) {}
    pub fn window_is_dark(_: &gpui::Window) -> Option<bool> {
        None
    }
    pub fn active_material(_: &gpui::App) -> Option<MaterialKind> {
        None
    }
}

mod store {
    #[derive(Clone, Copy)]
    pub enum AppTheme {
        System,
        Light,
        Dark,
    }
    pub struct GeneralSettings {
        pub theme: AppTheme,
    }
    impl GeneralSettings {
        pub fn load() -> Self {
            Self {
                theme: AppTheme::Light,
            }
        }
    }
}

const WIDTH: f32 = 1000.0;
const HEIGHT: f32 = 700.0;
const TRIGGER_X: f32 = 400.0;
const TRIGGER_Y: f32 = 100.0;
const MENU_X: f32 = TRIGGER_X + 60.0;
const MENU_Y: f32 = TRIGGER_Y + 36.0 + 4.0 + 17.0;
const WARMUPS: usize = 8;
const SAMPLES: usize = 7;
static CHECK_LOADS: AtomicUsize = AtomicUsize::new(0);

struct ProbeAssets;

impl gpui::AssetSource for ProbeAssets {
    fn load(&self, path: &str) -> gpui::Result<Option<Cow<'static, [u8]>>> {
        if path == "icons/check.svg" {
            CHECK_LOADS.fetch_add(1, Ordering::Relaxed);
            Ok(Some(Cow::Borrowed(include_bytes!("../check.svg"))))
        } else {
            Ok(None)
        }
    }

    fn list(&self, path: &str) -> gpui::Result<Vec<gpui::SharedString>> {
        Ok(if path == "icons" {
            vec!["icons/check.svg".into()]
        } else {
            Vec::new()
        })
    }
}

fn test_app() -> TestApp {
    TestApp::with_text_system_and_assets(Arc::new(NoopTextSystem), Arc::new(ProbeAssets))
}

fn open_window(app: &mut TestApp, count: usize) -> TestAppWindow<Harness> {
    app.open_window_with_options(
        WindowOptions {
            window_bounds: Some(WindowBounds::Windowed(Bounds::new(
                point(px(0.), px(0.)),
                size(px(WIDTH), px(HEIGHT)),
            ))),
            ..Default::default()
        },
        |window, cx| Harness::new(count, window, cx),
    )
}

#[cfg(feature = "candidate")]
const VARIANT: &str = "candidate";
#[cfg(not(feature = "candidate"))]
const VARIANT: &str = "baseline";

const FONT_NAMES: &[&str] = &[
    "Arial", "Aptos", "Avenir Next", "Baskerville", "Calibri", "Cambria",
    "Cascadia Code", "Century Gothic", "Charter", "Comic Sans MS", "Consolas",
    "Courier New", "DejaVu Sans", "DejaVu Sans Mono", "Didot", "DIN Alternate",
    "Futura", "Garamond", "Geist", "Georgia", "Gill Sans", "Helvetica Neue",
    "Hiragino Kaku Gothic ProN", "Hiragino Mincho ProN", "IBM Plex Mono",
    "IBM Plex Sans", "Inter", "Lato", "Liberation Sans", "Lucida Grande",
    "Menlo", "Merriweather", "Microsoft JhengHei", "Microsoft YaHei",
    "Montserrat", "Noto Sans Arabic", "Noto Sans Bengali", "Noto Sans CJK JP",
    "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Hebrew",
    "Noto Sans Tamil", "Noto Sans Thai", "Noto Serif CJK SC", "Open Sans",
    "Optima", "Palatino", "PingFang SC", "Roboto", "SF Pro Display",
    "Segoe UI", "Source Code Pro", "Source Sans 3", "Tahoma",
    "Times New Roman", "Trebuchet MS", "Ubuntu", "Verdana", "Yu Gothic",
    "Yu Mincho", "游ゴシック", "思源黑体", "ヒラギノ角ゴ ProN",
];

fn fixture_labels(count: usize) -> Vec<(String, String)> {
    let generics = [
        ("sans-serif", "System Sans"),
        ("serif", "System Serif"),
        ("monospace", "System Mono"),
    ];
    (0..count)
        .map(|index| {
            if index < generics.len() {
                let (value, label) = generics[index];
                (value.to_string(), label.to_string())
            } else {
                let font_index = index - generics.len();
                let family = FONT_NAMES[font_index % FONT_NAMES.len()];
                let label = if font_index < FONT_NAMES.len() {
                    family.to_string()
                } else {
                    format!("{family} Collection {}", 1 + font_index / FONT_NAMES.len())
                };
                (label.clone(), label)
            }
        })
        .collect()
}

struct Harness {
    fonts: Vec<(String, String)>,
    selected: String,
    state: menu::MenuState,
    theme: theme::Theme,
    focus: FocusHandle,
    open: bool,
    renders: usize,
    committed: Option<usize>,
}

impl Harness {
    fn new(count: usize, window: &mut Window, cx: &mut Context<Self>) -> Self {
        let fonts = fixture_labels(count);
        let selected = fonts.first().map_or_else(String::new, |x| x.0.clone());
        let items = Self::items_for(&fonts, &selected);
        let trigger = Bounds::new(
            point(px(TRIGGER_X), px(TRIGGER_Y)),
            size(px(180.), px(36.)),
        );
        let focus = cx.focus_handle();
        window.focus(&focus, cx);
        Self {
            fonts,
            selected,
            state: menu::MenuState::anchored(trigger, &items),
            theme: theme::Theme::light(),
            focus,
            open: false,
            renders: 0,
            committed: None,
        }
    }

    fn items_for(fonts: &[(String, String)], selected: &str) -> Vec<menu::MenuItem> {
        fonts
            .iter()
            .cloned()
            .map(|(value, label)| menu::MenuItem::new(label, value == selected))
            .collect()
    }

    fn on_key(&mut self, event: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) {
        if !self.open {
            return;
        }
        match self.state.on_key(&event.keystroke.key) {
            menu::MenuKey::Moved => cx.notify(),
            menu::MenuKey::Commit(index) => {
                self.committed = Some(index);
                self.open = false;
                cx.notify();
            }
            menu::MenuKey::Dismiss => {
                self.open = false;
                cx.notify();
            }
            menu::MenuKey::Ignored => {}
        }
    }
}

impl Render for Harness {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        self.renders += 1;
        div()
            .size_full()
            .relative()
            .font_family(theme::UI_FONT)
            .track_focus(&self.focus)
            .on_key_down(cx.listener(Self::on_key))
            .when(self.open, |root| {
                root.child(
                    menu::Menu::editor(
                        &self.theme,
                        "font-menu",
                        Self::items_for(&self.fonts, &self.selected),
                        &self.state,
                    )
                    .on_select(cx.listener(|this, index, _, cx| {
                        this.committed = Some(*index);
                        this.open = false;
                        cx.notify();
                    }))
                    .on_dismiss(cx.listener(|this, _, _, cx| {
                        this.open = false;
                        cx.notify();
                    })),
                )
            })
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum Scope {
    Reopen,
    KeyDownUp,
    Hover,
    Scroll,
}

impl Scope {
    fn name(self) -> &'static str {
        match self {
            Self::Reopen => "reopen",
            Self::KeyDownUp => "key_down_up",
            Self::Hover => "hover",
            Self::Scroll => "scroll",
        }
    }
}

fn set_open(window: &mut TestAppWindow<Harness>, open: bool) {
    window
        .update(|this, window, cx| {
            if open {
                window.focus(&this.focus, cx);
                let items = Harness::items_for(&this.fonts, &this.selected);
                this.state = menu::MenuState::anchored(
                    this.state.trigger_bounds.expect("font picker anchor"),
                    &items,
                );
            }
            this.open = open;
            cx.notify();
        });
}

fn operation(
    window: &mut TestAppWindow<Harness>,
    scope: Scope,
    iteration: usize,
) {
    match scope {
        Scope::Reopen => set_open(window, true),
        Scope::KeyDownUp => {
            window.simulate_keystroke(
                black_box(if iteration % 2 == 0 { "down" } else { "up" }),
            );
        }
        Scope::Hover => {
            window.simulate_mouse_move(
                point(px(MENU_X), px(MENU_Y + (iteration % 2) as f32 * 24.)),
            );
        }
        Scope::Scroll => {
            window.simulate_event(ScrollWheelEvent {
                position: point(px(MENU_X), px(MENU_Y + 48.)),
                delta: ScrollDelta::Pixels(point(
                    px(0.),
                    px(if iteration % 2 == 0 { -24. } else { 24. }),
                )),
                modifiers: Modifiers::none(),
                touch_phase: TouchPhase::Moved,
            });
        }
    }
}

fn batch(
    window: &mut TestAppWindow<Harness>,
    scope: Scope,
    iterations: usize,
) -> (u128, usize) {
    let renders_before = window.read(|view, _| view.renders);
    let elapsed = if scope == Scope::Reopen {
        let mut total = 0;
        for i in 0..iterations {
            set_open(window, false);
            let started = ThreadCpuInstant::now();
            operation(black_box(&mut *window), scope, black_box(i));
            total += started.elapsed().as_nanos();
        }
        total
    } else {
        let started = ThreadCpuInstant::now();
        for i in 0..iterations {
            operation(black_box(&mut *window), scope, black_box(i));
        }
        started.elapsed().as_nanos()
    };
    let renders_after = window.read(|view, _| view.renders);
    (elapsed, renders_after - renders_before)
}

fn benchmark(count: usize, scope: Scope) {
    let mut app = test_app();
    let mut window = open_window(&mut app, count);
    let asset_loads = CHECK_LOADS.load(Ordering::Relaxed);
    set_open(&mut window, true);
    app.run_until_parked();
    assert!(CHECK_LOADS.load(Ordering::Relaxed) > asset_loads);
    for i in 0..WARMUPS {
        if scope == Scope::Reopen {
            set_open(&mut window, false);
        }
        operation(&mut window, scope, i);
    }
    let pilot = batch(&mut window, scope, 2).0.max(1) as f64 / 2.;
    let iterations = ((20_000_000. / pilot) as usize).clamp(2, 2000) & !1;
    for sample in 0..SAMPLES {
        let (elapsed, renders) = batch(&mut window, scope, iterations);
        println!(
            "{VARIANT},{},{count},{WIDTH},{HEIGHT},{sample},{iterations},{elapsed},{:.3},{renders}",
            scope.name(),
            elapsed as f64 / iterations as f64,
        );
    }
    black_box(window.read(|this, _| this.state.clone()));
    window.update(|_, window, _| window.remove_window());
}

fn first_open(count: usize) -> u128 {
    let mut app = test_app();
    let mut window = open_window(&mut app, count);
    let before = window.read(|this, _| this.renders);
    let asset_loads = CHECK_LOADS.load(Ordering::Relaxed);
    let started = ThreadCpuInstant::now();
    set_open(black_box(&mut window), black_box(true));
    let elapsed = started.elapsed().as_nanos();
    assert_eq!(window.read(|this, _| this.renders) - before, 1);
    assert!(CHECK_LOADS.load(Ordering::Relaxed) > asset_loads);
    window.update(|_, window, _| window.remove_window());
    elapsed
}

fn benchmark_first_open(count: usize) {
    for _ in 0..WARMUPS {
        black_box(first_open(black_box(count)));
    }
    let pilot = (first_open(count) + first_open(count)).max(1) as f64 / 2.;
    let iterations = ((20_000_000. / pilot) as usize).clamp(2, 2000) & !1;
    for sample in 0..SAMPLES {
        let mut elapsed = 0;
        for _ in 0..iterations {
            elapsed += first_open(black_box(count));
        }
        println!(
            "{VARIANT},first_open,{count},{WIDTH},{HEIGHT},{sample},{iterations},{elapsed},{:.3},{iterations}",
            elapsed as f64 / iterations as f64,
        );
    }
}

fn verify_dispatch() {
    let mut app = test_app();
    let mut window = open_window(&mut app, 50);
    set_open(&mut window, true);
    app.run_until_parked();
    assert!(CHECK_LOADS.load(Ordering::Relaxed) > 0);
    for scope in [Scope::Reopen, Scope::KeyDownUp, Scope::Hover, Scope::Scroll] {
        for iteration in 0..WARMUPS {
            if scope == Scope::Reopen {
                set_open(&mut window, false);
            }
            operation(&mut window, scope, iteration);
        }
        for iteration in 0..4 {
            if scope == Scope::Reopen {
                set_open(&mut window, false);
            }
            let before = window.read(|this, _| this.renders);
            operation(&mut window, scope, iteration);
            let after = window.read(|this, _| this.renders);
            println!("dispatch,{VARIANT},{},{iteration},renders={}", scope.name(), after - before);
            assert_eq!(after - before, 1, "{} iteration {iteration}", scope.name());
        }
    }
    window.update(|_, window, _| window.remove_window());
}

#[cfg(feature = "geometry")]
fn main() {
    geometry::run();
}

#[cfg(not(feature = "geometry"))]
fn main() {
    if std::env::args().any(|argument| argument == "--verify-dispatch") {
        verify_dispatch();
        return;
    }
    let counts: Vec<usize> = std::env::var("MENU_COUNTS")
        .unwrap_or_else(|_| "4,12,50,200,500,1000".to_string())
        .split(',')
        .map(|count| count.parse().unwrap())
        .collect();
    println!("variant,scope,rows,viewport_width,viewport_height,sample,iterations,total_ns,ns_per_operation,render_count");
    for count in counts {
        benchmark_first_open(count);
        for scope in [Scope::Reopen, Scope::KeyDownUp, Scope::Hover, Scope::Scroll] {
            benchmark(count, scope);
        }
    }
}

struct ThreadCpuInstant(u64);
impl ThreadCpuInstant {
    fn now() -> Self {
        let mut value=libc::timespec {tv_sec:0,tv_nsec:0};
        assert_eq!(unsafe {libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID,&mut value)},0);
        Self(value.tv_sec as u64 * 1_000_000_000 + value.tv_nsec as u64)
    }
    fn elapsed(&self)->Duration {Duration::from_nanos(Self::now().0-self.0)}
}

build-binaries.py:

#!/usr/bin/env python3
"""Build the frozen baseline/candidate with identical flags and record provenance."""

import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tomllib
from datetime import datetime, timezone

ROOT = Path(__file__).resolve().parent


def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--gpui", type=Path, required=True, help="Pinned patched zed-cap checkout root")
    parser.add_argument("--cargo", default=os.environ.get("CARGO", "cargo"))
    parser.add_argument("--toolchain", default="1.95.0")
    parser.add_argument("--jobs", type=int, default=2)
    parser.add_argument("--variant", choices=["baseline", "candidate", "both"], default="both")
    parser.add_argument("--offline", action="store_true")
    args = parser.parse_args()
    gpui = args.gpui.resolve()
    assert (gpui / "crates/gpui/Cargo.toml").is_file(), gpui
    manifest = ROOT / "Cargo.toml"
    text, count = re.subn(
        r'^gpui = \{ path = "[^"]*"',
        lambda _: "gpui = { path = " + json.dumps(str(gpui / "crates/gpui")),
        manifest.read_text(),
        count=1,
        flags=re.MULTILINE,
    )
    assert count == 1
    manifest.write_text(text)
    profile = tomllib.loads(text)["profile"]["release"]
    env = os.environ.copy()
    env["CARGO_INCREMENTAL"] = "0"
    env["CARGO_TARGET_DIR"] = str(ROOT / "target")
    variants = ["baseline", "candidate"] if args.variant == "both" else [args.variant]
    (ROOT / "bin").mkdir(exist_ok=True)
    for variant in variants:
        source_names = ["Cargo.toml", "Cargo.lock", "src/main.rs", "baseline-theme.rs", "check.svg", f"{variant}-menu.rs"]
        source_hashes = {name: sha(ROOT / name) for name in source_names}
        command = [args.cargo, "+" + args.toolchain, "build", "--locked", "--release", "-j", str(args.jobs)]
        if args.offline:
            command.append("--offline")
        if variant == "candidate":
            command.extend(["--features", "candidate"])
        log = ROOT / f"build-{variant}-release.log"
        started = datetime.now(timezone.utc).isoformat()
        print(f"Building {variant}; log: {log}", flush=True)
        with log.open("w") as stream:
            result = subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT)
        if result.returncode:
            print("\n".join(log.read_text().splitlines()[-40:]))
            raise SystemExit(result.returncode)
        assert source_hashes == {name: sha(ROOT / name) for name in source_names}, "Source changed during build"
        output = ROOT / "bin" / f"menu-{variant}"
        shutil.copy2(ROOT / "target/release/cap-menu-draw-probe", output)
        rustc = str(Path(args.cargo).with_name("rustc")) if "/" in args.cargo else "rustc"
        metadata = {
            "variant": variant,
            "started_utc": started,
            "finished_utc": datetime.now(timezone.utc).isoformat(),
            "command": command,
            "cwd": str(ROOT),
            "profile": profile,
            "environment": {name: env.get(name) for name in ["CARGO_INCREMENTAL", "CARGO_TARGET_DIR", "RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "RUSTUP_HOME", "CARGO_HOME"]},
            "rustc_verbose": subprocess.check_output([rustc, "+" + args.toolchain, "-Vv"], env=env, text=True),
            "gpui": str(gpui),
            "gpui_head": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=gpui, text=True).strip(),
            "gpui_tracked_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "diff", "--binary"], cwd=gpui)).hexdigest(),
            "gpui_untracked_sha256": {name: sha(gpui / name) for name in subprocess.check_output(["git", "ls-files", "--others", "--exclude-standard"], cwd=gpui, text=True).splitlines()},
            "source_sha256": source_hashes,
            "binary_sha256": sha(output),
            "build_log_sha256": sha(log),
        }
        (ROOT / "bin" / f"menu-{variant}.build.json").write_text(json.dumps(metadata, indent=2) + "\n")
        print(f"Built {output}: {metadata['binary_sha256']}", flush=True)


if __name__ == "__main__":
    main()

run-pairs.py:

#!/usr/bin/env python3
"""Run serialized, alternating fresh-process menu comparisons; never build here."""

import argparse
import csv
import hashlib
import json
import os
from pathlib import Path
import platform
import statistics
import subprocess
import tomllib
from datetime import datetime, timezone


ROOT = Path(__file__).resolve().parent
SCRATCH = ROOT.parents[2]


def now():
    return datetime.now(timezone.utc).isoformat()


def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def capture(command, cwd=None):
    try:
        result = subprocess.run(command, cwd=cwd, text=True, capture_output=True)
    except FileNotFoundError as error:
        return {"command": command, "returncode": 127, "stdout": "", "stderr": str(error)}
    return {"command": command, "returncode": result.returncode, "stdout": result.stdout, "stderr": result.stderr}


def load_rows(path, variant, counts):
    with path.open(newline="") as stream:
        rows = list(csv.DictReader(stream))
    assert len(rows) == len(counts) * 5 * 7, (path, len(rows))
    grouped = {}
    for row in rows:
        assert row["variant"] == variant, (path, row)
        key = (row["scope"], int(row["rows"]), float(row["viewport_width"]), float(row["viewport_height"]))
        assert key[1] in counts
        iterations = int(row["iterations"])
        total = int(row["total_ns"])
        ns = float(row["ns_per_operation"])
        assert iterations >= 2 and iterations % 2 == 0
        assert abs(total / iterations - ns) <= 0.00051
        grouped.setdefault(key, []).append(row)
    assert len(grouped) == len(counts) * 5
    for key, samples in grouped.items():
        assert sorted(int(row["sample"]) for row in samples) == list(range(7)), (path, key)
        if key[0] in ("first_open", "key_down_up", "hover", "reopen"):
            expected = 2 if key[0] == "reopen" else 1
            for row in samples:
                assert int(row["render_count"]) == expected * int(row["iterations"]), (path, key, row)
        if key[0] == "scroll" and key[1] > 12:
            for row in samples:
                assert int(row["render_count"]) == int(row["iterations"]), (path, key, row)
    return grouped


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--baseline", type=Path, default=ROOT / "bin/menu-baseline")
    parser.add_argument("--candidate", type=Path, default=ROOT / "bin/menu-candidate")
    parser.add_argument("--gpui", type=Path, required=True, help="Pinned patched GPUI checkout root")
    parser.add_argument("--counts", default="4,12,50,200,500,1000")
    parser.add_argument("--pairs", type=int, default=3)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()
    gpui = args.gpui.resolve()
    assert (gpui / "crates/gpui/Cargo.toml").is_file(), gpui
    counts = [int(value) for value in args.counts.split(",")]
    assert counts and min(counts) >= 2 and len(set(counts)) == len(counts)
    assert args.pairs > 0
    args.output.mkdir(parents=True, exist_ok=False)
    binaries = {"baseline": args.baseline.resolve(), "candidate": args.candidate.resolve()}
    sources = [
        "Cargo.toml", "Cargo.lock", "src/main.rs", "baseline-menu.rs",
        "candidate-menu.rs", "baseline-theme.rs", "check.svg", "source-provenance.json", "run-pairs.py", "build-binaries.py",
    ]
    metadata = {
        "started_utc": now(),
        "pairs": args.pairs,
        "samples_per_process_case": 7,
        "timer": "POSIX CLOCK_THREAD_CPUTIME_ID on the main/UI thread",
        "target_sample_cpu_ns": 20000000,
        "measurement_scope": "Headless GPUI main-thread CPU time including the existing input/update/layout/scene/cleanup path. Excludes time descheduled by the OS and CPU time of other threads. This is distinct from the separately retained wall-clock series.",
        "warmup_operations": 8,
        "counts": counts,
        "baseline_commit": "ec15b98403441aee7780ff7734fef93d2e1a323f",
        "cpu_scope": "Real Menu item preparation, GPUI input dispatch/update, layout, CPU scene generation and old-frame release; TestApp with embedded production check.svg and NoopTextSystem, no native font shaping/GPU presentation/whole-editor throughput.",
        "first_open_scope": "First menu draw with a fresh GPUI TestApp and its text/asset caches each iteration; blank-window setup, fixture generation and teardown are outside timing; focus, opening-time items/MenuState and render-time item construction/drawing are timed. No native process startup/font enumeration is included.",
        "reopen_scope": "Previous close is excluded; focus, opening-time item construction and MenuState::anchored, new per-open cache construction, render-time item construction and menu rendering are timed. GPUI text-system process caches may be warm.",
        "fixture": "Synthetic catalog-size controls using common font-family-style labels; additional Collection suffixes keep labels unique. Does not enumerate this host's fonts.",
        "release_profile": tomllib.loads((ROOT / "Cargo.toml").read_text())["profile"]["release"],
        "build_metadata": {},
        "platform": platform.platform(),
        "lscpu": capture(["lscpu"]),
        "mac_hardware": capture(["sysctl", "hw.model", "hw.ncpu", "hw.memsize"]),
        "mac_os": capture(["sw_vers"]),
        "gpui_checkout": capture(["git", "rev-parse", "HEAD"], gpui),
        "gpui_status": capture(["git", "status", "--short"], gpui),
        "gpui_tracked_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "diff", "--binary"], cwd=gpui)).hexdigest(),
        "gpui_untracked_sha256": {
            path: sha(gpui / path)
            for path in subprocess.check_output(["git", "ls-files", "--others", "--exclude-standard"], cwd=gpui, text=True).splitlines()
        },
        "source_sha256": {source: sha(ROOT / source) for source in sources},
        "binaries": {name: {"path": str(path), "sha256": sha(path)} for name, path in binaries.items()},
        "runs": [],
    }
    for name, binary in binaries.items():
        sidecar = binary.with_name(binary.name + ".build.json")
        build = json.loads(sidecar.read_text())
        assert build["binary_sha256"] == sha(binary), (name, "binary/source provenance mismatch")
        assert build["profile"] == metadata["release_profile"], (name, "profile mismatch")
        for source, expected_hash in build["source_sha256"].items():
            assert sha(ROOT / source) == expected_hash, (name, source, "source changed after build")
        assert build["gpui_head"] == metadata["gpui_checkout"]["stdout"].strip()
        assert build["gpui_tracked_diff_sha256"] == metadata["gpui_tracked_diff_sha256"]
        assert build["gpui_untracked_sha256"] == metadata["gpui_untracked_sha256"]
        metadata["build_metadata"][name] = build
    paired = {}
    for pair in range(args.pairs):
        order = ["baseline", "candidate"] if pair % 2 == 0 else ["candidate", "baseline"]
        result = {}
        for variant in order:
            raw = args.output / f"pair-{pair + 1}-{variant}.csv"
            log = args.output / f"pair-{pair + 1}-{variant}.stderr.log"
            run = {"pair": pair + 1, "variant": variant, "started_utc": now(), "raw": raw.name, "stderr": log.name}
            env = os.environ.copy()
            env["MENU_COUNTS"] = args.counts
            with raw.open("w") as out, log.open("w") as err:
                subprocess.run([str(binaries[variant])], env=env, cwd=ROOT, stdout=out, stderr=err, check=True)
            run["finished_utc"] = now()
            run["raw_sha256"] = sha(raw)
            metadata["runs"].append(run)
            result[variant] = load_rows(raw, variant, counts)
            (args.output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
            print(f"Finished pair {pair + 1}: {variant}", flush=True)
        assert result["baseline"].keys() == result["candidate"].keys()
        for key in result["baseline"]:
            baseline = statistics.median(float(row["ns_per_operation"]) for row in result["baseline"][key])
            candidate = statistics.median(float(row["ns_per_operation"]) for row in result["candidate"][key])
            paired.setdefault(key, []).append((baseline, candidate, baseline / candidate))
    with (args.output / "summary.csv").open("w", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["scope", "rows", "viewport_width", "viewport_height", "pairs", "baseline_median_ns", "candidate_median_ns", "paired_ratio_median", "paired_ratio_min", "paired_ratio_max"])
        for key, pairs in sorted(paired.items()):
            ratios = [pair[2] for pair in pairs]
            writer.writerow([*key, len(pairs), statistics.median(pair[0] for pair in pairs), statistics.median(pair[1] for pair in pairs), statistics.median(ratios), min(ratios), max(ratios)])
    metadata["finished_utc"] = now()
    metadata["summary_sha256"] = sha(args.output / "summary.csv")
    (args.output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")


if __name__ == "__main__":
    main()

run-controls.py, with only the original local directory paths adapted to this standalone directory:

from pathlib import Path
import subprocess,os,csv,statistics,json,datetime,hashlib
w=Path(__file__).resolve().parent;out=w/'fresh-same-binary-cpu-controls';out.mkdir(exist_ok=False)
binary=w/'bin/menu-baseline';env=os.environ.copy();env['MENU_COUNTS']='4,12,200,500,1000'
runs=[];pairs={}
for pair in range(3):
 med=[]
 for run in range(2):
  p=out/f'pair-{pair+1}-run-{run+1}.csv'
  start=datetime.datetime.now(datetime.timezone.utc).isoformat()
  with p.open('w') as stdout,(out/f'pair-{pair+1}-run-{run+1}.stderr.log').open('w') as stderr:subprocess.run([str(binary)],env=env,stdout=stdout,stderr=stderr,check=True)
  groups={}
  for row in csv.DictReader(p.open()):groups.setdefault((row['scope'],row['rows']),[]).append(float(row['ns_per_operation']))
  med.append({k:statistics.median(v) for k,v in groups.items()})
  runs.append({'file':p.name,'started_utc':start,'finished_utc':datetime.datetime.now(datetime.timezone.utc).isoformat()})
 for k in med[0]:pairs.setdefault(k,[]).append(med[0][k]/med[1][k])
result={'timer':'POSIX CLOCK_THREAD_CPUTIME_ID main thread','identical_binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest(),'runs':runs,'cases':[{'scope':k[0],'rows':int(k[1]),'ratios':v,'median':statistics.median(v),'min':min(v),'max':max(v)} for k,v in sorted(pairs.items())]}
(out/'summary.json').write_text(json.dumps(result,indent=2)+'\n')
print('Same-binary ratios range',min(x['min'] for x in result['cases']),max(x['max'] for x in result['cases']))

Generate a Cargo.lock once before building the pair, then keep it fixed. The helpers build both variants with --locked and record each compiler version, GPUI tree hash, source hash, profile and executable hash. A newly generated dependency lock may differ from the measured lock; record that difference rather than claiming a byte-identical environment.

cargo +1.95.0 generate-lockfile
python3 build-binaries.py --gpui /path/to/zed-cap --cargo /path/to/cargo
bin/menu-baseline --verify-dispatch
bin/menu-candidate --verify-dispatch
python3 run-pairs.py --gpui /path/to/zed-cap --output fresh-cpu-results
python3 run-controls.py

The runner alternates baseline/candidate order across three fresh-process pairs, retains every raw CSV, verifies sample identities/arithmetic/render counts and emits paired summaries. Its original baseline commit label describes the archive's snapshot; the baseline menu/theme/SVG were independently verified equal to the upstream validation base above. Run matched controls with the same baseline executable in both positions to measure host variation. Do not build or run other owned test workloads concurrently with timing.

Reproduce the native macOS capture matrix

Use a separate copy of the standalone fixture above. Add this dependency under [dependencies] in Cargo.toml, with the prepared GPUI path:

gpui_platform = { path = "/path/to/zed-cap/crates/gpui_platform", features = ["test-support", "font-kit", "runtime_shaders"] }

Save the following as src/native.rs. It is the native capture driver used in review. The unused panel argument in its scroll helper is historical fixture code; scroll position comes from the dispatched event.

use gpui::{AppContext, Bounds, Context, Font, FontStyle, FontWeight, IntoElement, Modifiers, MouseButton, ParentElement, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, SharedString, Size, Styled, TouchPhase, VisualTestAppContext, Window, WindowHandle, div, font, point, px, size};
use std::{path::PathBuf, sync::Arc};
use crate::{menu, theme, ProbeAssets, VARIANT};

struct NativeMenu {
    items: Vec<menu::MenuItem>,
    state: menu::MenuState,
    font: Font,
    line_height: Pixels,
    dark: bool,
    kind: usize,
    selected: Option<usize>,
}
impl Render for NativeMenu {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let theme = if self.dark {theme::Theme::dark()} else {theme::Theme::light()};
        let menu=match self.kind {
            0 => menu::Menu::editor(&theme,"native-menu",self.items.clone(),&self.state),
            1 => menu::Menu::settings(&theme,"native-menu",self.items.clone(),&self.state),
            _ => menu::Menu::plain(&theme,"native-menu",self.items.clone(),&self.state),
        }.on_select(cx.listener(|this,index,_,cx|{this.selected=Some(*index);cx.notify();}));
        div().size_full().relative().bg(if self.dark {gpui::black()} else {gpui::white()})
            .font(self.font.clone()).line_height(self.line_height).child(menu)
    }
}
fn snapshot(app:&mut VisualTestAppContext,window:WindowHandle<NativeMenu>,dir:&std::path::Path,name:&str) {
    app.update_window(window.into(),|_,w,cx|{ w.refresh();w.draw(cx).clear(); }).unwrap();
    let image=app.capture_screenshot(window.into()).unwrap();
    image.save(dir.join(format!("{name}.png"))).unwrap();
    println!("{name},image={}x{}",image.width(),image.height());
}
fn scroll(app:&mut VisualTestAppContext,window:WindowHandle<NativeMenu>,panel:Bounds<Pixels>,delta:f32){
    app.simulate_event(window.into(),ScrollWheelEvent{
        position:point(px(100.),px(100.)),delta:ScrollDelta::Pixels(point(px(0.),px(delta))),
        modifiers:Modifiers::none(),touch_phase:TouchPhase::Moved,
    });
}
pub fn run(){
    let dir=PathBuf::from(std::env::var_os("MENU_NATIVE_OUTPUT").unwrap());
    std::fs::create_dir_all(&dir).unwrap();
    let platform=gpui_platform::current_platform(false);
    let mut app=VisualTestAppContext::with_asset_source(platform,Arc::new(ProbeAssets));
    let system_fonts=app.text_system().all_font_names();
    println!("variant={VARIANT},installed_font_count={}",system_fonts.len());
    for count in [0,4,12,13,50,200,500,1000] {
        for kind in 0..3 {
            let items=(0..count).map(|i|menu::MenuItem::new(if i+1==count {"Widest offscreen label 日本語 العربية 🧑🏽‍💻".to_string()} else {format!("{} font {i}",system_fonts.get(i%system_fonts.len()).unwrap())},i==0)).collect::<Vec<_>>();
            let state=menu::MenuState::anchored(Bounds::new(point(px(60.),px(40.)),size(px(180.),px(30.))),&items);
            let window=app.open_offscreen_window(size(px(560.),px(420.)),|_,cx|cx.new(|_|NativeMenu{items,state,font:font(theme::UI_FONT),line_height:px(18.),dark:kind==1,kind,selected:None})).unwrap();
            let tag=format!("{count}-{kind}");
            snapshot(&mut app,window,&dir,&format!("{tag}-initial"));
            let panel=Bounds::new(point(px(60.),px(74.)),size(px(180.),px(320.)));
            scroll(&mut app,window,panel,-24.375);
            snapshot(&mut app,window,&dir,&format!("{tag}-fractional"));
            scroll(&mut app,window,panel,-100_000.);
            snapshot(&mut app,window,&dir,&format!("{tag}-bottom"));
            window.update(&mut app,|v,_,cx|{v.font=Font{family:"Times New Roman".into(),weight:FontWeight::BOLD,style:FontStyle::Italic,..font("Times New Roman")};v.dark=!v.dark;cx.notify();}).unwrap();
            snapshot(&mut app,window,&dir,&format!("{tag}-font-theme"));
            window.update(&mut app,|v,_,cx|{v.items.truncate(4);v.state.len=v.items.len();cx.notify();}).unwrap();
            snapshot(&mut app,window,&dir,&format!("{tag}-shrunk"));
            app.update_window(window.into(),|_,w,_|w.remove_window()).unwrap();
        }
    }
    println!("native,complete");
}

Change the benchmark entry point to retain it as an unused function, then call the native driver instead. This adapts the CPU fixture's entry point for the native matrix; it does not modify production menu code.

from pathlib import Path
p = Path("src/main.rs")
s = p.read_text()
old = '#[cfg(not(feature = "geometry"))]\nfn main() {'
assert s.count(old) == 1
p.write_text(s.replace(old, old.replace("fn main()", "fn archived_main()")) + '\nmod native;\nfn main() { native::run(); }\n')

After generating the fixture lockfile, build without/with --features candidate and preserve both executables before the next build replaces the output. Run each with a fresh MENU_NATIVE_OUTPUT directory on macOS. Do not enable the optional geometry feature here. Compare decoded PNG RGBA bytes, not compressed PNG sizes. Repeat both unchanged binaries to distinguish deterministic candidate differences from unstable rendering. The 1,236-bound comparison was a separate passive Element-wrapper probe, and is not performed by this uninstrumented image driver.

Measured artifact fingerprints
Artifact SHA256
baseline-menu.rs de1425ea4177ab97be69012ee68bb498ddef4c18ccb16945b24965044ea04852
candidate-menu.rs 7010fe819b95bdfb67f128a5f69863210238cb15cf412c46abb2fa69f2003b2f
baseline-theme.rs 604d7dc9c674dbb6e75c2c5fb2ee3d6a12861873ea6390f01898acb18e4c3da4
check.svg 3751b37796055d12e3b955b9afb117878d82ba26d35629afd460d7b070885c17
Cargo.lock 756840ffdcd1e6c02aa9a34a96845e0d16ddada610a192b5ebe817ff0da4d878
src/main.rs 2242767959072ca7892ce82e75f4f378f799924fad2477d0deb33fa457a901cf
CPU baseline executable a7b62e9181eb39f64da77771a79f17f0e4e72dffffc67107b649f2315d62909d
CPU candidate executable 592e04661fa6b0f1ab9d6aa4e17c46294af0c476966cc06c48dfba3f2c6d7b5f
CPU summary.csv 149bb65183a42b4335fd19c6fd92605aa8503959ddcb00ffcb03a528ba4f65e8

Executable and lock hashes identify the measured artifacts; they are not expected to match across platforms or independently resolved lockfiles. Original archive results from Linux were checked for arithmetic consistency but are not used as locally reproduced Linux evidence.

Merge danger

This is reversible by reverting one commit. It changes the shared dropdown component used by editor, settings and screenshot controls. The main risks are intrinsic-width changes, scroll boundaries and pending-click lifetimes; the eager fallbacks and geometry/interaction/native comparisons address those paths. First-open width measurement and exact-label cache validation still scale with item count, and holding the left button intentionally gives up virtualization until release. No migrations, persisted-data changes, feature flags or new production dependencies.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable issues were found.

Summary

Reduces work when drawing overflowing, fixed-height menus.

  • Long dropdowns now render only the rows near the scroll position.

No actionable issues were found. Tests and native rendering were not run during this review.

Reviews (1) · Last reviewed commit: "perf(desktop-gpui): virtualize long drop..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

@kartikkabadi is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

@richiemcilroy
richiemcilroy merged commit 06154a9 into CapSoftware:main Oct 25, 2026
41 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants