Skip to content

perf(animator): cache the spring settle time - #226

Merged
LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
perf/spring-settle-cache
Sep 22, 2026
Merged

LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
perf/spring-settle-cache

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Low, category quality. Location: crates/rustmotion-core/src/engine/animator.rs:470

Impact

spring_value is reached per animated property, per node, per frame (box_builder.rs:538 -> resolve_props_for_effects -> resolve_animation_value_full, animator.rs:997). When SpringConfig::duration is set, each of those calls runs a full coarse scan plus 40 bisection steps, with steps clamped to [2000, 20000]. For the shipped defaults (stiffness 100, mass 1 -> omega 10, period 0.628 s) desired_steps is 2292, so one animated property costs ~2333 exp/sin/cos evaluations per frame to recompute a value that depends only on (damping, stiffness, mass, rest_threshold) — completely invariant across frames. A 30 s/60 fps render with 20 nodes carrying two spring-duration properties each burns roughly 165 million redundant solver evaluations. The function's own doc comment acknowledges it is "called on every spring_value sample when duration is set".

Fix

Memoize spring_settle_time on the quantized (damping, stiffness, mass, threshold) tuple in a thread-local HashMap, or precompute time_scale once when the SpringConfig is resolved and carry it on the resolved animation instead of recomputing it inside spring_value.

Evidence the audit read

let natural_rest = spring_settle_time(
                damping,
                stiffness,
                mass,
                threshold,
                MAX_SPRING_SEARCH_SECONDS,
            );
---
    let steps = desired_steps.clamp(SPRING_SETTLE_MIN_SAMPLES, SPRING_SETTLE_MAX_SAMPLES);
    let dt = max_t / steps as f64;
    let mut last_exceed_idx: usize = 0;
    for i in 0..=steps {

Stacked on fix/gradient-angle, which carries the previous finding of this workstream. GitHub shows only this finding's diff; merge in order.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-37).

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from 1526e13 to eda0946 Compare September 21, 2026 23:39
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from dfa79d7 to d5f4911 Compare September 22, 2026 06:09
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from d5f4911 to 4cbf315 Compare September 22, 2026 08:33
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from 4cbf315 to 3dc1213 Compare September 22, 2026 08:43
@LeadcodeDev
LeadcodeDev changed the base branch from fix/gradient-angle to chantier/audit-2026-09 September 22, 2026 08:52
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from 3dc1213 to 62bfbd0 Compare September 22, 2026 08:58
`spring_value` is reached per animated property, per node, per frame (`box_builder.rs:538` -> `resolve_props_for_effects` -> `resolve_animation_value_full`, animator.rs:997). When `SpringConfig::duration` is set, each of those calls runs a full coarse scan plus 40 bisection steps, with `steps` clamped to [2000, 20000]. For the shipped defaults (stiffness 100, mass 1 -> omega 10, period 0.628 s) `desired_steps` is 2292, so one animated property costs ~2333 `exp`/`sin`/`cos` evaluations per frame to recompute a value that depends only on (damping, stiffness, mass, rest_threshold) — completely invariant across frames. A 30 s/60 fps render with 20 nodes carrying two spring-duration properties each burns roughly 165 million redundant solver evaluations. The function's own doc comment acknowledges it is "called on every `spring_value` sample when `duration` is set".

Refs #220
@LeadcodeDev
LeadcodeDev force-pushed the perf/spring-settle-cache branch from 62bfbd0 to 99a9496 Compare September 22, 2026 09:04
@LeadcodeDev
LeadcodeDev merged commit 4274f9f into chantier/audit-2026-09 Sep 22, 2026
LeadcodeDev added a commit that referenced this pull request Sep 22, 2026
`spring_value` is reached per animated property, per node, per frame (`box_builder.rs:538` -> `resolve_props_for_effects` -> `resolve_animation_value_full`, animator.rs:997). When `SpringConfig::duration` is set, each of those calls runs a full coarse scan plus 40 bisection steps, with `steps` clamped to [2000, 20000]. For the shipped defaults (stiffness 100, mass 1 -> omega 10, period 0.628 s) `desired_steps` is 2292, so one animated property costs ~2333 `exp`/`sin`/`cos` evaluations per frame to recompute a value that depends only on (damping, stiffness, mass, rest_threshold) — completely invariant across frames. A 30 s/60 fps render with 20 nodes carrying two spring-duration properties each burns roughly 165 million redundant solver evaluations. The function's own doc comment acknowledges it is "called on every `spring_value` sample when `duration` is set".

Refs #220
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant