Skip to content

[perf] Add throttled telemetry values hook and prevent driver standings memo from recalculation on each re-render - #603

Draft
pohy wants to merge 3 commits into
tariknz:mainfrom
pohy:perf
Draft

[perf] Add throttled telemetry values hook and prevent driver standings memo from recalculation on each re-render#603
pohy wants to merge 3 commits into
tariknz:mainfrom
pohy:perf

Conversation

@pohy

@pohy pohy commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Plus add benchmark testing, not sure about integration into CI, tho.

Screenshots

Before

After

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Performance improvement
  • Refactoring (no functional changes)
  • Documentation update
  • Dependency update

Checklist

  • I have discussed this change in the discord server
  • I have tested this in iRacing (either in an online session or with AI)
  • All tests pass locally via npm test
  • I have added tests that prove my fix is effective or that my feature works
  • I have run npm run lint and fixed any issues
  • I have performed a self-review of my own code
  • I have added/updated Storybook stories for visual changes
  • I have updated the README.md (if applicable)
  • I have updated defaultDashboard.ts if introducing new widgets or configurations (if applicable)

Summary by CodeRabbit

Release Notes

  • Tests

    • Added benchmarks measuring telemetry subscription performance
    • Added performance specifications for standings display updates
  • Chores

    • CI pipeline now executes benchmarks after tests
    • Added npm bench script for local performance testing
  • Documentation

    • Added guidance on benchmarking and performance testing best practices

pohy and others added 3 commits June 22, 2026 01:56
CarIdxLapDistPct rounded to 3-4dp still recomputed Standings ~every
60Hz tick — with 24 cars on track, some car crosses any rounding
threshold almost every frame, so value-based rounding never throttled
the cadence. Added useTelemetryValuesThrottled (time-gated sampling,
event-driven via setTimeout, no idle polling) and switched the
Standings/Relative hot paths to it. Also lifted CarIdxLapDistPct/
CarIdxTrackSurface subscriptions to the top of each hook tree instead
of each child hook (useDriverPositions, useCarState,
useDriverLivePositions) resubscribing independently.

Added a deterministic render-count regression test and a vitest bench
comparing wall-clock cost: throttled is 75% fewer renders and ~1.5x
faster under a simulated 24-car race.
Document the perf.spec.ts/bench.ts split established for the Standings
throttle fix so future perf claims get a render-count test or a wall-clock
bench instead of Task Manager eyeballing. CI runs npm run bench after tests,
non-blocking since wall-clock numbers are noisy on shared runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every call site passed the same 66ms, so make it the default and drop the
redundant argument.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces useTelemetryValuesThrottled, a new hook in TelemetryStore that samples telemetry arrays on a time interval (default 66ms) using a leading-edge timeout and arrayCompare for reference stability. Standings hooks (useDriverPositions, useCarState, useDriverLivePositions, useDriverRelatives, useDriverStandings) are refactored to use it with shared subscriptions via optional override parameters. Benchmarking infrastructure is added: a perf spec, a Vitest bench file, an npm bench script, a CI step, and AGENTS.md documentation.

Changes

Throttled Telemetry for Standings

Layer / File(s) Summary
useTelemetryValuesThrottled hook implementation and tests
src/frontend/context/TelemetryStore/TelemetryStore.tsx, src/frontend/context/TelemetryStore/TelemetryStore.spec.tsx
Exports useTelemetryValuesThrottled(key, intervalMs=66) using useState/useEffect with a single timeout-based flush per interval and arrayCompare for reference stability; tests verify throttling and stable array references under fake timers.
Standings hooks: shared subscriptions and override params
src/frontend/components/Standings/hooks/useDriverPositions.tsx, src/frontend/components/Standings/hooks/useDriverStandings.tsx, src/frontend/components/Standings/hooks/useDriverLivePositions.tsx, src/frontend/components/Standings/hooks/useDriverRelatives.tsx, src/frontend/components/Standings/hooks/useDriverRelatives.spec.tsx
useDriverStandings now calls useTelemetryValuesThrottled once for CarIdxLapDistPct and useTelemetry once for CarIdxTrackSurface, then passes results as override parameters to useDriverPositions, useCarState, and useDriverLivePositions; useDriverRelatives similarly switches to the throttled hook; test mocks are extended to include useTelemetryValuesThrottled.
Benchmark and perf-spec infrastructure
src/frontend/context/TelemetryStore/standingsThrottle.bench.ts, src/frontend/context/TelemetryStore/standingsThrottle.perf.spec.ts, package.json, .github/workflows/ci.yml, AGENTS.md
Adds a Vitest bench file comparing rounded vs. throttled subscription render cost, a CI-gated perf spec asserting throttling produces fewer renders than rounding over a 6-second 60Hz simulation, a bench npm script, a CI step with continue-on-error: true, and AGENTS.md documentation for *.perf.spec.ts vs *.bench.ts conventions.

Sequence Diagram(s)

