Skip to content

Commit 355a6d6

Browse files
authored
refactor: isolate Telemetry Inspector stream (#671)
1 parent 781f93f commit 355a6d6

63 files changed

Lines changed: 598 additions & 305 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/telemetryDecorator.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
DashboardProvider,
44
RunningStateProvider,
55
SessionProvider,
6-
TelemetryProvider,
6+
StoryTelemetryProvider,
77
} from '@irdashies/context';
88
import { generateMockDataFromPath } from '../src/app/bridge/iracingSdk/mock-data/generateMockData';
99
import { mockDashboardBridge } from './mockDashboardBridge';
@@ -15,7 +15,7 @@ export const TelemetryDecorator: (path?: string) => Decorator = (path) => {
1515
const DecoratorComponent = (Story: ComponentType) => (
1616
<>
1717
<SessionProvider bridge={generateMockDataFromPath(path)} />
18-
<TelemetryProvider bridge={generateMockDataFromPath(path)} />
18+
<StoryTelemetryProvider bridge={generateMockDataFromPath(path)} />
1919
<DashboardProvider bridge={mockDashboardBridge}>
2020
<RunningStateProvider bridge={generateMockDataFromPath(path)}>
2121
<Story />
@@ -71,7 +71,7 @@ export const TelemetryDecoratorWithConfig: (
7171
return (
7272
<>
7373
<SessionProvider bridge={generateMockDataFromPath(path)} />
74-
<TelemetryProvider bridge={generateMockDataFromPath(path)} />
74+
<StoryTelemetryProvider bridge={generateMockDataFromPath(path)} />
7575
<DashboardProvider bridge={bridge}>
7676
<RunningStateProvider bridge={generateMockDataFromPath(path)}>
7777
<Story />

docs/IMPLEMENTATION_PLAN.md

Lines changed: 22 additions & 20 deletions
Large diffs are not rendered by default.

site/src/utils/mockSetup.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type ReactNode, useMemo } from 'react';
22
import {
33
SessionProvider,
4-
TelemetryProvider,
4+
StoryTelemetryProvider,
55
RunningStateProvider,
66
DashboardProvider,
77
} from '@irdashies/context';
@@ -131,7 +131,7 @@ export function LivePreviewProvider({
131131
<DashboardProvider bridge={dashboardBridge}>
132132
<RunningStateProvider bridge={bridge}>
133133
<SessionProvider bridge={bridge} />
134-
<TelemetryProvider bridge={bridge} />
134+
<StoryTelemetryProvider bridge={bridge} />
135135
{children}
136136
</RunningStateProvider>
137137
</DashboardProvider>

src/app/bridge/iracingSdk/iracingSdkBridge.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
getPerfRunConfig,
66
PERF_REPLAY_READY_LOG_MARKER,
77
} from '../../perfRunConfig';
8-
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
8+
import {
9+
TELEMETRY_INSPECTOR_RATE_HZ,
10+
type IrSdkSourceBridge,
11+
type Session,
12+
type Telemetry,
13+
} from '@irdashies/types';
914
import logger from '../../logger';
1015
import type { SessionLifecycle } from '../../sessionLifecycle';
1116
import type { ChannelBus } from '../channelBridge';
@@ -148,7 +153,7 @@ export async function publishIRacingSDKEvents(
148153
overlayManager: OverlayManager,
149154
lifecycle?: SessionLifecycle,
150155
channelBus?: ChannelBus
151-
): Promise<IrSdkBridge> {
156+
): Promise<IrSdkSourceBridge> {
152157
logger.info('[iracingSdkBridge] Loading iRacing SDK bridge...');
153158
const isTapeReplay = Boolean(process.env.IRDASHIES_TELEMETRY_REPLAY);
154159
const sourceName = isTapeReplay ? 'telemetry replay' : 'iRacing';
@@ -238,7 +243,6 @@ export async function publishIRacingSDKEvents(
238243

239244
let shouldStop = false;
240245
let lastRunningState: boolean | undefined = undefined;
241-
let latestTelemetry: Telemetry | null = null;
242246
let latestSession: Session | null = null;
243247

244248
const telemetryCallbacks = new Set<(value: Telemetry) => void>();
@@ -256,12 +260,6 @@ export async function publishIRacingSDKEvents(
256260
'runningState',
257261
lastRunningState
258262
);
259-
if (latestTelemetry && perfTelemetryDeliveryEnabled)
260-
overlayManager.publishMessageToOverlay(
261-
id,
262-
'telemetry',
263-
telemetryForRenderer(latestTelemetry)
264-
);
265263
if (latestSession)
266264
overlayManager.publishMessageToOverlay(id, 'sessionData', latestSession);
267265
});
@@ -302,6 +300,7 @@ export async function publishIRacingSDKEvents(
302300
(async () => {
303301
while (!shouldStop) {
304302
let lastSessionVersion = -1;
303+
let lastInspectorTelemetryPublishTime = Number.NEGATIVE_INFINITY;
305304
// Negative infinity makes the first tick fetch and publish immediately.
306305
let lastSessionPublishTime = Number.NEGATIVE_INFINITY;
307306
let lastSessionPollTime = Number.NEGATIVE_INFINITY;
@@ -327,7 +326,6 @@ export async function publishIRacingSDKEvents(
327326
}
328327

329328
if (telemetry) {
330-
latestTelemetry = telemetry;
331329
perfMetrics.markStart('lifecycleTelemetry');
332330
lifecycle?._onTelemetry(telemetry);
333331
perfMetrics.markEnd('lifecycleTelemetry');
@@ -346,13 +344,19 @@ export async function publishIRacingSDKEvents(
346344
lapLogRuntime?.onFrame(telemetry);
347345
if (
348346
perfTelemetryDeliveryEnabled &&
349-
overlayManager.hasLegacyStreamSubscribers('telemetry')
347+
overlayManager.hasTelemetryInspectorSubscribers() &&
348+
tickTime - lastInspectorTelemetryPublishTime >=
349+
1000 / TELEMETRY_INSPECTOR_RATE_HZ
350350
) {
351+
lastInspectorTelemetryPublishTime = tickTime;
351352
perfMetrics.markStart('telemetryProjection');
352353
const rendererTelemetry = telemetryForRenderer(telemetry);
353354
perfMetrics.markEnd('telemetryProjection');
354355
perfMetrics.markStart('broadcast');
355-
overlayManager.publishMessage('telemetry', rendererTelemetry);
356+
overlayManager.publishMessage(
357+
'telemetryInspector:telemetry',
358+
rendererTelemetry
359+
);
356360
perfMetrics.markEnd('broadcast');
357361
}
358362
perfMetrics.markStart('telemetryCallbacks');
@@ -404,8 +408,8 @@ export async function publishIRacingSDKEvents(
404408
// opened during a disconnect don't get re-seeded with stale data, and
405409
// so the references don't sit in main-process memory indefinitely.
406410
// They get repopulated on the next successful waitForData tick.
407-
latestTelemetry = null;
408411
latestSession = null;
412+
overlayManager.clearLatestSessionData?.();
409413
lifecycle?._onDisconnect();
410414
}
411415

@@ -434,6 +438,7 @@ export async function publishIRacingSDKEvents(
434438
},
435439
stop: () => {
436440
shouldStop = true;
441+
overlayManager.clearLatestSessionData?.();
437442
sdk.stopSDK();
438443
clearInterval(runningStateInterval);
439444
telemetryCallbacks.clear();

src/app/bridge/iracingSdk/mock-data/generateMockData.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
1+
import type { IrSdkSourceBridge, Session, Telemetry } from '@irdashies/types';
22
import mockSessionInfo from '../../../irsdk/node/utils/mock-data/session.json';
33
import mockTelemetry from '../../../irsdk/node/utils/mock-data/telemetry.json';
44

55
export async function generateMockDataFromPath(
66
path?: string
7-
): Promise<IrSdkBridge> {
7+
): Promise<IrSdkSourceBridge> {
88
if (!path) {
99
return generateMockData();
1010
}
@@ -23,7 +23,7 @@ export async function generateMockDataFromPath(
2323
export function generateMockData(sessionData?: {
2424
telemetry: Telemetry | Telemetry[];
2525
sessionInfo: Session | Session[];
26-
}): IrSdkBridge {
26+
}): IrSdkSourceBridge {
2727
let telemetryInterval: NodeJS.Timeout | null = null;
2828
let sessionInfoInterval: NodeJS.Timeout | null = null;
2929
let runningStateInterval: NodeJS.Timeout | null = null;

src/app/bridge/iracingSdk/mock-data/mockSdkBridge.spec.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
2-
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { IrSdkSourceBridge, Session, Telemetry } from '@irdashies/types';
33
import { ChannelBus } from '../../channelBridge';
44

55
const callbacks = vi.hoisted(() => ({
@@ -9,7 +9,7 @@ const callbacks = vi.hoisted(() => ({
99
const stop = vi.hoisted(() => vi.fn());
1010

1111
vi.mock('./generateMockData', () => ({
12-
generateMockData: (): IrSdkBridge => ({
12+
generateMockData: (): IrSdkSourceBridge => ({
1313
onTelemetry(callback) {
1414
callbacks.telemetry = callback;
1515
return () => undefined;
@@ -51,6 +51,36 @@ describe('mockSdkBridge processor channels', () => {
5151
stop.mockReset();
5252
});
5353

54+
afterEach(() => vi.restoreAllMocks());
55+
56+
it('publishes subscribed Inspector telemetry at no more than 10 Hz', async () => {
57+
const publishMessage = vi.fn();
58+
const now = vi
59+
.spyOn(performance, 'now')
60+
.mockReturnValueOnce(0)
61+
.mockReturnValueOnce(50)
62+
.mockReturnValueOnce(100);
63+
const bridge = await publishIRacingSDKEvents({
64+
publishMessage,
65+
hasTelemetryInspectorSubscribers: () => true,
66+
} as never);
67+
68+
try {
69+
callbacks.telemetry?.(telemetry(0.1, 1));
70+
callbacks.telemetry?.(telemetry(0.11, 1.05));
71+
callbacks.telemetry?.(telemetry(0.12, 1.1));
72+
expect(now).toHaveBeenCalledTimes(3);
73+
expect(publishMessage).toHaveBeenCalledTimes(2);
74+
expect(publishMessage).toHaveBeenNthCalledWith(
75+
1,
76+
'telemetryInspector:telemetry',
77+
expect.anything()
78+
);
79+
} finally {
80+
bridge.stop();
81+
}
82+
});
83+
5484
it('feeds mock session and telemetry through the car-speed runtime', async () => {
5585
const bus = new ChannelBus();
5686
const publish = vi.spyOn(bus, 'publish');
@@ -61,7 +91,10 @@ describe('mockSdkBridge processor channels', () => {
6191
send: vi.fn(),
6292
};
6393
bus.subscribe(target, 'car-speeds.snapshot');
64-
const overlayManager = { publishMessage: vi.fn() };
94+
const overlayManager = {
95+
publishMessage: vi.fn(),
96+
hasTelemetryInspectorSubscribers: () => false,
97+
};
6598
const bridge = await publishIRacingSDKEvents(
6699
overlayManager as never,
67100
undefined,
@@ -79,6 +112,10 @@ describe('mockSdkBridge processor channels', () => {
79112
'car-speeds.snapshot',
80113
expect.objectContaining({ carSpeeds: [360] })
81114
);
115+
expect(overlayManager.publishMessage).not.toHaveBeenCalledWith(
116+
'telemetryInspector:telemetry',
117+
expect.anything()
118+
);
82119
} finally {
83120
bridge.stop();
84121
}
@@ -101,7 +138,10 @@ describe('mockSdkBridge processor channels', () => {
101138
'session-timing.snapshot'
102139
);
103140
const bridge = await publishIRacingSDKEvents(
104-
{ publishMessage: vi.fn() } as never,
141+
{
142+
publishMessage: vi.fn(),
143+
hasTelemetryInspectorSubscribers: () => false,
144+
} as never,
105145
undefined,
106146
bus
107147
);
@@ -148,7 +188,10 @@ describe('mockSdkBridge processor channels', () => {
148188
'relative-gaps.snapshot'
149189
);
150190
const bridge = await publishIRacingSDKEvents(
151-
{ publishMessage: vi.fn() } as never,
191+
{
192+
publishMessage: vi.fn(),
193+
hasTelemetryInspectorSubscribers: () => false,
194+
} as never,
152195
undefined,
153196
bus
154197
);

src/app/bridge/iracingSdk/mock-data/mockSdkBridge.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
1515
import { DriverControlsRuntime } from '../../../processors/driverControlsRuntime';
1616
import { TrackStateRuntime } from '../../../processors/trackStateRuntime';
1717
import { LapLogRuntime } from '../../../processors/lapLogRuntime';
18+
import { TELEMETRY_INSPECTOR_RATE_HZ } from '@irdashies/types';
1819

1920
export async function publishIRacingSDKEvents(
2021
overlayManager: OverlayManager,
2122
lifecycle?: SessionLifecycle,
2223
channelBus?: ChannelBus
2324
) {
2425
const perfMetrics = new TelemetryPerfMetrics();
26+
let lastInspectorTelemetryPublishTime = Number.NEGATIVE_INFINITY;
2527
perfMetrics.startReporting();
2628

2729
const bridge = generateMockData();
@@ -105,9 +107,17 @@ export async function publishIRacingSDKEvents(
105107
driverControlsRuntime?.onFrame(telemetry);
106108
trackStateRuntime?.onFrame(telemetry);
107109
lapLogRuntime?.onFrame(telemetry);
108-
perfMetrics.markStart('broadcast');
109-
overlayManager.publishMessage('telemetry', telemetry);
110-
perfMetrics.markEnd('broadcast');
110+
const tickTime = performance.now();
111+
if (
112+
overlayManager.hasTelemetryInspectorSubscribers() &&
113+
tickTime - lastInspectorTelemetryPublishTime >=
114+
1000 / TELEMETRY_INSPECTOR_RATE_HZ
115+
) {
116+
lastInspectorTelemetryPublishTime = tickTime;
117+
perfMetrics.markStart('broadcast');
118+
overlayManager.publishMessage('telemetryInspector:telemetry', telemetry);
119+
perfMetrics.markEnd('broadcast');
120+
}
111121
perfMetrics.markEnd('processTelemetry');
112122
perfMetrics.tick(telemetry);
113123
});
@@ -120,6 +130,7 @@ export async function publishIRacingSDKEvents(
120130
return {
121131
...bridge,
122132
stop: () => {
133+
overlayManager.clearLatestSessionData?.();
123134
carSpeedsRuntime?.dispose();
124135
lapTimesRuntime?.dispose();
125136
relativeGapRuntime?.dispose();

src/app/bridge/iracingSdk/setup.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OverlayManager } from '../../overlayManager';
22
import { ipcMain } from 'electron';
3-
import type { IrSdkBridge } from '@irdashies/types';
3+
import type { IrSdkSourceBridge } from '@irdashies/types';
44
import logger from '../../logger';
55
import {
66
createSessionLifecycle,
@@ -9,8 +9,8 @@ import {
99
import type { ChannelBus } from '../channelBridge';
1010

1111
let isDemoMode = false;
12-
let currentBridge: IrSdkBridge | undefined;
13-
const onBridgeChangedCallbacks = new Set<(bridge: IrSdkBridge) => void>();
12+
let currentBridge: IrSdkSourceBridge | undefined;
13+
const onBridgeChangedCallbacks = new Set<(bridge: IrSdkSourceBridge) => void>();
1414

1515
// Singleton lifecycle — created once; survives bridge restarts so subscribers
1616
// registered before a demo-mode toggle are preserved.
@@ -23,15 +23,15 @@ export function getSessionLifecycle(): SessionLifecycle {
2323
return sessionLifecycle;
2424
}
2525

26-
export function getCurrentBridge(): IrSdkBridge | undefined {
26+
export function getCurrentBridge(): IrSdkSourceBridge | undefined {
2727
return currentBridge;
2828
}
2929

3030
export function getIsDemoMode(): boolean {
3131
return isDemoMode;
3232
}
3333

34-
export function onBridgeChanged(callback: (bridge: IrSdkBridge) => void) {
34+
export function onBridgeChanged(callback: (bridge: IrSdkSourceBridge) => void) {
3535
onBridgeChangedCallbacks.add(callback);
3636
return () => onBridgeChangedCallbacks.delete(callback);
3737
}

src/app/bridge/legacyRendererSubscriptions.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineRendererSubscriptionBridge } from './defineBridge';
2+
3+
export const RENDERER_DATA_SUBSCRIPTION_BRIDGE = 'renderer-data';
4+
5+
export type RendererDataStream = 'sessionData' | 'telemetryInspector';
6+
7+
export const isRendererDataStream = (
8+
value: unknown
9+
): value is RendererDataStream =>
10+
value === 'sessionData' || value === 'telemetryInspector';
11+
12+
export const setupRendererDataSubscriptions = () =>
13+
defineRendererSubscriptionBridge<RendererDataStream>({
14+
name: RENDERER_DATA_SUBSCRIPTION_BRIDGE,
15+
isValidKey: isRendererDataStream,
16+
});

0 commit comments

Comments
 (0)