Skip to content

perf(gpui): avoid allocations during freehand hit testing - #3488

Open
himanshu748 wants to merge 1 commit into
CapSoftware:mainfrom
himanshu748:perf/gpui-freehand-hit-testing
Open

himanshu748 wants to merge 1 commit into
CapSoftware:mainfrom
himanshu748:perf/gpui-freehand-hit-testing

Conversation

@himanshu748

@himanshu748 himanshu748 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Freehand annotation hover and selection currently allocate a transformed point vector for every stroke examined, even when the first segment is hit. Pointer movement pays that cost before deciding whether the hover state changed.

Stream transformed points through stroke-distance and nonzero-fill checks in one pass. This removes the temporary vector and lets early hits stop transforming samples. The implicit closing edge still contributes only to fill; distance arithmetic, zero-size transforms and topmost selection are preserved. Painting and export continue to receive the same collected points. No cache, new dependencies or persistent state.

Measured tradeoff

These are isolated pointer hit-test measurements, not frame time, painting, exported pixels or end-to-end UI latency. Synthetic fixtures contain circular strokes with 8, 32, 256, 2,048 or 8,192 samples, with 1, 8 or 32 annotations. Strokes occupy 2,000 × 2,000 units and are separated by 10,000-unit x offsets to isolate scan depth; they are not estimates of typical on-screen projects. Early/late targets hit the first or roughly three-quarter segment in the topmost/bottommost annotation; interior and miss cases scan further.

Linux x86-64, AMD EPYC 7763 VM, four-CPU quota, Rust 1.99.1, opt-level 3, one codegen unit, no LTO. Three sequential processes; each case uses 10 warmup pairs followed by 31 alternating before/after pairs. Batch size is (32768 / (points * annotations)).clamp(16, 256). Fixture construction is excluded. Values are medians of the three process medians. The timing binary has no allocation counters.

Samples per stroke Annotations Query Before After
8 1 Early hit 0.0346 µs 0.0155 µs
8 1 Miss 0.0716 µs 0.0567 µs
256 1 Late hit 1.232 µs 1.294 µs
2,048 1 Early hit 0.885 µs 0.0176 µs
8,192 1 Early hit 2.750 µs 0.0176 µs
8,192 1 Late hit 39.199 µs 41.289 µs
8,192 1 Interior 57.020 µs 54.534 µs
8,192 1 Miss 58.389 µs 54.634 µs
8,192 32 All miss 1.887 ms 1.779 ms

Late hits can be slower: the fused loop performs winding work before discovering a late stroke hit, whereas the old path skipped winding entirely after any stroke hit. The worst median regression in this 90-case matrix is about 6.7%; a 256-sample late hit is about 5.1% slower. This is a workload-dependent improvement, not a universal hover speedup. For the 32-stroke miss case, process medians ranged 1.876–1.966 ms before and 1.753–1.805 ms after; the three paired speedup ratios were 1.061–1.089.

A separate counting binary measured 1 allocation / 131,072 requested bytes → 0 for an examined 8,192-sample stroke, and 32 allocations / 4,194,304 requested bytes → 0 for the 32-stroke miss case. Requested bytes are not RSS or retained memory.

Correctness and reproduction

  • Four actual-source regressions pass in isolated debug and optimized builds, including nonzero winding, the fill-only closing edge, transforms/degenerate samples, 10,000 seeded float cases and topmost selection.
  • Independent source review and 300,000 additional differential cases pass in both builds, including missing samples, negative/zero dimensions, signed zero, arbitrary floating-point values and nonfinite inputs. Transformed point arrays also match, including bit comparisons in the final check.
  • The isolated build extracts production methods from this change and a frozen baseline at 0a35a8840a4924cd6bbbd5e82900018221e158cf, using real project types whose source inputs were verified unchanged. It excludes GPUI/font shaping and does not build the application.
  • The same fixture matrix and paired timing loop are committed as an ignored test. On a configured native development environment:
cd apps/desktop-gpui
cargo test --bin cap-gpui screenshot_annotations::tests::freehand_hit_tests
cargo test --release --bin cap-gpui screenshot_annotations::tests::freehand_hit_tests::benchmark_freehand_hit_testing -- --ignored --nocapture

Repeat the benchmark in three processes. It emits each process's per-case median; individual paired intervals are not emitted. Native release uses thin LTO, so its timings can differ from the isolated measurements above.

Formatting, diff checks and extracted-source Clippy pass. Local full-app checking reached the missing alsa.pc prerequisite and was stopped while other dependency jobs were still running; a focused offline alsa-sys check exits 101 for that prerequisite. Native app tests, manual interaction, pixel comparisons and end-to-end latency measurements were not run locally. Native CI subsequently passed on this exact head (cc40ac575f13b8058ebb4755efbe354ecc1128be), tested as merge 2db7af63406a075f7b594066f62475d32dcedfee into base 0a35a8840a4924cd6bbbd5e82900018221e158cf. All four new regressions passed on Linux (1,603 app tests), Windows (1,482), Apple Silicon (1,525) and Intel macOS (1,520), with zero failures. The benchmark remains ignored in CI; these runs do not extend the isolated timing claims.

This is separate from #3341's stroke ingestion and #3347's painting cache.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with one non-blocking improvement to the generated test.

Findings

  1. P2 Generated categories repeat fixed pairings ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/src/screenshot_annotations.rs:4454-4461
In `freehand_hits_preserve_float_edge_cases`, `state % 8` repeats a fixed eight-value cycle. Consecutive coordinates cannot vary independently: positive infinity is always followed by negative zero, and NaN by negative infinity. This limits what the 10,000 comparisons cover and could hide failures on other combinations. Choose categories from higher bits and add a separate set of all-finite generated paths.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Freehand hit testing now transforms samples as needed instead of collecting a temporary vector.

  • Freehand hit tests stream samples and stop when the pointer reaches a stroke segment.

Review checks were source-based; application tests and benchmarks were not run during this review.

himanshu748 explicitly acknowledges that late hits can be slower because the fused loop performs extra winding work, and accepts this workload-dependent tradeoff.

Reviews (1) · Last reviewed commit: "perf(gpui): avoid allocations during fre..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

@himanshu748
himanshu748 marked this pull request as ready for review October 27, 2026 23:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-27T23:39:54.454504Z
We couldn’t complete this review. Comment @codex review to try again.
cc40ac5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment on lines +4454 to +4461
match state % 8 {
0 => 0.,
1 => -0.,
2 => f64::INFINITY,
3 => f64::NEG_INFINITY,
4 => f64::NAN,
5 => f64::from_bits(state),
_ => ((state >> 32) as i32) as f64 / 65536.,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Generated categories repeat fixed pairings

In freehand_hits_preserve_float_edge_cases, state % 8 repeats a fixed eight-value cycle. Consecutive coordinates cannot vary independently: positive infinity is always followed by negative zero, and NaN by negative infinity. This limits what the 10,000 comparisons cover and could hide failures on other combinations. Choose categories from higher bits and add a separate set of all-finite generated paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/screenshot_annotations.rs
Line: 4454-4461

Comment:
**Generated categories repeat fixed pairings**

In `freehand_hits_preserve_float_edge_cases`, `state % 8` repeats a fixed eight-value cycle. Consecutive coordinates cannot vary independently: positive infinity is always followed by negative zero, and NaN by negative infinity. This limits what the 10,000 comparisons cover and could hide failures on other combinations. Choose categories from higher bits and add a separate set of all-finite generated paths.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

This branch has not been deployed

No deployments
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.

1 participant