sequenceDiagram
  participant TelemetryFeed as Telemetry Feed (60Hz)
  participant Store as useTelemetryStore (Zustand)
  participant Throttle as useTelemetryValuesThrottled
  participant Standings as useDriverStandings
  participant ChildHooks as useDriverPositions / useCarState / useDriverLivePositions

  TelemetryFeed->>Store: update CarIdxLapDistPct + CarIdxTrackSurface each tick
  Store-->>Throttle: notify subscriber
  Throttle->>Throttle: schedule timeout if none pending (intervalMs=66)
  Note over Throttle: intermediate ticks ignored
  Throttle->>Store: read latest on timeout fire
  Throttle-->>Standings: updated carIdxLapDistPct array
  Standings->>ChildHooks: pass carIdxLapDistPct + carIdxTrackSurface as overrides
  ChildHooks-->>Standings: computed positions / car state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • tariknz/irdashies#567: Modifies useDriverLivePositions call sites (hoisting to TrackMap and passing via props), directly intersecting with this PR's changes to that hook's signature and telemetry subscription behavior.

Suggested reviewers

  • tariknz

🐇 A throttle per frame, not a rounding per change,
The standings now sample at time's gentle range.
One subscription shared, no duplicates galore,
The renders grow fewer — who could ask for more?
bench runs on CI with nary a fail,
The rabbit hops faster on this smoother trail! 🏎️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. While the author marked 'Performance improvement' and completed multiple checklist items, the description section itself is minimal ('Plus add benchmark testing, not sure about integration into CI, tho.') and lacks meaningful detail about what was changed and why. Expand the description to explain the performance problem being solved, the solution approach (throttled telemetry hook), and key changes to the codebase, even if briefly.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main performance improvements: introducing a throttled telemetry hook and optimizing driver standings calculations to reduce re-renders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/components/Standings/hooks/useDriverLivePositions.tsx`:
- Around line 39-46: The useDriverLivePositions hook opens subscriptions for
carIdxLapDistPct and carIdxTrackSurface on lines 84 and 88 regardless of whether
override arrays are supplied, defeating the purpose of the optional parameters.
Modify the subscription logic to conditionally subscribe only when the
corresponding override is not provided, so that when a caller passes
carIdxLapDistPct or carIdxTrackSurface overrides, those local subscriptions are
skipped and the caller's already-subscribed arrays are used instead.

In `@src/frontend/components/Standings/hooks/useDriverPositions.tsx`:
- Around line 45-49: The useDriverPositions hook has unconditional telemetry
subscriptions that create duplicate listeners even when the carIdxLapDstPct
override is provided. To fix this, wrap the store subscriptions at the locations
mentioned in the comment (around lines 63 and 129) with conditional checks that
only execute these subscriptions if the corresponding override parameter
(carIdxLapDstPct) was not provided. When the override is provided, use that
value directly instead of subscribing, thereby eliminating the duplicate
listener issue.

In `@src/frontend/context/TelemetryStore/standingsThrottle.perf.spec.ts`:
- Around line 91-95: Replace the `console.log` call in the performance test with
`logger.info()` to align with established test patterns. Import the logger
utility from `src/frontend/utils/logger.ts` at the top of the
standingsThrottle.perf.spec.ts file, then replace the console.log statement with
logger.info() using the same message content, and remove the
eslint-disable-next-line comment since it will no longer be needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f0aa3b0c-254d-496f-851d-c9f2714f6083

📥 Commits

Reviewing files that changed from the base of the PR and between 5b363fa and e9428ae.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • AGENTS.md
  • package.json
  • src/frontend/components/Standings/hooks/useDriverLivePositions.tsx
  • src/frontend/components/Standings/hooks/useDriverPositions.tsx
  • src/frontend/components/Standings/hooks/useDriverRelatives.spec.tsx
  • src/frontend/components/Standings/hooks/useDriverRelatives.tsx
  • src/frontend/components/Standings/hooks/useDriverStandings.tsx
  • src/frontend/context/TelemetryStore/TelemetryStore.spec.tsx
  • src/frontend/context/TelemetryStore/TelemetryStore.tsx
  • src/frontend/context/TelemetryStore/standingsThrottle.bench.ts
  • src/frontend/context/TelemetryStore/standingsThrottle.perf.spec.ts

Comment on lines +39 to +46
carIdxLapDistPct: carIdxLapDistPctOverride,
carIdxTrackSurface: carIdxTrackSurfaceOverride,
}: {
enabled: boolean;
/** Pass an already-subscribed array to avoid a duplicate store subscription
* when the caller already holds this telemetry key. */
carIdxLapDistPct?: number[];
carIdxTrackSurface?: number[];

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

useDriverLivePositions still subscribes even when override arrays are supplied.

Line [84] and Line [88] always open local subscriptions, so the new override inputs do not actually eliminate duplicate listeners.

Also applies to: 84-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/Standings/hooks/useDriverLivePositions.tsx` around
lines 39 - 46, The useDriverLivePositions hook opens subscriptions for
carIdxLapDistPct and carIdxTrackSurface on lines 84 and 88 regardless of whether
override arrays are supplied, defeating the purpose of the optional parameters.
Modify the subscription logic to conditionally subscribe only when the
corresponding override is not provided, so that when a caller passes
carIdxLapDistPct or carIdxTrackSurface overrides, those local subscriptions are
skipped and the caller's already-subscribed arrays are used instead.

