From 8e64a9dbd6804b77bf11f584130a8abc46eab70f Mon Sep 17 00:00:00 2001 From: Colin Kim <121141519+colinvkim@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:47:20 -0700 Subject: [PATCH 1/4] perf(rendering): index auto-zoom cursor clusters --- crates/rendering/benches/zoom-clusters.csv | 61 ++++ crates/rendering/benches/zoom-clusters.md | 83 +++++ crates/rendering/src/zoom_spring.rs | 358 +++++++++++++++++++-- scripts/benchmark-zoom-clusters.py | 133 ++++++++ 4 files changed, 616 insertions(+), 19 deletions(-) create mode 100644 crates/rendering/benches/zoom-clusters.csv create mode 100644 crates/rendering/benches/zoom-clusters.md create mode 100644 scripts/benchmark-zoom-clusters.py diff --git a/crates/rendering/benches/zoom-clusters.csv b/crates/rendering/benches/zoom-clusters.csv new file mode 100644 index 00000000000..aea7fc76e17 --- /dev/null +++ b/crates/rendering/benches/zoom-clusters.csv @@ -0,0 +1,61 @@ +fixture,run,variant,durationSecs,zoomSegments,completedAwaitedEdits,terminalFailures,stderrErrors,stderrWarnings,editCpuSecs,early_atSecs,early_seekMs,early_editMedianMs,early_editP95Ms,middle_atSecs,middle_seekMs,middle_editMedianMs,middle_editP95Ms,late_atSecs,late_seekMs,late_editMedianMs,late_editP95Ms +smooth-30.cap,1,baseline,30.0,1,90,0,0,0,0.176116,0.5833333333333334,14.535833,6.014083,9.471791999999999,14.983333333333333,22.618125,6.410792,10.370208,29.383333333333333,22.452792,9.150917,13.705333 +smooth-30.cap,1,candidate,30.0,1,90,0,0,0,0.193078,0.5833333333333334,15.171125,6.070167,8.379707999999999,14.983333333333333,22.378709,8.138959,10.947792,29.383333333333333,23.029999999999998,10.571041,13.780041 +smooth-30.cap,2,candidate,30.0,1,90,0,0,0,0.14635,0.5833333333333334,18.434791999999998,6.074834,8.364375,14.983333333333333,31.232291999999998,7.974458,11.515917,29.383333333333333,18.025917,7.031833000000001,10.406708 +smooth-30.cap,2,baseline,30.0,1,90,0,0,0,0.20678600000000003,0.5833333333333334,15.822709000000001,8.396249999999998,9.595459,14.983333333333333,25.102541,7.688875,9.992666000000002,29.383333333333333,27.852291,7.868541,11.2005 +smooth-30.cap,3,baseline,30.0,1,90,0,0,0,0.12227999999999997,0.5833333333333334,14.68525,6.005958,7.184167,14.983333333333333,22.380459,7.3140410000000005,10.767125,29.383333333333333,18.689542,7.515541,10.470417 +smooth-30.cap,3,candidate,30.0,1,90,0,0,0,0.15273599999999998,0.5833333333333334,14.435292,5.970083,7.165792,14.983333333333333,21.131332999999998,5.092625,9.655958,29.383333333333333,20.224290999999997,7.9645410000000005,10.515416 +smooth-30.cap,4,candidate,30.0,1,90,0,0,0,0.17471899999999993,0.5833333333333334,16.013042000000002,8.393374999999999,9.563416,14.983333333333333,32.867541,7.733042,11.358792,29.383333333333333,32.4925,8.461916,11.756166 +smooth-30.cap,4,baseline,30.0,1,90,0,0,0,0.159136,0.5833333333333334,19.161583,5.9818750000000005,7.178249999999999,14.983333333333333,30.798333,8.471459000000001,11.462458999999999,29.383333333333333,21.190875000000002,7.8408750000000005,9.906208 +smooth-30.cap,5,baseline,30.0,1,90,0,0,0,0.17704,0.5833333333333334,15.852834,6.08925,9.427833,14.983333333333333,25.203792,6.533167000000001,9.871292,29.383333333333333,28.503209000000002,8.115458,9.365 +smooth-30.cap,5,candidate,30.0,1,90,0,0,0,0.15830499999999997,0.5833333333333334,14.468958,4.949833,9.428958,14.983333333333333,19.027790999999997,5.4025419999999995,6.8065,29.383333333333333,24.3315,7.816,12.702833 +smooth-120.cap,1,baseline,120.0,1,90,0,0,0,0.224209,2.3833333333333333,15.726207999999998,6.066125,8.480959,59.983333333333334,24.454625,8.39775,10.291042000000001,117.58333333333333,27.800667,10.882,14.25825 +smooth-120.cap,1,candidate,120.0,1,90,0,0,0,0.251459,2.3833333333333333,14.195167,7.284833,8.471667,59.983333333333334,29.787125,8.987958,11.002875000000001,117.58333333333333,30.72525,9.794625,13.674916 +smooth-120.cap,2,candidate,120.0,1,90,0,0,0,0.193544,2.3833333333333333,15.332709,5.93825,8.416916,59.983333333333334,21.452875,7.297,10.384834000000001,117.58333333333333,22.954791,9.060417,11.821584 +smooth-120.cap,2,baseline,120.0,1,90,0,0,0,0.20932700000000004,2.3833333333333333,16.424,6.039708,8.50275,59.983333333333334,24.345125,8.442958,12.520708,117.58333333333333,26.141583,10.481541,16.255125 +smooth-120.cap,3,baseline,120.0,1,90,0,0,0,0.19719,2.3833333333333333,23.206833,5.02325,8.41225,59.983333333333334,33.208917,7.766833,12.16,117.58333333333333,23.45425,9.719458,12.879541 +smooth-120.cap,3,candidate,120.0,1,90,0,0,0,0.171051,2.3833333333333333,12.924667000000001,6.052166000000001,8.533875,59.983333333333334,25.25975,6.2983329999999995,11.30725,117.58333333333333,19.834334,8.003,10.947458000000001 +smooth-120.cap,4,candidate,120.0,1,90,0,0,0,0.17092500000000005,2.3833333333333333,14.21225,5.993875,9.621792000000001,59.983333333333334,28.377916,6.891958,10.863958,117.58333333333333,28.847250000000003,8.117958999999999,10.484333 +smooth-120.cap,4,baseline,120.0,1,90,0,0,0,0.146543,2.3833333333333333,14.073584,5.985125,9.788625,59.983333333333334,23.654333,9.14675,11.713959,117.58333333333333,18.633792,7.041083,9.425625 +smooth-120.cap,5,baseline,120.0,1,90,0,0,0,0.17456399999999997,2.3833333333333333,17.661417,6.105916000000001,8.461917,59.983333333333334,26.056791,9.710208999999999,15.137292,117.58333333333333,22.76875,10.335375,13.898375 +smooth-120.cap,5,candidate,120.0,1,90,0,0,0,0.18976700000000002,2.3833333333333333,15.454959,8.333874999999999,9.628708,59.983333333333334,26.591083,7.852792000000001,10.353875,117.58333333333333,28.871791,9.567417,12.074625000000001 +smooth-600.cap,1,baseline,600.0,1,90,0,0,0,0.7870170000000001,11.983333333333333,20.805291999999998,7.8325830000000005,9.270875,299.98333333333335,33.287625,18.199458,20.498209,587.9833333333333,25.181375,20.704832999999997,22.787625 +smooth-600.cap,1,candidate,600.0,1,90,0,0,0,0.29328199999999993,11.983333333333333,18.462709,5.13025,9.870084,299.98333333333335,27.7975,10.9885,12.097833000000001,587.9833333333333,25.595,12.488624999999999,13.95275 +smooth-600.cap,2,candidate,600.0,1,90,0,0,0,0.300285,11.983333333333333,18.231167,8.74875,9.968665999999999,299.98333333333335,28.597832999999998,11.159917,12.444875,587.9833333333333,25.355584,12.18375,13.497291 +smooth-600.cap,2,baseline,600.0,1,90,0,0,0,0.799258,11.983333333333333,19.45475,8.029917,10.616541999999999,299.98333333333335,37.664375,18.138749999999998,20.003292,587.9833333333333,23.160957999999997,21.5395,23.0665 +smooth-600.cap,3,baseline,600.0,1,90,0,0,0,0.7913209999999999,11.983333333333333,16.532708,5.625167,9.744667,299.98333333333335,30.937042,18.237083000000002,20.668958,587.9833333333333,27.398,19.503417,22.644166000000002 +smooth-600.cap,3,candidate,600.0,1,90,0,0,0,0.24995000000000003,11.983333333333333,18.864667,5.2249159999999994,11.944958,299.98333333333335,14.497333,7.89425,10.152750000000001,587.9833333333333,21.846,12.16025,14.034958999999999 +smooth-600.cap,4,candidate,600.0,1,90,0,0,0,0.306248,11.983333333333333,18.400458,9.780875,10.953583,299.98333333333335,39.607666,11.93975,13.570958,587.9833333333333,24.109708,11.828875,13.841417 +smooth-600.cap,4,baseline,600.0,1,90,0,0,0,0.7959860000000001,11.983333333333333,22.244875,9.385083,10.936625,299.98333333333335,37.927958,18.19375,19.8995,587.9833333333333,27.922,19.754082999999998,22.30625 +smooth-600.cap,5,baseline,600.0,1,90,0,0,0,0.8500629999999999,11.983333333333333,19.923083000000002,5.600499999999999,7.425916,299.98333333333335,31.534542,18.435792,19.470292,587.9833333333333,28.754790999999997,22.533792000000002,27.9235 +smooth-600.cap,5,candidate,600.0,1,90,0,0,0,0.29704699999999995,11.983333333333333,16.002083,3.9802500000000003,7.591292,299.98333333333335,23.937,11.020875,12.189625000000001,587.9833333333333,23.991583000000002,12.61575,14.9935 +short-600.cap,1,baseline,600.0,20,90,0,0,0,0.18384099999999995,11.983333333333333,17.502584000000002,8.402415999999999,9.662833,299.98333333333335,24.26375,4.972042,6.084542,587.9833333333333,20.83075,4.603917,7.335167 +short-600.cap,1,candidate,600.0,20,90,0,0,0,0.18166500000000005,11.983333333333333,18.54975,8.476416,9.528,299.98333333333335,26.784208,5.045084,6.180333,587.9833333333333,16.545583999999998,4.450791,5.7895840000000005 +short-600.cap,2,candidate,600.0,20,90,0,0,0,0.19037800000000005,11.983333333333333,19.514375,8.65525,9.812208,299.98333333333335,22.327042,3.965916,5.255166,587.9833333333333,22.590041000000003,4.4185,6.105625 +short-600.cap,2,baseline,600.0,20,90,0,0,0,0.15605000000000002,11.983333333333333,26.488125,6.218292,9.588332999999999,299.98333333333335,20.710375,3.46575,5.692333,587.9833333333333,17.165,4.20925,5.40575 +short-600.cap,3,baseline,600.0,20,90,0,0,0,0.13899400000000003,11.983333333333333,19.96575,8.536833000000001,9.681583,299.98333333333335,13.389833,3.21775,4.413875,587.9833333333333,17.208791,4.093292,6.333333 +short-600.cap,3,candidate,600.0,20,90,0,0,0,0.171271,11.983333333333333,18.468999999999998,8.466916999999999,9.65275,299.98333333333335,20.040875,4.107875000000001,5.059125,587.9833333333333,20.485500000000002,4.170084,5.580792000000001 +short-600.cap,4,candidate,600.0,20,90,0,0,0,0.14888799999999996,11.983333333333333,19.622833,5.179084,12.993333,299.98333333333335,14.398833,3.522209,4.717333,587.9833333333333,17.7845,4.2772499999999996,7.453542 +short-600.cap,4,baseline,600.0,20,90,0,0,0,0.16166200000000003,11.983333333333333,18.580291,8.365875,14.466166000000001,299.98333333333335,21.544125,3.3558339999999998,6.638459,587.9833333333333,14.053125,5.141,8.518333 +short-600.cap,5,baseline,600.0,20,90,0,0,0,0.13187200000000004,11.983333333333333,23.094167,6.122291000000001,8.23225,299.98333333333335,12.352333,3.4052499999999997,5.68825,587.9833333333333,14.874167,4.271417,6.147667 +short-600.cap,5,candidate,600.0,20,90,0,0,0,0.1251480000000001,11.983333333333333,18.681292,6.097667,8.411458999999999,299.98333333333335,14.862959,3.1945419999999998,5.466042,587.9833333333333,12.712041000000001,3.81975,5.2449580000000005 +full-600.cap,1,baseline,600.0,1,90,0,0,0,0.793398,11.983333333333333,38.12525,9.143625,10.412958,299.98333333333335,47.956209,18.054000000000002,19.226042,587.9833333333333,35.881375,21.426542,25.209792 +full-600.cap,1,candidate,600.0,1,90,0,0,0,0.2794740000000001,11.983333333333333,33.098416,5.608458000000001,7.411541,299.98333333333335,44.592542,8.525917,12.928875,587.9833333333333,50.435874999999996,15.05625,46.178583 +full-600.cap,2,candidate,600.0,1,90,0,0,0,0.2102550000000001,11.983333333333333,31.675167,5.0607500000000005,7.234875,299.98333333333335,29.008291,10.156,11.570542,587.9833333333333,35.719167000000006,8.864458,12.521083 +full-600.cap,2,baseline,600.0,1,90,0,0,0,0.7273779999999999,11.983333333333333,28.10925,6.579542,10.038042,299.98333333333335,34.837667,17.214,18.638542,587.9833333333333,33.684125,20.893333,22.05 +full-600.cap,3,baseline,600.0,1,90,0,0,0,0.727938,11.983333333333333,26.689375000000002,4.377167,9.088333,299.98333333333335,43.131958,17.886375,20.495124999999998,587.9833333333333,31.318749999999998,18.5335,21.800375 +full-600.cap,3,candidate,600.0,1,90,0,0,0,0.22764099999999998,11.983333333333333,25.579833999999998,4.951416,8.518333,299.98333333333335,36.102458,10.437750000000001,11.473333,587.9833333333333,30.738083,12.865167,14.070915999999999 +full-600.cap,4,candidate,600.0,1,90,0,0,0,0.22326500000000002,11.983333333333333,25.176209,7.310334,9.607125,299.98333333333335,27.181333,8.835334,10.491208,587.9833333333333,29.287084,11.609417,12.840709 +full-600.cap,4,baseline,600.0,1,90,0,0,0,0.7603270000000001,11.983333333333333,28.901666,9.077292,10.549959,299.98333333333335,41.204499999999996,18.013250000000003,18.139291,587.9833333333333,33.018458,20.744041000000003,23.427417 +full-600.cap,5,baseline,600.0,1,90,0,0,0,0.74926,11.983333333333333,26.578,5.412208,6.711208,299.98333333333335,44.466832999999994,17.996417,19.164082999999998,587.9833333333333,35.046375,19.518167,23.114875 +full-600.cap,5,candidate,600.0,1,90,0,0,0,0.25702100000000005,11.983333333333333,28.757875,5.155583,8.602291999999998,299.98333333333335,39.540040999999995,9.625125,12.353625,587.9833333333333,31.536250000000003,12.342042,14.453875 +full-short-600.cap,1,baseline,600.0,20,90,0,0,0,0.13350600000000004,11.983333333333333,25.029708000000003,7.089042,10.560917,299.98333333333335,32.516208,4.649208,5.986458,587.9833333333333,31.411458000000003,5.225917,5.449792 +full-short-600.cap,1,candidate,600.0,20,90,0,0,0,0.12485600000000001,11.983333333333333,25.301417,5.9717080000000005,9.47575,299.98333333333335,23.605833999999998,3.414958,4.599959,587.9833333333333,25.462333,4.19625,5.358209 +full-short-600.cap,2,candidate,600.0,20,90,0,0,0,0.12378300000000003,11.983333333333333,28.065542,8.269125,9.441167,299.98333333333335,25.191917,3.429792,4.562125,587.9833333333333,25.460041999999998,4.161625000000001,6.342875 +full-short-600.cap,2,baseline,600.0,20,90,0,0,0,0.12141800000000001,11.983333333333333,25.996416,8.259542,9.563125000000001,299.98333333333335,28.879915999999998,3.49825,4.659042,587.9833333333333,30.213875,3.8865000000000003,5.052875 +full-short-600.cap,3,baseline,600.0,20,90,0,0,0,0.11791599999999999,11.983333333333333,28.318833,5.945709000000001,9.446665999999999,299.98333333333335,25.501333000000002,3.388,4.531375,587.9833333333333,26.651457999999998,4.040916,5.27925 +full-short-600.cap,3,candidate,600.0,20,90,0,0,0,0.115095,11.983333333333333,26.286417,5.933041,7.138208,299.98333333333335,26.458375,3.333833,5.629208,587.9833333333333,23.773542000000003,4.029917,5.15975 +full-short-600.cap,4,candidate,600.0,20,90,0,0,0,0.12557899999999997,11.983333333333333,26.28625,8.244792,10.472083,299.98333333333335,23.680041999999997,3.56275,4.935959,587.9833333333333,26.645666,4.07675,5.30425 +full-short-600.cap,4,baseline,600.0,20,90,0,0,0,0.12949300000000002,11.983333333333333,26.042708,8.278875,9.474375,299.98333333333335,34.110208,3.5752919999999997,4.860125,587.9833333333333,25.577166,4.102333,5.285416000000001 +full-short-600.cap,5,baseline,600.0,20,90,0,0,0,0.11517700000000002,11.983333333333333,25.45225,8.312792,10.509792000000001,299.98333333333335,26.264542,3.327333,5.532459,587.9833333333333,28.734917,4.919207999999999,6.169084 +full-short-600.cap,5,candidate,600.0,20,90,0,0,0,0.127569,11.983333333333333,25.420375,8.314541,10.450834,299.98333333333335,27.60925,3.252917,5.809333,587.9833333333333,25.541417,3.9958329999999997,5.127667 diff --git a/crates/rendering/benches/zoom-clusters.md b/crates/rendering/benches/zoom-clusters.md new file mode 100644 index 00000000000..4d5335f2e37 --- /dev/null +++ b/crates/rendering/benches/zoom-clusters.md @@ -0,0 +1,83 @@ +# Auto-zoom cluster benchmark results + +On the synthetic ten-minute 1080p recording, a paused edit at 9:48 takes 20.74 ms before and 12.34 ms after, a 40.5% latency reduction. These are medians of five run medians. The ranges are 18.53–21.43 ms before and 8.86–15.06 ms after. + +The improvement concerns long auto-zoom segments. Shorter recordings and four-second zoom controls have overlapping run ranges and do not establish a performance difference. + +## Measurement + +The baseline is upstream `34036657845bf063c875e66a10ad537afa415f35`, which includes #2557. Both binaries use the GPUI workspace release profile, Rust 1.97.1, and the same native FFmpeg libraries. The machine is an Apple M5 with ten logical cores and macOS 27.0.1. + +The existing GPUI `live-edit-benchmark` sends a padding change to `EditorInstance`, requests the paused frame, and awaits the matching frame callback. It uses two Tokio workers, a 1248×702 preview, 60 fps, and the app's `BgraSurface` output on macOS. The timer includes project publication, zoom reconstruction and simulation, decoding, rendering, and frame delivery. It excludes editor startup, the first warmup frame, and window composition. + +Each fixture runs five times per binary. The binary order reverses between pairs. Each run completes 30 edits at each of the 2%, 50%, and 98% playhead positions. The 60 successful runs complete 5,400 timed edits with no terminal failures, renderer errors, or warnings. The runner checks the loaded duration, zoom count, and exact playhead positions. + +The machine has other desktop applications running. The initial one-minute load average is 8.47. Compilation and profiling finish before the timed comparisons. Interleaving limits the effect of background activity, and the ranges below retain its variation. + +The primary H.264 recording is 1920×1080 at 30 fps, with 18,000 video frames. A second suite uses 320×180 video. Cursor events arrive at 60 Hz. The cursor moves smoothly between x=0.1 and x=0.9 over a 3.2-second cycle, with a slower vertical oscillation. Cursor smoothing remains enabled. The recording has no audio or camera. The long case sets one continuous 2× auto-zoom segment. This is a supported manually extended segment, rather than output from the click generator. The control contains four-second zooms every 30 seconds. + +## Results + +Each cell gives the median of five run medians, followed by their minimum and maximum, in milliseconds. The [raw CSV](zoom-clusters.csv) contains every run and region, plus process CPU time and error counts. + +| 1080p fixture | Playhead | Before | After | +| ------------------------------ | -------: | -------------------: | ------------------: | +| 600 seconds, continuous zoom | 2% | 6.58 [4.38, 9.14] | 5.16 [4.95, 7.31] | +| 600 seconds, continuous zoom | 50% | 18.00 [17.21, 18.05] | 9.63 [8.53, 10.44] | +| 600 seconds, continuous zoom | 98% | 20.74 [18.53, 21.43] | 12.34 [8.86, 15.06] | +| 600 seconds, four-second zooms | 2% | 8.26 [5.95, 8.31] | 8.24 [5.93, 8.31] | +| 600 seconds, four-second zooms | 50% | 3.50 [3.33, 4.65] | 3.41 [3.25, 3.56] | +| 600 seconds, four-second zooms | 98% | 4.10 [3.89, 5.23] | 4.08 [4.00, 4.20] | + +| 320×180 fixture | Playhead | Before | After | +| ------------------------------ | -------: | -------------------: | -------------------: | +| 30 seconds, continuous zoom | 2% | 6.01 [5.98, 8.40] | 6.07 [4.95, 8.39] | +| 30 seconds, continuous zoom | 50% | 7.31 [6.41, 8.47] | 7.73 [5.09, 8.14] | +| 30 seconds, continuous zoom | 98% | 7.87 [7.52, 9.15] | 7.96 [7.03, 10.57] | +| 120 seconds, continuous zoom | 2% | 6.04 [5.02, 6.11] | 6.05 [5.94, 8.33] | +| 120 seconds, continuous zoom | 50% | 8.44 [7.77, 9.71] | 7.30 [6.30, 8.99] | +| 120 seconds, continuous zoom | 98% | 10.34 [7.04, 10.88] | 9.06 [8.00, 9.79] | +| 600 seconds, continuous zoom | 2% | 7.83 [5.60, 9.39] | 5.22 [3.98, 9.78] | +| 600 seconds, continuous zoom | 50% | 18.20 [18.14, 18.44] | 11.02 [7.89, 11.94] | +| 600 seconds, continuous zoom | 98% | 20.70 [19.50, 22.53] | 12.18 [11.83, 12.62] | +| 600 seconds, four-second zooms | 2% | 8.37 [6.12, 8.54] | 8.47 [5.18, 8.66] | +| 600 seconds, four-second zooms | 50% | 3.41 [3.22, 4.97] | 3.97 [3.19, 5.05] | +| 600 seconds, four-second zooms | 98% | 4.27 [4.09, 5.14] | 4.28 [3.82, 4.45] | + +An exact two-sided rank-sum check on the five run medians gives p=0.0079 for both the middle and late continuous ten-minute case at either resolution. The other comparisons have p≥0.095. The 320×180 four-second control's middle-position median increases, but its overlapping ranges and p=0.42 do not establish a regression. These small samples do not prove that all short inputs have identical cost. + +Median process CPU time over the 90 edits in the continuous ten-minute 1080p case is 0.749 seconds before and 0.228 seconds after. The average CPU saving is smaller than the original average CPU cost per edit. The latency result therefore does not require saving more CPU work than the baseline performs. + +## Profiler and correctness evidence + +A separate, unreported `sample` run uses the 320×180 baseline with symbols retained. The dominant `targets_at` offsets, +788 and +792, resolve to the reverse cluster loop: load a timestamp, move backward 48 bytes, compare with the requested time, and branch back. `targets_at` accounts for 2,096 of the 2,172 samples below `advance_one_step`. This identifies the precompute CPU cost, not its fraction of whole-edit wall time. Sleeping decoder, GPU, and runtime threads are also present in the profile. + +The ten-minute continuous fixture produces 1,126 clusters and up to 75,002 cached spring samples. The indexed lookup changes a linear reverse search into a binary search for ordered clusters. Unordered lists retain the original scan. The ignored rebuild benchmark compares every sample bit with the linear path outside its timer, including the long and four-second controls. The full ten-minute continuous case has sample hash `8f8ba71ca9a5b122`. + +Validation passes all 307 rendering unit tests, including 30,811 lookup comparisons and 24 complete-timeline comparisons across crop styles, fullscreen holds, reordered moves, clip jumps, and transitions. The export equivalence test also passes for eager, incremental, and seek paths. Rust formatting, all-target compilation, and Python Ruff checks pass. + +Strict Rust 1.97 Clippy reports existing modulo expressions in `crates/project/src/animated_gradient.rs:259,266`, loop suggestions in `crates/rendering/src/lib.rs:738,1325`, and a cloned-reference suggestion in `crates/rendering/src/layers/text.rs:992`. These files match the baseline. Running Clippy again with the original zoom source reproduces the same rendering findings. No lint suppressions are added. Root CI uses Rust 1.88. + +## Commands + +Fixture creation uses the new generator. It refuses an existing destination. + +```sh +python3 scripts/benchmark-zoom-clusters.py /tmp/zoom-long.cap --duration 600 --width 1920 --height 1080 +python3 scripts/benchmark-zoom-clusters.py /tmp/zoom-short.cap --duration 600 --width 1920 --height 1080 --short-zooms +``` + +The existing benchmark command runs from `apps/desktop-gpui` on each revision, with the same generated fixture and native dependency environment. The measurements above use five interleaved runs per revision. + +```sh +cargo +stable run --release --example live-edit-benchmark -- /tmp/zoom-long.cap 30 +cargo +stable run --release --example live-edit-benchmark -- /tmp/zoom-short.cap 30 +``` + +The rendering crate owns the ignored diagnostic benchmark and correctness tests. Its release profile uses size optimization by default. The opt-level override below matches the GPUI app's optimization level. + +```sh +cargo +stable test -p cap-rendering --lib --release --config profile.release.opt-level=3 zoom_cluster_timeline_rebuild -- --ignored --nocapture +cargo +stable test -p cap-rendering --lib +cargo +stable test -p cap-rendering --test export_zoom_equivalence +``` diff --git a/crates/rendering/src/zoom_spring.rs b/crates/rendering/src/zoom_spring.rs index eb85f8b8ed6..497ecd99616 100644 --- a/crates/rendering/src/zoom_spring.rs +++ b/crates/rendering/src/zoom_spring.rs @@ -183,9 +183,41 @@ impl CursorCropMaps { } } +struct ClusterCenters { + clusters: Vec, + time_ordered: bool, +} + +impl ClusterCenters { + fn new(clusters: Vec) -> Self { + let time_ordered = + clusters.is_sorted_by(|left, right| left.start_time_ms <= right.start_time_ms); + Self { + clusters, + time_ordered, + } + } + + fn center_at(&self, time_ms: f64) -> Option<(f64, f64)> { + if self.time_ordered { + let through_time = self + .clusters + .partition_point(|cluster| cluster.start_time_ms <= time_ms); + self.clusters.get(through_time.saturating_sub(1)) + } else { + self.clusters + .iter() + .rev() + .find(|cluster| cluster.start_time_ms <= time_ms) + .or_else(|| self.clusters.first()) + } + .map(ClickCluster::center) + } +} + struct CroppedClusters { crop: Option, - clusters: Vec, + clusters: ClusterCenters, } struct TimedClusterVariant { @@ -195,7 +227,7 @@ struct TimedClusterVariant { } enum ZoomClusters { - Static(Vec), + Static(ClusterCenters), Styled { variants: Vec, styles: Vec, @@ -212,14 +244,14 @@ impl ZoomClusters { crops: &CursorCropMaps, ) -> Self { let build = |crop| { - build_clusters( + ClusterCenters::new(build_clusters( cursor_events, moves_time_ordered, recording_start, recording_end, segment.amount, crop, - ) + )) }; let base = build(crops.base); let overlaps = @@ -275,7 +307,7 @@ impl ZoomClusters { &variants[variant].clusters } }; - cluster_center_at_time(clusters, recording_ms) + clusters.center_at(recording_ms) } } @@ -417,20 +449,6 @@ pub(crate) fn build_clusters( clusters } -/// The active cluster at `time_ms` (RECORDING time) is the last cluster whose -/// first event time is <= t; the spring smooths the discrete re-aims. -pub(crate) fn cluster_center_at_time( - clusters: &[ClickCluster], - time_ms: f64, -) -> Option<(f64, f64)> { - clusters - .iter() - .rev() - .find(|c| c.start_time_ms <= time_ms) - .or_else(|| clusters.first()) - .map(|c| c.center()) -} - /// One entry of the timeline-time -> recording-time mapping derived from /// [`TimelineConfiguration::get_segment_time`]'s accumulation logic. #[derive(Clone, Copy)] @@ -2613,4 +2631,306 @@ mod tests { } } } + + fn linear_cluster_center_at_time( + clusters: &[ClickCluster], + time_ms: f64, + ) -> Option<(f64, f64)> { + clusters + .iter() + .rev() + .find(|cluster| cluster.start_time_ms <= time_ms) + .or_else(|| clusters.first()) + .map(ClickCluster::center) + } + + #[test] + fn cluster_centers_match_linear_lookup_for_all_short_sequences() { + let starts = [ + f64::NEG_INFINITY, + -0.0, + 0.0, + 1.0, + 2.0, + f64::INFINITY, + f64::NAN, + ]; + let queries = [ + f64::NEG_INFINITY, + -1.0, + -0.0, + 0.0, + 1.0_f64.next_down(), + 1.0, + 1.0_f64.next_up(), + 2.0, + 3.0, + f64::INFINITY, + f64::NAN, + ]; + for length in 0..=4 { + for mut sequence in 0..starts.len().pow(length) { + let clusters = (0..length) + .map(|index| { + let start = starts[sequence % starts.len()]; + sequence /= starts.len(); + ClickCluster::new(f64::from(index), f64::from(index) * 0.5, start) + }) + .collect(); + let centers = ClusterCenters::new(clusters); + for time in queries { + assert_eq!( + centers.center_at(time), + linear_cluster_center_at_time(¢ers.clusters, time), + "clusters {:?}, time {time}", + centers.clusters + ); + } + } + } + } + + fn smooth_cluster_cursor(duration_secs: f64) -> CursorEvents { + let moves = (0..(duration_secs * 60.0) as usize) + .map(|index| { + let time = index as f64 / 60.0; + let phase = (time % 3.2) / 3.2; + let triangle = 1.0 - (2.0 * phase - 1.0).abs(); + move_event( + time * 1000.0, + 0.1 + 0.8 * triangle, + 0.5 + 0.2 * (time * std::f64::consts::TAU / 11.0).sin(), + ) + }) + .collect(); + CursorEvents { + moves, + clicks: Vec::new(), + } + } + + fn force_linear_cluster_lookup(timeline: &mut ZoomTransformTimeline) { + for clusters in timeline.clusters.iter_mut().flatten() { + match clusters { + ZoomClusters::Static(centers) => centers.time_ordered = false, + ZoomClusters::Styled { variants, .. } => { + for variant in variants { + variant.clusters.time_ordered = false; + } + } + } + } + } + + #[test] + fn indexed_cluster_samples_match_linear_lookup_across_crops_and_clip_jumps() { + for styled in [false, true] { + let styles = if styled { + vec![ + crop_style( + 1.5, + 6.0, + 0, + Some(Crop { + position: XY::new(0, 0), + size: XY::new(500, 1000), + }), + ), + crop_style( + 3.0, + 4.5, + 1, + Some(Crop { + position: XY::new(0, 500), + size: XY::new(1000, 500), + }), + ), + ] + } else { + Vec::new() + }; + for reordered_clips in [false, true] { + let mut project = + project_with_crop_styles(vec![auto_segment(0.25, 7.75, 2.0)], styles.clone()); + let config = project.timeline.as_mut().unwrap(); + if styled { + config.text_segments.push( + serde_json::from_value(serde_json::json!({ + "start": 2.0, + "end": 3.0, + "layout": "fullscreen", + })) + .unwrap(), + ); + } + let ranges = if reordered_clips { + vec![(0, 0.0, 2.0), (1, 6.0, 8.0), (0, 2.0, 6.0)] + } else { + vec![(0, 0.0, 8.0)] + }; + config.segments = ranges + .into_iter() + .map(|(recording_clip, start, end)| TimelineSegment { + recording_clip, + timescale: 1.0, + start, + end, + name: None, + speed_audio_mode: None, + hide_cursor: None, + volume: None, + }) + .collect(); + if reordered_clips { + config.transitions = [1, 2] + .into_iter() + .map(|segment_index| ClipTransition { + segment_index, + kind: ClipTransitionType::CrossFade, + duration: 0.25, + }) + .collect(); + } + for reordered_moves in [false, true] { + let mut cursor = smooth_cluster_cursor(8.0); + if reordered_moves { + cursor.moves.reverse(); + } + for (recording_clip, outgoing) in [(0, false), (0, true), (1, false)] { + let build = || { + if outgoing { + ZoomTransformTimeline::from_project_for_outgoing_clip( + &project, + &cursor, + 8.0, + XY::new(1000, 1000), + recording_clip, + ) + } else { + ZoomTransformTimeline::from_project_for_clip( + &project, + &cursor, + 8.0, + XY::new(1000, 1000), + recording_clip, + ) + } + }; + let mut indexed = build(); + let mut linear = build(); + force_linear_cluster_lookup(&mut linear); + indexed.precompute(); + for time in [1.0, 3.5, 2.0, 8.0] { + linear.ensure_precomputed_until(time); + } + assert_samples_identical(&indexed, &linear); + } + } + } + } + } + + #[test] + #[ignore = "benchmark; run with --release --ignored --nocapture"] + fn zoom_cluster_timeline_rebuild() { + use std::time::{Duration, Instant}; + + const RUNS: usize = 7; + let percentile = |times: &mut Vec, p: f64| { + times.sort_unstable(); + let index = ((times.len() - 1) as f64 * p).round() as usize; + times[index].as_secs_f64() * 1000.0 + }; + let cluster_count = |timeline: &ZoomTransformTimeline| { + timeline + .clusters + .iter() + .flatten() + .map(|clusters| match clusters { + ZoomClusters::Static(centers) => centers.clusters.len(), + ZoomClusters::Styled { variants, .. } => variants + .iter() + .map(|variant| variant.clusters.clusters.len()) + .sum(), + }) + .sum::() + }; + println!( + "| seconds | zoom | playhead | moves | clusters | samples | build p50 ms | precompute p50 ms | rebuild p50 ms | rebuild min ms | rebuild max ms | samples hash |" + ); + println!( + "| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |" + ); + for duration_secs in [60.0, 600.0] { + let cursor = smooth_cluster_cursor(duration_secs); + for long_zoom in [false, true] { + let mut project = benchmark_project(duration_secs, (duration_secs / 30.0) as usize); + if long_zoom { + project.timeline.as_mut().unwrap().zoom_segments = + vec![auto_segment(0.0, duration_secs, 2.0)]; + } + for fraction in [0.5, 1.0] { + let playhead = (duration_secs * fraction) as f32; + let mut build_times = Vec::with_capacity(RUNS); + let mut precompute_times = Vec::with_capacity(RUNS); + let mut rebuild_times = Vec::with_capacity(RUNS); + let mut expected_hash = None; + let mut clusters = 0; + let mut samples = 0; + let mut reference = ZoomTransformTimeline::from_project_for_clip( + &project, + &cursor, + duration_secs, + XY::new(1920, 1080), + 0, + ); + force_linear_cluster_lookup(&mut reference); + reference.ensure_precomputed_until(playhead); + for run in 0..=RUNS { + let build_start = Instant::now(); + let mut timeline = ZoomTransformTimeline::from_project_for_clip( + &project, + &cursor, + duration_secs, + XY::new(1920, 1080), + 0, + ); + let build_elapsed = build_start.elapsed(); + let precompute_start = Instant::now(); + timeline.ensure_precomputed_until(playhead); + let precompute_elapsed = precompute_start.elapsed(); + let rebuild_elapsed = build_start.elapsed(); + assert_samples_identical(&timeline, &reference); + let hash = samples_hash(&timeline); + if let Some(expected_hash) = expected_hash { + assert_eq!(hash, expected_hash); + } else { + expected_hash = Some(hash); + } + clusters = cluster_count(&timeline); + samples = timeline.samples.len(); + assert!(samples > (f64::from(playhead) * 1000.0 / STEP_MS) as usize); + std::hint::black_box(timeline.sample(playhead)); + if run > 0 { + build_times.push(build_elapsed); + precompute_times.push(precompute_elapsed); + rebuild_times.push(rebuild_elapsed); + } + } + let rebuild_p50 = percentile(&mut rebuild_times, 0.5); + println!( + "| {duration_secs:.0} | {} | {:.0}% | {} | {clusters} | {samples} | {:.3} | {:.3} | {rebuild_p50:.3} | {:.3} | {:.3} | {:016x} |", + if long_zoom { "long" } else { "4s every 30s" }, + fraction * 100.0, + cursor.moves.len(), + percentile(&mut build_times, 0.5), + percentile(&mut precompute_times, 0.5), + rebuild_times[0].as_secs_f64() * 1000.0, + rebuild_times[RUNS - 1].as_secs_f64() * 1000.0, + expected_hash.unwrap(), + ); + } + } + } + } } diff --git a/scripts/benchmark-zoom-clusters.py b/scripts/benchmark-zoom-clusters.py new file mode 100644 index 00000000000..c717c1a3b72 --- /dev/null +++ b/scripts/benchmark-zoom-clusters.py @@ -0,0 +1,133 @@ +import argparse +import json +import math +import subprocess +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("target", type=Path) + parser.add_argument("--duration", type=int, default=120) + parser.add_argument("--width", type=int, default=320) + parser.add_argument("--height", type=int, default=180) + parser.add_argument("--short-zooms", action="store_true") + args = parser.parse_args() + if args.duration <= 0: + parser.error("duration must be positive") + if args.width <= 0 or args.height <= 0 or args.width % 2 or args.height % 2: + parser.error("width and height must be positive even integers") + target = args.target.resolve() + if target.exists(): + parser.error("target must not exist") + target.mkdir(parents=True) + + subprocess.run( + [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "error", + "-nostdin", + "-f", + "lavfi", + "-i", + f"testsrc2=size={args.width}x{args.height}:rate=30", + "-t", + str(args.duration), + "-an", + "-c:v", + "libx264", + "-preset", + "ultrafast", + "-crf", + "35", + "-g", + "30", + "-keyint_min", + "30", + "-sc_threshold", + "0", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + str(target / "display.mp4"), + ], + check=True, + ) + + moves = [] + for index in range(args.duration * 60): + time = index / 60 + phase = (time % 3.2) / 3.2 + triangle = 1 - abs(2 * phase - 1) + moves.append( + { + "active_modifiers": [], + "cursor_id": "default", + "time_ms": time * 1000, + "x": 0.1 + 0.8 * triangle, + "y": 0.5 + 0.2 * math.sin(time * math.tau / 11), + } + ) + (target / "cursor.json").write_text(json.dumps({"moves": moves, "clicks": []})) + metadata = { + "pretty_name": f"Synthetic smooth cursor zoom ({args.duration}s)", + "platform": "MacOS", + "segments": [ + { + "display": {"path": "display.mp4", "fps": 30}, + "cursor": "cursor.json", + } + ], + } + project = { + "camera": {"hide": True}, + "audio": {"mute": True}, + "cursor": {"type": "circle", "useSvg": False, "raw": False}, + "screenMotionBlur": 1.0, + "textSizeVersion": 1, + "textAnimVersion": 1, + "timeline": { + "segments": [ + { + "recordingSegment": 0, + "timescale": 1.0, + "start": 0.0, + "end": args.duration, + } + ], + "zoomSegments": [ + {"start": 0.0, "end": args.duration, "amount": 2.0, "mode": "auto"} + ], + }, + } + if args.short_zooms: + project["timeline"]["zoomSegments"] = [ + { + "start": start, + "end": min(start + 4, args.duration), + "amount": 2, + "mode": "auto", + } + for start in range(8, args.duration, 30) + ] + (target / "recording-meta.json").write_text(json.dumps(metadata, indent=2) + "\n") + (target / "project-config.json").write_text(json.dumps(project, indent=2) + "\n") + + print( + json.dumps( + { + "project": str(target), + "durationSecs": args.duration, + "moves": len(moves), + "videoSize": [args.width, args.height], + "shortZooms": args.short_zooms, + } + ) + ) + + +if __name__ == "__main__": + main() From e1ecebb8d4b60ab70709c7c0b297457170a94a69 Mon Sep 17 00:00:00 2001 From: Colin Kim <121141519+colinvkim@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:25:26 -0700 Subject: [PATCH 2/4] refactor(rendering): simplify zoom lookup patch --- crates/rendering/benches/zoom-clusters.csv | 61 ------ crates/rendering/benches/zoom-clusters.md | 83 ------- crates/rendering/src/zoom_spring.rs | 244 --------------------- scripts/benchmark-zoom-clusters.py | 133 ----------- 4 files changed, 521 deletions(-) delete mode 100644 crates/rendering/benches/zoom-clusters.csv delete mode 100644 crates/rendering/benches/zoom-clusters.md delete mode 100644 scripts/benchmark-zoom-clusters.py diff --git a/crates/rendering/benches/zoom-clusters.csv b/crates/rendering/benches/zoom-clusters.csv deleted file mode 100644 index aea7fc76e17..00000000000 --- a/crates/rendering/benches/zoom-clusters.csv +++ /dev/null @@ -1,61 +0,0 @@ -fixture,run,variant,durationSecs,zoomSegments,completedAwaitedEdits,terminalFailures,stderrErrors,stderrWarnings,editCpuSecs,early_atSecs,early_seekMs,early_editMedianMs,early_editP95Ms,middle_atSecs,middle_seekMs,middle_editMedianMs,middle_editP95Ms,late_atSecs,late_seekMs,late_editMedianMs,late_editP95Ms -smooth-30.cap,1,baseline,30.0,1,90,0,0,0,0.176116,0.5833333333333334,14.535833,6.014083,9.471791999999999,14.983333333333333,22.618125,6.410792,10.370208,29.383333333333333,22.452792,9.150917,13.705333 -smooth-30.cap,1,candidate,30.0,1,90,0,0,0,0.193078,0.5833333333333334,15.171125,6.070167,8.379707999999999,14.983333333333333,22.378709,8.138959,10.947792,29.383333333333333,23.029999999999998,10.571041,13.780041 -smooth-30.cap,2,candidate,30.0,1,90,0,0,0,0.14635,0.5833333333333334,18.434791999999998,6.074834,8.364375,14.983333333333333,31.232291999999998,7.974458,11.515917,29.383333333333333,18.025917,7.031833000000001,10.406708 -smooth-30.cap,2,baseline,30.0,1,90,0,0,0,0.20678600000000003,0.5833333333333334,15.822709000000001,8.396249999999998,9.595459,14.983333333333333,25.102541,7.688875,9.992666000000002,29.383333333333333,27.852291,7.868541,11.2005 -smooth-30.cap,3,baseline,30.0,1,90,0,0,0,0.12227999999999997,0.5833333333333334,14.68525,6.005958,7.184167,14.983333333333333,22.380459,7.3140410000000005,10.767125,29.383333333333333,18.689542,7.515541,10.470417 -smooth-30.cap,3,candidate,30.0,1,90,0,0,0,0.15273599999999998,0.5833333333333334,14.435292,5.970083,7.165792,14.983333333333333,21.131332999999998,5.092625,9.655958,29.383333333333333,20.224290999999997,7.9645410000000005,10.515416 -smooth-30.cap,4,candidate,30.0,1,90,0,0,0,0.17471899999999993,0.5833333333333334,16.013042000000002,8.393374999999999,9.563416,14.983333333333333,32.867541,7.733042,11.358792,29.383333333333333,32.4925,8.461916,11.756166 -smooth-30.cap,4,baseline,30.0,1,90,0,0,0,0.159136,0.5833333333333334,19.161583,5.9818750000000005,7.178249999999999,14.983333333333333,30.798333,8.471459000000001,11.462458999999999,29.383333333333333,21.190875000000002,7.8408750000000005,9.906208 -smooth-30.cap,5,baseline,30.0,1,90,0,0,0,0.17704,0.5833333333333334,15.852834,6.08925,9.427833,14.983333333333333,25.203792,6.533167000000001,9.871292,29.383333333333333,28.503209000000002,8.115458,9.365 -smooth-30.cap,5,candidate,30.0,1,90,0,0,0,0.15830499999999997,0.5833333333333334,14.468958,4.949833,9.428958,14.983333333333333,19.027790999999997,5.4025419999999995,6.8065,29.383333333333333,24.3315,7.816,12.702833 -smooth-120.cap,1,baseline,120.0,1,90,0,0,0,0.224209,2.3833333333333333,15.726207999999998,6.066125,8.480959,59.983333333333334,24.454625,8.39775,10.291042000000001,117.58333333333333,27.800667,10.882,14.25825 -smooth-120.cap,1,candidate,120.0,1,90,0,0,0,0.251459,2.3833333333333333,14.195167,7.284833,8.471667,59.983333333333334,29.787125,8.987958,11.002875000000001,117.58333333333333,30.72525,9.794625,13.674916 -smooth-120.cap,2,candidate,120.0,1,90,0,0,0,0.193544,2.3833333333333333,15.332709,5.93825,8.416916,59.983333333333334,21.452875,7.297,10.384834000000001,117.58333333333333,22.954791,9.060417,11.821584 -smooth-120.cap,2,baseline,120.0,1,90,0,0,0,0.20932700000000004,2.3833333333333333,16.424,6.039708,8.50275,59.983333333333334,24.345125,8.442958,12.520708,117.58333333333333,26.141583,10.481541,16.255125 -smooth-120.cap,3,baseline,120.0,1,90,0,0,0,0.19719,2.3833333333333333,23.206833,5.02325,8.41225,59.983333333333334,33.208917,7.766833,12.16,117.58333333333333,23.45425,9.719458,12.879541 -smooth-120.cap,3,candidate,120.0,1,90,0,0,0,0.171051,2.3833333333333333,12.924667000000001,6.052166000000001,8.533875,59.983333333333334,25.25975,6.2983329999999995,11.30725,117.58333333333333,19.834334,8.003,10.947458000000001 -smooth-120.cap,4,candidate,120.0,1,90,0,0,0,0.17092500000000005,2.3833333333333333,14.21225,5.993875,9.621792000000001,59.983333333333334,28.377916,6.891958,10.863958,117.58333333333333,28.847250000000003,8.117958999999999,10.484333 -smooth-120.cap,4,baseline,120.0,1,90,0,0,0,0.146543,2.3833333333333333,14.073584,5.985125,9.788625,59.983333333333334,23.654333,9.14675,11.713959,117.58333333333333,18.633792,7.041083,9.425625 -smooth-120.cap,5,baseline,120.0,1,90,0,0,0,0.17456399999999997,2.3833333333333333,17.661417,6.105916000000001,8.461917,59.983333333333334,26.056791,9.710208999999999,15.137292,117.58333333333333,22.76875,10.335375,13.898375 -smooth-120.cap,5,candidate,120.0,1,90,0,0,0,0.18976700000000002,2.3833333333333333,15.454959,8.333874999999999,9.628708,59.983333333333334,26.591083,7.852792000000001,10.353875,117.58333333333333,28.871791,9.567417,12.074625000000001 -smooth-600.cap,1,baseline,600.0,1,90,0,0,0,0.7870170000000001,11.983333333333333,20.805291999999998,7.8325830000000005,9.270875,299.98333333333335,33.287625,18.199458,20.498209,587.9833333333333,25.181375,20.704832999999997,22.787625 -smooth-600.cap,1,candidate,600.0,1,90,0,0,0,0.29328199999999993,11.983333333333333,18.462709,5.13025,9.870084,299.98333333333335,27.7975,10.9885,12.097833000000001,587.9833333333333,25.595,12.488624999999999,13.95275 -smooth-600.cap,2,candidate,600.0,1,90,0,0,0,0.300285,11.983333333333333,18.231167,8.74875,9.968665999999999,299.98333333333335,28.597832999999998,11.159917,12.444875,587.9833333333333,25.355584,12.18375,13.497291 -smooth-600.cap,2,baseline,600.0,1,90,0,0,0,0.799258,11.983333333333333,19.45475,8.029917,10.616541999999999,299.98333333333335,37.664375,18.138749999999998,20.003292,587.9833333333333,23.160957999999997,21.5395,23.0665 -smooth-600.cap,3,baseline,600.0,1,90,0,0,0,0.7913209999999999,11.983333333333333,16.532708,5.625167,9.744667,299.98333333333335,30.937042,18.237083000000002,20.668958,587.9833333333333,27.398,19.503417,22.644166000000002 -smooth-600.cap,3,candidate,600.0,1,90,0,0,0,0.24995000000000003,11.983333333333333,18.864667,5.2249159999999994,11.944958,299.98333333333335,14.497333,7.89425,10.152750000000001,587.9833333333333,21.846,12.16025,14.034958999999999 -smooth-600.cap,4,candidate,600.0,1,90,0,0,0,0.306248,11.983333333333333,18.400458,9.780875,10.953583,299.98333333333335,39.607666,11.93975,13.570958,587.9833333333333,24.109708,11.828875,13.841417 -smooth-600.cap,4,baseline,600.0,1,90,0,0,0,0.7959860000000001,11.983333333333333,22.244875,9.385083,10.936625,299.98333333333335,37.927958,18.19375,19.8995,587.9833333333333,27.922,19.754082999999998,22.30625 -smooth-600.cap,5,baseline,600.0,1,90,0,0,0,0.8500629999999999,11.983333333333333,19.923083000000002,5.600499999999999,7.425916,299.98333333333335,31.534542,18.435792,19.470292,587.9833333333333,28.754790999999997,22.533792000000002,27.9235 -smooth-600.cap,5,candidate,600.0,1,90,0,0,0,0.29704699999999995,11.983333333333333,16.002083,3.9802500000000003,7.591292,299.98333333333335,23.937,11.020875,12.189625000000001,587.9833333333333,23.991583000000002,12.61575,14.9935 -short-600.cap,1,baseline,600.0,20,90,0,0,0,0.18384099999999995,11.983333333333333,17.502584000000002,8.402415999999999,9.662833,299.98333333333335,24.26375,4.972042,6.084542,587.9833333333333,20.83075,4.603917,7.335167 -short-600.cap,1,candidate,600.0,20,90,0,0,0,0.18166500000000005,11.983333333333333,18.54975,8.476416,9.528,299.98333333333335,26.784208,5.045084,6.180333,587.9833333333333,16.545583999999998,4.450791,5.7895840000000005 -short-600.cap,2,candidate,600.0,20,90,0,0,0,0.19037800000000005,11.983333333333333,19.514375,8.65525,9.812208,299.98333333333335,22.327042,3.965916,5.255166,587.9833333333333,22.590041000000003,4.4185,6.105625 -short-600.cap,2,baseline,600.0,20,90,0,0,0,0.15605000000000002,11.983333333333333,26.488125,6.218292,9.588332999999999,299.98333333333335,20.710375,3.46575,5.692333,587.9833333333333,17.165,4.20925,5.40575 -short-600.cap,3,baseline,600.0,20,90,0,0,0,0.13899400000000003,11.983333333333333,19.96575,8.536833000000001,9.681583,299.98333333333335,13.389833,3.21775,4.413875,587.9833333333333,17.208791,4.093292,6.333333 -short-600.cap,3,candidate,600.0,20,90,0,0,0,0.171271,11.983333333333333,18.468999999999998,8.466916999999999,9.65275,299.98333333333335,20.040875,4.107875000000001,5.059125,587.9833333333333,20.485500000000002,4.170084,5.580792000000001 -short-600.cap,4,candidate,600.0,20,90,0,0,0,0.14888799999999996,11.983333333333333,19.622833,5.179084,12.993333,299.98333333333335,14.398833,3.522209,4.717333,587.9833333333333,17.7845,4.2772499999999996,7.453542 -short-600.cap,4,baseline,600.0,20,90,0,0,0,0.16166200000000003,11.983333333333333,18.580291,8.365875,14.466166000000001,299.98333333333335,21.544125,3.3558339999999998,6.638459,587.9833333333333,14.053125,5.141,8.518333 -short-600.cap,5,baseline,600.0,20,90,0,0,0,0.13187200000000004,11.983333333333333,23.094167,6.122291000000001,8.23225,299.98333333333335,12.352333,3.4052499999999997,5.68825,587.9833333333333,14.874167,4.271417,6.147667 -short-600.cap,5,candidate,600.0,20,90,0,0,0,0.1251480000000001,11.983333333333333,18.681292,6.097667,8.411458999999999,299.98333333333335,14.862959,3.1945419999999998,5.466042,587.9833333333333,12.712041000000001,3.81975,5.2449580000000005 -full-600.cap,1,baseline,600.0,1,90,0,0,0,0.793398,11.983333333333333,38.12525,9.143625,10.412958,299.98333333333335,47.956209,18.054000000000002,19.226042,587.9833333333333,35.881375,21.426542,25.209792 -full-600.cap,1,candidate,600.0,1,90,0,0,0,0.2794740000000001,11.983333333333333,33.098416,5.608458000000001,7.411541,299.98333333333335,44.592542,8.525917,12.928875,587.9833333333333,50.435874999999996,15.05625,46.178583 -full-600.cap,2,candidate,600.0,1,90,0,0,0,0.2102550000000001,11.983333333333333,31.675167,5.0607500000000005,7.234875,299.98333333333335,29.008291,10.156,11.570542,587.9833333333333,35.719167000000006,8.864458,12.521083 -full-600.cap,2,baseline,600.0,1,90,0,0,0,0.7273779999999999,11.983333333333333,28.10925,6.579542,10.038042,299.98333333333335,34.837667,17.214,18.638542,587.9833333333333,33.684125,20.893333,22.05 -full-600.cap,3,baseline,600.0,1,90,0,0,0,0.727938,11.983333333333333,26.689375000000002,4.377167,9.088333,299.98333333333335,43.131958,17.886375,20.495124999999998,587.9833333333333,31.318749999999998,18.5335,21.800375 -full-600.cap,3,candidate,600.0,1,90,0,0,0,0.22764099999999998,11.983333333333333,25.579833999999998,4.951416,8.518333,299.98333333333335,36.102458,10.437750000000001,11.473333,587.9833333333333,30.738083,12.865167,14.070915999999999 -full-600.cap,4,candidate,600.0,1,90,0,0,0,0.22326500000000002,11.983333333333333,25.176209,7.310334,9.607125,299.98333333333335,27.181333,8.835334,10.491208,587.9833333333333,29.287084,11.609417,12.840709 -full-600.cap,4,baseline,600.0,1,90,0,0,0,0.7603270000000001,11.983333333333333,28.901666,9.077292,10.549959,299.98333333333335,41.204499999999996,18.013250000000003,18.139291,587.9833333333333,33.018458,20.744041000000003,23.427417 -full-600.cap,5,baseline,600.0,1,90,0,0,0,0.74926,11.983333333333333,26.578,5.412208,6.711208,299.98333333333335,44.466832999999994,17.996417,19.164082999999998,587.9833333333333,35.046375,19.518167,23.114875 -full-600.cap,5,candidate,600.0,1,90,0,0,0,0.25702100000000005,11.983333333333333,28.757875,5.155583,8.602291999999998,299.98333333333335,39.540040999999995,9.625125,12.353625,587.9833333333333,31.536250000000003,12.342042,14.453875 -full-short-600.cap,1,baseline,600.0,20,90,0,0,0,0.13350600000000004,11.983333333333333,25.029708000000003,7.089042,10.560917,299.98333333333335,32.516208,4.649208,5.986458,587.9833333333333,31.411458000000003,5.225917,5.449792 -full-short-600.cap,1,candidate,600.0,20,90,0,0,0,0.12485600000000001,11.983333333333333,25.301417,5.9717080000000005,9.47575,299.98333333333335,23.605833999999998,3.414958,4.599959,587.9833333333333,25.462333,4.19625,5.358209 -full-short-600.cap,2,candidate,600.0,20,90,0,0,0,0.12378300000000003,11.983333333333333,28.065542,8.269125,9.441167,299.98333333333335,25.191917,3.429792,4.562125,587.9833333333333,25.460041999999998,4.161625000000001,6.342875 -full-short-600.cap,2,baseline,600.0,20,90,0,0,0,0.12141800000000001,11.983333333333333,25.996416,8.259542,9.563125000000001,299.98333333333335,28.879915999999998,3.49825,4.659042,587.9833333333333,30.213875,3.8865000000000003,5.052875 -full-short-600.cap,3,baseline,600.0,20,90,0,0,0,0.11791599999999999,11.983333333333333,28.318833,5.945709000000001,9.446665999999999,299.98333333333335,25.501333000000002,3.388,4.531375,587.9833333333333,26.651457999999998,4.040916,5.27925 -full-short-600.cap,3,candidate,600.0,20,90,0,0,0,0.115095,11.983333333333333,26.286417,5.933041,7.138208,299.98333333333335,26.458375,3.333833,5.629208,587.9833333333333,23.773542000000003,4.029917,5.15975 -full-short-600.cap,4,candidate,600.0,20,90,0,0,0,0.12557899999999997,11.983333333333333,26.28625,8.244792,10.472083,299.98333333333335,23.680041999999997,3.56275,4.935959,587.9833333333333,26.645666,4.07675,5.30425 -full-short-600.cap,4,baseline,600.0,20,90,0,0,0,0.12949300000000002,11.983333333333333,26.042708,8.278875,9.474375,299.98333333333335,34.110208,3.5752919999999997,4.860125,587.9833333333333,25.577166,4.102333,5.285416000000001 -full-short-600.cap,5,baseline,600.0,20,90,0,0,0,0.11517700000000002,11.983333333333333,25.45225,8.312792,10.509792000000001,299.98333333333335,26.264542,3.327333,5.532459,587.9833333333333,28.734917,4.919207999999999,6.169084 -full-short-600.cap,5,candidate,600.0,20,90,0,0,0,0.127569,11.983333333333333,25.420375,8.314541,10.450834,299.98333333333335,27.60925,3.252917,5.809333,587.9833333333333,25.541417,3.9958329999999997,5.127667 diff --git a/crates/rendering/benches/zoom-clusters.md b/crates/rendering/benches/zoom-clusters.md deleted file mode 100644 index 4d5335f2e37..00000000000 --- a/crates/rendering/benches/zoom-clusters.md +++ /dev/null @@ -1,83 +0,0 @@ -# Auto-zoom cluster benchmark results - -On the synthetic ten-minute 1080p recording, a paused edit at 9:48 takes 20.74 ms before and 12.34 ms after, a 40.5% latency reduction. These are medians of five run medians. The ranges are 18.53–21.43 ms before and 8.86–15.06 ms after. - -The improvement concerns long auto-zoom segments. Shorter recordings and four-second zoom controls have overlapping run ranges and do not establish a performance difference. - -## Measurement - -The baseline is upstream `34036657845bf063c875e66a10ad537afa415f35`, which includes #2557. Both binaries use the GPUI workspace release profile, Rust 1.97.1, and the same native FFmpeg libraries. The machine is an Apple M5 with ten logical cores and macOS 27.0.1. - -The existing GPUI `live-edit-benchmark` sends a padding change to `EditorInstance`, requests the paused frame, and awaits the matching frame callback. It uses two Tokio workers, a 1248×702 preview, 60 fps, and the app's `BgraSurface` output on macOS. The timer includes project publication, zoom reconstruction and simulation, decoding, rendering, and frame delivery. It excludes editor startup, the first warmup frame, and window composition. - -Each fixture runs five times per binary. The binary order reverses between pairs. Each run completes 30 edits at each of the 2%, 50%, and 98% playhead positions. The 60 successful runs complete 5,400 timed edits with no terminal failures, renderer errors, or warnings. The runner checks the loaded duration, zoom count, and exact playhead positions. - -The machine has other desktop applications running. The initial one-minute load average is 8.47. Compilation and profiling finish before the timed comparisons. Interleaving limits the effect of background activity, and the ranges below retain its variation. - -The primary H.264 recording is 1920×1080 at 30 fps, with 18,000 video frames. A second suite uses 320×180 video. Cursor events arrive at 60 Hz. The cursor moves smoothly between x=0.1 and x=0.9 over a 3.2-second cycle, with a slower vertical oscillation. Cursor smoothing remains enabled. The recording has no audio or camera. The long case sets one continuous 2× auto-zoom segment. This is a supported manually extended segment, rather than output from the click generator. The control contains four-second zooms every 30 seconds. - -## Results - -Each cell gives the median of five run medians, followed by their minimum and maximum, in milliseconds. The [raw CSV](zoom-clusters.csv) contains every run and region, plus process CPU time and error counts. - -| 1080p fixture | Playhead | Before | After | -| ------------------------------ | -------: | -------------------: | ------------------: | -| 600 seconds, continuous zoom | 2% | 6.58 [4.38, 9.14] | 5.16 [4.95, 7.31] | -| 600 seconds, continuous zoom | 50% | 18.00 [17.21, 18.05] | 9.63 [8.53, 10.44] | -| 600 seconds, continuous zoom | 98% | 20.74 [18.53, 21.43] | 12.34 [8.86, 15.06] | -| 600 seconds, four-second zooms | 2% | 8.26 [5.95, 8.31] | 8.24 [5.93, 8.31] | -| 600 seconds, four-second zooms | 50% | 3.50 [3.33, 4.65] | 3.41 [3.25, 3.56] | -| 600 seconds, four-second zooms | 98% | 4.10 [3.89, 5.23] | 4.08 [4.00, 4.20] | - -| 320×180 fixture | Playhead | Before | After | -| ------------------------------ | -------: | -------------------: | -------------------: | -| 30 seconds, continuous zoom | 2% | 6.01 [5.98, 8.40] | 6.07 [4.95, 8.39] | -| 30 seconds, continuous zoom | 50% | 7.31 [6.41, 8.47] | 7.73 [5.09, 8.14] | -| 30 seconds, continuous zoom | 98% | 7.87 [7.52, 9.15] | 7.96 [7.03, 10.57] | -| 120 seconds, continuous zoom | 2% | 6.04 [5.02, 6.11] | 6.05 [5.94, 8.33] | -| 120 seconds, continuous zoom | 50% | 8.44 [7.77, 9.71] | 7.30 [6.30, 8.99] | -| 120 seconds, continuous zoom | 98% | 10.34 [7.04, 10.88] | 9.06 [8.00, 9.79] | -| 600 seconds, continuous zoom | 2% | 7.83 [5.60, 9.39] | 5.22 [3.98, 9.78] | -| 600 seconds, continuous zoom | 50% | 18.20 [18.14, 18.44] | 11.02 [7.89, 11.94] | -| 600 seconds, continuous zoom | 98% | 20.70 [19.50, 22.53] | 12.18 [11.83, 12.62] | -| 600 seconds, four-second zooms | 2% | 8.37 [6.12, 8.54] | 8.47 [5.18, 8.66] | -| 600 seconds, four-second zooms | 50% | 3.41 [3.22, 4.97] | 3.97 [3.19, 5.05] | -| 600 seconds, four-second zooms | 98% | 4.27 [4.09, 5.14] | 4.28 [3.82, 4.45] | - -An exact two-sided rank-sum check on the five run medians gives p=0.0079 for both the middle and late continuous ten-minute case at either resolution. The other comparisons have p≥0.095. The 320×180 four-second control's middle-position median increases, but its overlapping ranges and p=0.42 do not establish a regression. These small samples do not prove that all short inputs have identical cost. - -Median process CPU time over the 90 edits in the continuous ten-minute 1080p case is 0.749 seconds before and 0.228 seconds after. The average CPU saving is smaller than the original average CPU cost per edit. The latency result therefore does not require saving more CPU work than the baseline performs. - -## Profiler and correctness evidence - -A separate, unreported `sample` run uses the 320×180 baseline with symbols retained. The dominant `targets_at` offsets, +788 and +792, resolve to the reverse cluster loop: load a timestamp, move backward 48 bytes, compare with the requested time, and branch back. `targets_at` accounts for 2,096 of the 2,172 samples below `advance_one_step`. This identifies the precompute CPU cost, not its fraction of whole-edit wall time. Sleeping decoder, GPU, and runtime threads are also present in the profile. - -The ten-minute continuous fixture produces 1,126 clusters and up to 75,002 cached spring samples. The indexed lookup changes a linear reverse search into a binary search for ordered clusters. Unordered lists retain the original scan. The ignored rebuild benchmark compares every sample bit with the linear path outside its timer, including the long and four-second controls. The full ten-minute continuous case has sample hash `8f8ba71ca9a5b122`. - -Validation passes all 307 rendering unit tests, including 30,811 lookup comparisons and 24 complete-timeline comparisons across crop styles, fullscreen holds, reordered moves, clip jumps, and transitions. The export equivalence test also passes for eager, incremental, and seek paths. Rust formatting, all-target compilation, and Python Ruff checks pass. - -Strict Rust 1.97 Clippy reports existing modulo expressions in `crates/project/src/animated_gradient.rs:259,266`, loop suggestions in `crates/rendering/src/lib.rs:738,1325`, and a cloned-reference suggestion in `crates/rendering/src/layers/text.rs:992`. These files match the baseline. Running Clippy again with the original zoom source reproduces the same rendering findings. No lint suppressions are added. Root CI uses Rust 1.88. - -## Commands - -Fixture creation uses the new generator. It refuses an existing destination. - -```sh -python3 scripts/benchmark-zoom-clusters.py /tmp/zoom-long.cap --duration 600 --width 1920 --height 1080 -python3 scripts/benchmark-zoom-clusters.py /tmp/zoom-short.cap --duration 600 --width 1920 --height 1080 --short-zooms -``` - -The existing benchmark command runs from `apps/desktop-gpui` on each revision, with the same generated fixture and native dependency environment. The measurements above use five interleaved runs per revision. - -```sh -cargo +stable run --release --example live-edit-benchmark -- /tmp/zoom-long.cap 30 -cargo +stable run --release --example live-edit-benchmark -- /tmp/zoom-short.cap 30 -``` - -The rendering crate owns the ignored diagnostic benchmark and correctness tests. Its release profile uses size optimization by default. The opt-level override below matches the GPUI app's optimization level. - -```sh -cargo +stable test -p cap-rendering --lib --release --config profile.release.opt-level=3 zoom_cluster_timeline_rebuild -- --ignored --nocapture -cargo +stable test -p cap-rendering --lib -cargo +stable test -p cap-rendering --test export_zoom_equivalence -``` diff --git a/crates/rendering/src/zoom_spring.rs b/crates/rendering/src/zoom_spring.rs index 497ecd99616..90012d90bb3 100644 --- a/crates/rendering/src/zoom_spring.rs +++ b/crates/rendering/src/zoom_spring.rs @@ -2689,248 +2689,4 @@ mod tests { } } } - - fn smooth_cluster_cursor(duration_secs: f64) -> CursorEvents { - let moves = (0..(duration_secs * 60.0) as usize) - .map(|index| { - let time = index as f64 / 60.0; - let phase = (time % 3.2) / 3.2; - let triangle = 1.0 - (2.0 * phase - 1.0).abs(); - move_event( - time * 1000.0, - 0.1 + 0.8 * triangle, - 0.5 + 0.2 * (time * std::f64::consts::TAU / 11.0).sin(), - ) - }) - .collect(); - CursorEvents { - moves, - clicks: Vec::new(), - } - } - - fn force_linear_cluster_lookup(timeline: &mut ZoomTransformTimeline) { - for clusters in timeline.clusters.iter_mut().flatten() { - match clusters { - ZoomClusters::Static(centers) => centers.time_ordered = false, - ZoomClusters::Styled { variants, .. } => { - for variant in variants { - variant.clusters.time_ordered = false; - } - } - } - } - } - - #[test] - fn indexed_cluster_samples_match_linear_lookup_across_crops_and_clip_jumps() { - for styled in [false, true] { - let styles = if styled { - vec![ - crop_style( - 1.5, - 6.0, - 0, - Some(Crop { - position: XY::new(0, 0), - size: XY::new(500, 1000), - }), - ), - crop_style( - 3.0, - 4.5, - 1, - Some(Crop { - position: XY::new(0, 500), - size: XY::new(1000, 500), - }), - ), - ] - } else { - Vec::new() - }; - for reordered_clips in [false, true] { - let mut project = - project_with_crop_styles(vec![auto_segment(0.25, 7.75, 2.0)], styles.clone()); - let config = project.timeline.as_mut().unwrap(); - if styled { - config.text_segments.push( - serde_json::from_value(serde_json::json!({ - "start": 2.0, - "end": 3.0, - "layout": "fullscreen", - })) - .unwrap(), - ); - } - let ranges = if reordered_clips { - vec![(0, 0.0, 2.0), (1, 6.0, 8.0), (0, 2.0, 6.0)] - } else { - vec![(0, 0.0, 8.0)] - }; - config.segments = ranges - .into_iter() - .map(|(recording_clip, start, end)| TimelineSegment { - recording_clip, - timescale: 1.0, - start, - end, - name: None, - speed_audio_mode: None, - hide_cursor: None, - volume: None, - }) - .collect(); - if reordered_clips { - config.transitions = [1, 2] - .into_iter() - .map(|segment_index| ClipTransition { - segment_index, - kind: ClipTransitionType::CrossFade, - duration: 0.25, - }) - .collect(); - } - for reordered_moves in [false, true] { - let mut cursor = smooth_cluster_cursor(8.0); - if reordered_moves { - cursor.moves.reverse(); - } - for (recording_clip, outgoing) in [(0, false), (0, true), (1, false)] { - let build = || { - if outgoing { - ZoomTransformTimeline::from_project_for_outgoing_clip( - &project, - &cursor, - 8.0, - XY::new(1000, 1000), - recording_clip, - ) - } else { - ZoomTransformTimeline::from_project_for_clip( - &project, - &cursor, - 8.0, - XY::new(1000, 1000), - recording_clip, - ) - } - }; - let mut indexed = build(); - let mut linear = build(); - force_linear_cluster_lookup(&mut linear); - indexed.precompute(); - for time in [1.0, 3.5, 2.0, 8.0] { - linear.ensure_precomputed_until(time); - } - assert_samples_identical(&indexed, &linear); - } - } - } - } - } - - #[test] - #[ignore = "benchmark; run with --release --ignored --nocapture"] - fn zoom_cluster_timeline_rebuild() { - use std::time::{Duration, Instant}; - - const RUNS: usize = 7; - let percentile = |times: &mut Vec, p: f64| { - times.sort_unstable(); - let index = ((times.len() - 1) as f64 * p).round() as usize; - times[index].as_secs_f64() * 1000.0 - }; - let cluster_count = |timeline: &ZoomTransformTimeline| { - timeline - .clusters - .iter() - .flatten() - .map(|clusters| match clusters { - ZoomClusters::Static(centers) => centers.clusters.len(), - ZoomClusters::Styled { variants, .. } => variants - .iter() - .map(|variant| variant.clusters.clusters.len()) - .sum(), - }) - .sum::() - }; - println!( - "| seconds | zoom | playhead | moves | clusters | samples | build p50 ms | precompute p50 ms | rebuild p50 ms | rebuild min ms | rebuild max ms | samples hash |" - ); - println!( - "| ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |" - ); - for duration_secs in [60.0, 600.0] { - let cursor = smooth_cluster_cursor(duration_secs); - for long_zoom in [false, true] { - let mut project = benchmark_project(duration_secs, (duration_secs / 30.0) as usize); - if long_zoom { - project.timeline.as_mut().unwrap().zoom_segments = - vec![auto_segment(0.0, duration_secs, 2.0)]; - } - for fraction in [0.5, 1.0] { - let playhead = (duration_secs * fraction) as f32; - let mut build_times = Vec::with_capacity(RUNS); - let mut precompute_times = Vec::with_capacity(RUNS); - let mut rebuild_times = Vec::with_capacity(RUNS); - let mut expected_hash = None; - let mut clusters = 0; - let mut samples = 0; - let mut reference = ZoomTransformTimeline::from_project_for_clip( - &project, - &cursor, - duration_secs, - XY::new(1920, 1080), - 0, - ); - force_linear_cluster_lookup(&mut reference); - reference.ensure_precomputed_until(playhead); - for run in 0..=RUNS { - let build_start = Instant::now(); - let mut timeline = ZoomTransformTimeline::from_project_for_clip( - &project, - &cursor, - duration_secs, - XY::new(1920, 1080), - 0, - ); - let build_elapsed = build_start.elapsed(); - let precompute_start = Instant::now(); - timeline.ensure_precomputed_until(playhead); - let precompute_elapsed = precompute_start.elapsed(); - let rebuild_elapsed = build_start.elapsed(); - assert_samples_identical(&timeline, &reference); - let hash = samples_hash(&timeline); - if let Some(expected_hash) = expected_hash { - assert_eq!(hash, expected_hash); - } else { - expected_hash = Some(hash); - } - clusters = cluster_count(&timeline); - samples = timeline.samples.len(); - assert!(samples > (f64::from(playhead) * 1000.0 / STEP_MS) as usize); - std::hint::black_box(timeline.sample(playhead)); - if run > 0 { - build_times.push(build_elapsed); - precompute_times.push(precompute_elapsed); - rebuild_times.push(rebuild_elapsed); - } - } - let rebuild_p50 = percentile(&mut rebuild_times, 0.5); - println!( - "| {duration_secs:.0} | {} | {:.0}% | {} | {clusters} | {samples} | {:.3} | {:.3} | {rebuild_p50:.3} | {:.3} | {:.3} | {:016x} |", - if long_zoom { "long" } else { "4s every 30s" }, - fraction * 100.0, - cursor.moves.len(), - percentile(&mut build_times, 0.5), - percentile(&mut precompute_times, 0.5), - rebuild_times[0].as_secs_f64() * 1000.0, - rebuild_times[RUNS - 1].as_secs_f64() * 1000.0, - expected_hash.unwrap(), - ); - } - } - } - } } diff --git a/scripts/benchmark-zoom-clusters.py b/scripts/benchmark-zoom-clusters.py deleted file mode 100644 index c717c1a3b72..00000000000 --- a/scripts/benchmark-zoom-clusters.py +++ /dev/null @@ -1,133 +0,0 @@ -import argparse -import json -import math -import subprocess -from pathlib import Path - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("target", type=Path) - parser.add_argument("--duration", type=int, default=120) - parser.add_argument("--width", type=int, default=320) - parser.add_argument("--height", type=int, default=180) - parser.add_argument("--short-zooms", action="store_true") - args = parser.parse_args() - if args.duration <= 0: - parser.error("duration must be positive") - if args.width <= 0 or args.height <= 0 or args.width % 2 or args.height % 2: - parser.error("width and height must be positive even integers") - target = args.target.resolve() - if target.exists(): - parser.error("target must not exist") - target.mkdir(parents=True) - - subprocess.run( - [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-nostdin", - "-f", - "lavfi", - "-i", - f"testsrc2=size={args.width}x{args.height}:rate=30", - "-t", - str(args.duration), - "-an", - "-c:v", - "libx264", - "-preset", - "ultrafast", - "-crf", - "35", - "-g", - "30", - "-keyint_min", - "30", - "-sc_threshold", - "0", - "-pix_fmt", - "yuv420p", - "-movflags", - "+faststart", - str(target / "display.mp4"), - ], - check=True, - ) - - moves = [] - for index in range(args.duration * 60): - time = index / 60 - phase = (time % 3.2) / 3.2 - triangle = 1 - abs(2 * phase - 1) - moves.append( - { - "active_modifiers": [], - "cursor_id": "default", - "time_ms": time * 1000, - "x": 0.1 + 0.8 * triangle, - "y": 0.5 + 0.2 * math.sin(time * math.tau / 11), - } - ) - (target / "cursor.json").write_text(json.dumps({"moves": moves, "clicks": []})) - metadata = { - "pretty_name": f"Synthetic smooth cursor zoom ({args.duration}s)", - "platform": "MacOS", - "segments": [ - { - "display": {"path": "display.mp4", "fps": 30}, - "cursor": "cursor.json", - } - ], - } - project = { - "camera": {"hide": True}, - "audio": {"mute": True}, - "cursor": {"type": "circle", "useSvg": False, "raw": False}, - "screenMotionBlur": 1.0, - "textSizeVersion": 1, - "textAnimVersion": 1, - "timeline": { - "segments": [ - { - "recordingSegment": 0, - "timescale": 1.0, - "start": 0.0, - "end": args.duration, - } - ], - "zoomSegments": [ - {"start": 0.0, "end": args.duration, "amount": 2.0, "mode": "auto"} - ], - }, - } - if args.short_zooms: - project["timeline"]["zoomSegments"] = [ - { - "start": start, - "end": min(start + 4, args.duration), - "amount": 2, - "mode": "auto", - } - for start in range(8, args.duration, 30) - ] - (target / "recording-meta.json").write_text(json.dumps(metadata, indent=2) + "\n") - (target / "project-config.json").write_text(json.dumps(project, indent=2) + "\n") - - print( - json.dumps( - { - "project": str(target), - "durationSecs": args.duration, - "moves": len(moves), - "videoSize": [args.width, args.height], - "shortZooms": args.short_zooms, - } - ) - ) - - -if __name__ == "__main__": - main() From 5aa476a8fb914deff0d4ddabe5d9d3d16abd56f3 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:43:35 +0900 Subject: [PATCH 3/4] test(rendering): use the indexed cluster lookup in the idle-cursor auto zoom test --- crates/rendering/src/zoom_spring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rendering/src/zoom_spring.rs b/crates/rendering/src/zoom_spring.rs index 57a93c2e815..897014084dc 100644 --- a/crates/rendering/src/zoom_spring.rs +++ b/crates/rendering/src/zoom_spring.rs @@ -1871,7 +1871,7 @@ mod tests { }; for ordered in [false, true] { let clusters = build_clusters(&cursor, ordered, 2.0, 10.0, 2.0, None); - assert_eq!(cluster_center_at_time(&clusters, 2000.0), Some((0.1, 0.1))); + assert_eq!(ClusterCenters::new(clusters).center_at(2000.0), Some((0.1, 0.1))); } } From 148798120eb6f7bfc146f829ae5a10f1657f042b Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:47:00 +0800 Subject: [PATCH 4/4] style: cargo fmt --- crates/rendering/src/zoom_spring.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/rendering/src/zoom_spring.rs b/crates/rendering/src/zoom_spring.rs index a95740009be..a8ddaab2ca0 100644 --- a/crates/rendering/src/zoom_spring.rs +++ b/crates/rendering/src/zoom_spring.rs @@ -1873,7 +1873,10 @@ mod tests { }; for ordered in [false, true] { let clusters = build_clusters(&cursor, ordered, 2.0, 10.0, 2.0, None); - assert_eq!(ClusterCenters::new(clusters).center_at(2000.0), Some((0.1, 0.1))); + assert_eq!( + ClusterCenters::new(clusters).center_at(2000.0), + Some((0.1, 0.1)) + ); } }