Stop rebuilding the whole terrain array once per point while scrubbing - #26
Merged
Conversation
`interceptsTerrain(at:)` read `filledTerrainFt`, a computed property that maps every point in the path, and then took a single element out of it. The impact overlay calls it twice per segment, so a path of n points rebuilt an n-element array roughly 2n times to read 2n values — quadratic work to answer a linear question. That was latent until scrubbing landed. `chartXSelection` writes `@State` on every frame of the drag, so the chart body — and this loop — now runs per frame rather than once per data change. Paths are sampled every 0.1 NM and capped at 1000 steps a leg, so a multi-leg procedure lands squarely in the range where this hurts. Measured on a release build, the loop alone: 0.27 ms at 500 points, 1.18 ms at 1000, 7.1 ms at 2000 — against 0.0008 ms once the array is hoisted, which is the difference between a smooth scrub and a stuttering one on a device several times slower than the machine that measured it. The array was already hoisted in `terrainLayer` for the area marks; this passes that same array down instead of re-deriving it. Also clears the selection when the distance unit changes. It is stored in the axis's own unit, so switching to kilometres with the profile still on screen reinterpreted 10 NM as 10 km and moved the rule to a sample nobody scrubbed to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeE5kdGjdXQim98Fi1kVyG
RISCfuture
added a commit
that referenced
this pull request
Sep 6, 2026
#26) `interceptsTerrain(at:)` read `filledTerrainFt`, a computed property that maps every point in the path, and then took a single element out of it. The impact overlay calls it twice per segment, so a path of n points rebuilt an n-element array roughly 2n times to read 2n values — quadratic work to answer a linear question. That was latent until scrubbing landed. `chartXSelection` writes `@State` on every frame of the drag, so the chart body — and this loop — now runs per frame rather than once per data change. Paths are sampled every 0.1 NM and capped at 1000 steps a leg, so a multi-leg procedure lands squarely in the range where this hurts. Measured on a release build, the loop alone: 0.27 ms at 500 points, 1.18 ms at 1000, 7.1 ms at 2000 — against 0.0008 ms once the array is hoisted, which is the difference between a smooth scrub and a stuttering one on a device several times slower than the machine that measured it. The array was already hoisted in `terrainLayer` for the area marks; this passes that same array down instead of re-deriving it. Claude-Session: https://claude.ai/code/session_01XeE5kdGjdXQim98Fi1kVyG Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while reviewing something else. Not a new bug, but scrubbing (#24) made it hot.
The quadratic
interceptsTerrain(at:)readfilledTerrainFt— a computed property that maps every point in the path — and then took one element out of it. The impact overlay calls it twice per segment:So a path of n points rebuilt an n-element array about 2n times, to read 2n values. Quadratic work to answer a linear question.
That was invisible until #24, which added
chartXSelectionwriting@State selectedDistanceon every frame of the drag — so the chart body, and this loop, now runs per frame rather than once per data change.Scale
Paths are sampled every
groundStepNM = 0.1and capped atmaxSteps = 1000per leg, so a multi-leg procedure lands squarely in the range where this bites. Measured on a release build of the same access pattern:That's on this Mac; a phone is several times slower, and this loop is only part of what the body re-does each frame. Against a 16 ms budget it's the difference between a smooth scrub and a stuttering one.
The fix is small:
terrainLayeralready hoists the array for its area marks (let terrain = filledTerrainFt), so this passes that same array intointerceptsTerraininstead of re-deriving it. Behavior is identical.Also
Clears the selection when the distance unit changes.
selectedDistanceis held in the axis's own unit, so switching Settings to kilometres with the profile still on screen reinterpreted a stored10from NM to km and moved the rule to a sample the pilot never scrubbed to.Verified
swift format lint --strict·swiftlint --strict·periphery scan --strict· warning-free build ·xcodebuild docbuild(warnings-as-errors) · theSF50 Shared Unit Testsplan, 388 cases green.No new tests: the change is a parameter pass-through with identical behavior, and the existing suite covers
interceptsTerrain's logic through the generator tests. The performance claim is measured above rather than asserted in a test, since a timing assertion on CI runners would be flaky.Two things I looked at and deliberately did not change
#Preview("No terrain data")pinning it. That reads as deliberate. Worth a second opinion; happy to change it if the intent was only to suppress AGL.🤖 Generated with Claude Code
https://claude.ai/code/session_01XeE5kdGjdXQim98Fi1kVyG