Comment on lines +45 to +49
export const useDriverPositions = (overrides?: {
/** Pass an already-subscribed array to avoid a duplicate store subscription
* when the caller already holds this telemetry key. */
carIdxLapDstPct?: number[];
}) => {

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Override paths still create duplicate telemetry subscriptions.

Line [63] and Line [129] subscribe unconditionally, so passing overrides does not actually remove the duplicate listeners that these comments describe.

Refactor pattern to make deduplication real
- export const useDriverPositions = (overrides?: { carIdxLapDstPct?: number[] }) => {
-   const ownCarIdxLapDstPct = useTelemetryValuesThrottled('CarIdxLapDistPct');
-   const carIdxLapDstPct = overrides?.carIdxLapDstPct ?? ownCarIdxLapDstPct;
+ const useDriverPositionsBase = (carIdxLapDstPct: number[]) => {
    // existing body
- }
+ };
+
+ export const useDriverPositions = () => {
+   const carIdxLapDstPct = useTelemetryValuesThrottled('CarIdxLapDistPct');
+   return useDriverPositionsBase(carIdxLapDstPct);
+ };
+
+ export const useDriverPositionsWithOverrides = (carIdxLapDstPct: number[]) =>
+   useDriverPositionsBase(carIdxLapDstPct);
- export const useCarState = (overrides?: { carIdxTrackSurface?: ReturnType<typeof useTelemetry<number[]>> }) => {
-   const ownCarIdxTrackSurface = useTelemetry('CarIdxTrackSurface');
-   const carIdxTrackSurface = overrides?.carIdxTrackSurface ?? ownCarIdxTrackSurface;
+ const useCarStateBase = (
+   carIdxTrackSurface: ReturnType<typeof useTelemetry<number[]>>
+ ) => {
    // existing body
- }
+ };

Also applies to: 60-64, 124-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/Standings/hooks/useDriverPositions.tsx` around lines
45 - 49, The useDriverPositions hook has unconditional telemetry subscriptions
that create duplicate listeners even when the carIdxLapDstPct override is
provided. To fix this, wrap the store subscriptions at the locations mentioned
in the comment (around lines 63 and 129) with conditional checks that only
execute these subscriptions if the corresponding override parameter
(carIdxLapDstPct) was not provided. When the override is provided, use that
value directly instead of subscribing, thereby eliminating the duplicate
listener issue.

Comment on lines +91 to +95
// eslint-disable-next-line no-console
console.log(
`[perf] ${TICK_COUNT} ticks @ 60Hz, ${CAR_COUNT} cars: ` +
`rounded(3dp)=${roundedTickRenders} renders, throttled(66ms)=${throttledTickRenders} renders`
);

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.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for test logger utilities or patterns

# Check for logger exports from utils
rg -n 'export.*logger' --type=ts src/frontend/utils/

# Check how other test files handle logging
rg -n 'console\.(log|warn|error)' --type=ts -g '*.spec.ts' -C2

Repository: tariknz/irdashies

Length of output: 762


🏁 Script executed:

cat -n src/frontend/utils/logger.ts | head -50

Repository: tariknz/irdashies

Length of output: 1341


🏁 Script executed:

rg -l '\.spec\.ts' src/frontend --type=ts | head -10 | xargs -I {} sh -c 'echo "File: {}" && rg "logger|console\.(log|warn|error)" {} -A1'

Repository: tariknz/irdashies

Length of output: 132


🏁 Script executed:

rg 'import.*logger' --type=ts -g '*.spec.ts'

Repository: tariknz/irdashies

Length of output: 235


🏁 Script executed:

head -40 src/frontend/context/TelemetryStore/standingsThrottle.perf.spec.ts

Repository: tariknz/irdashies

Length of output: 1758


Use the frontend logger instead of console.log to align with established test patterns.

The logger utility at src/frontend/utils/logger.ts explicitly supports test environments by falling back to console when window.logBridge is unavailable—no IPC overhead in tests. Replace console.log with logger.info() and remove the eslint-disable comment. This pattern is already established in src/frontend/context/ReferenceLapStore/ReferenceLapStore.bench.spec.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/context/TelemetryStore/standingsThrottle.perf.spec.ts` around
lines 91 - 95, Replace the `console.log` call in the performance test with
`logger.info()` to align with established test patterns. Import the logger
utility from `src/frontend/utils/logger.ts` at the top of the
standingsThrottle.perf.spec.ts file, then replace the console.log statement with
logger.info() using the same message content, and remove the
eslint-disable-next-line comment since it will no longer be needed.

Source: Coding guidelines

@pohy
pohy marked this pull request as draft June 22, 2026 22:25
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