Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .storybook/telemetryDecorator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
DashboardProvider,
RunningStateProvider,
SessionProvider,
TelemetryProvider,
StoryTelemetryProvider,
} from '@irdashies/context';
import { generateMockDataFromPath } from '../src/app/bridge/iracingSdk/mock-data/generateMockData';
import { mockDashboardBridge } from './mockDashboardBridge';
Expand All @@ -15,7 +15,7 @@ export const TelemetryDecorator: (path?: string) => Decorator = (path) => {
const DecoratorComponent = (Story: ComponentType) => (
<>
<SessionProvider bridge={generateMockDataFromPath(path)} />
<TelemetryProvider bridge={generateMockDataFromPath(path)} />
<StoryTelemetryProvider bridge={generateMockDataFromPath(path)} />
<DashboardProvider bridge={mockDashboardBridge}>
<RunningStateProvider bridge={generateMockDataFromPath(path)}>
<Story />
Expand Down Expand Up @@ -71,7 +71,7 @@ export const TelemetryDecoratorWithConfig: (
return (
<>
<SessionProvider bridge={generateMockDataFromPath(path)} />
<TelemetryProvider bridge={generateMockDataFromPath(path)} />
<StoryTelemetryProvider bridge={generateMockDataFromPath(path)} />
<DashboardProvider bridge={bridge}>
<RunningStateProvider bridge={generateMockDataFromPath(path)}>
<Story />
Expand Down
42 changes: 22 additions & 20 deletions docs/IMPLEMENTATION_PLAN.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions site/src/utils/mockSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ReactNode, useMemo } from 'react';
import {
SessionProvider,
TelemetryProvider,
StoryTelemetryProvider,
RunningStateProvider,
DashboardProvider,
} from '@irdashies/context';
Expand Down Expand Up @@ -131,7 +131,7 @@ export function LivePreviewProvider({
<DashboardProvider bridge={dashboardBridge}>
<RunningStateProvider bridge={bridge}>
<SessionProvider bridge={bridge} />
<TelemetryProvider bridge={bridge} />
<StoryTelemetryProvider bridge={bridge} />
{children}
</RunningStateProvider>
</DashboardProvider>
Expand Down
31 changes: 18 additions & 13 deletions src/app/bridge/iracingSdk/iracingSdkBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {
getPerfRunConfig,
PERF_REPLAY_READY_LOG_MARKER,
} from '../../perfRunConfig';
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
import {
TELEMETRY_INSPECTOR_RATE_HZ,
type IrSdkSourceBridge,
type Session,
type Telemetry,
} from '@irdashies/types';
import logger from '../../logger';
import type { SessionLifecycle } from '../../sessionLifecycle';
import type { ChannelBus } from '../channelBridge';
Expand Down Expand Up @@ -148,7 +153,7 @@ export async function publishIRacingSDKEvents(
overlayManager: OverlayManager,
lifecycle?: SessionLifecycle,
channelBus?: ChannelBus
): Promise<IrSdkBridge> {
): Promise<IrSdkSourceBridge> {
logger.info('[iracingSdkBridge] Loading iRacing SDK bridge...');
const isTapeReplay = Boolean(process.env.IRDASHIES_TELEMETRY_REPLAY);
const sourceName = isTapeReplay ? 'telemetry replay' : 'iRacing';
Expand Down Expand Up @@ -238,7 +243,6 @@ export async function publishIRacingSDKEvents(

let shouldStop = false;
let lastRunningState: boolean | undefined = undefined;
let latestTelemetry: Telemetry | null = null;
let latestSession: Session | null = null;

const telemetryCallbacks = new Set<(value: Telemetry) => void>();
Expand All @@ -256,12 +260,6 @@ export async function publishIRacingSDKEvents(
'runningState',
lastRunningState
);
if (latestTelemetry && perfTelemetryDeliveryEnabled)
overlayManager.publishMessageToOverlay(
id,
'telemetry',
telemetryForRenderer(latestTelemetry)
);
if (latestSession)
overlayManager.publishMessageToOverlay(id, 'sessionData', latestSession);
});
Expand Down Expand Up @@ -302,6 +300,7 @@ export async function publishIRacingSDKEvents(
(async () => {
while (!shouldStop) {
let lastSessionVersion = -1;
let lastInspectorTelemetryPublishTime = Number.NEGATIVE_INFINITY;
// Negative infinity makes the first tick fetch and publish immediately.
let lastSessionPublishTime = Number.NEGATIVE_INFINITY;
let lastSessionPollTime = Number.NEGATIVE_INFINITY;
Expand All @@ -327,7 +326,6 @@ export async function publishIRacingSDKEvents(
}

if (telemetry) {
latestTelemetry = telemetry;
perfMetrics.markStart('lifecycleTelemetry');
lifecycle?._onTelemetry(telemetry);
perfMetrics.markEnd('lifecycleTelemetry');
Expand All @@ -346,13 +344,19 @@ export async function publishIRacingSDKEvents(
lapLogRuntime?.onFrame(telemetry);
if (
perfTelemetryDeliveryEnabled &&
overlayManager.hasLegacyStreamSubscribers('telemetry')
overlayManager.hasTelemetryInspectorSubscribers() &&
tickTime - lastInspectorTelemetryPublishTime >=
1000 / TELEMETRY_INSPECTOR_RATE_HZ
) {
lastInspectorTelemetryPublishTime = tickTime;
perfMetrics.markStart('telemetryProjection');
const rendererTelemetry = telemetryForRenderer(telemetry);
perfMetrics.markEnd('telemetryProjection');
perfMetrics.markStart('broadcast');
overlayManager.publishMessage('telemetry', rendererTelemetry);
overlayManager.publishMessage(
'telemetryInspector:telemetry',
rendererTelemetry
);
perfMetrics.markEnd('broadcast');
}
perfMetrics.markStart('telemetryCallbacks');
Expand Down Expand Up @@ -404,8 +408,8 @@ export async function publishIRacingSDKEvents(
// opened during a disconnect don't get re-seeded with stale data, and
// so the references don't sit in main-process memory indefinitely.
// They get repopulated on the next successful waitForData tick.
latestTelemetry = null;
latestSession = null;
overlayManager.clearLatestSessionData?.();
lifecycle?._onDisconnect();
}

Expand Down Expand Up @@ -434,6 +438,7 @@ export async function publishIRacingSDKEvents(
},
stop: () => {
shouldStop = true;
overlayManager.clearLatestSessionData?.();
sdk.stopSDK();
clearInterval(runningStateInterval);
telemetryCallbacks.clear();
Expand Down
6 changes: 3 additions & 3 deletions src/app/bridge/iracingSdk/mock-data/generateMockData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
import type { IrSdkSourceBridge, Session, Telemetry } from '@irdashies/types';
import mockSessionInfo from '../../../irsdk/node/utils/mock-data/session.json';
import mockTelemetry from '../../../irsdk/node/utils/mock-data/telemetry.json';

export async function generateMockDataFromPath(
path?: string
): Promise<IrSdkBridge> {
): Promise<IrSdkSourceBridge> {
if (!path) {
return generateMockData();
}
Expand All @@ -23,7 +23,7 @@ export async function generateMockDataFromPath(
export function generateMockData(sessionData?: {
telemetry: Telemetry | Telemetry[];
sessionInfo: Session | Session[];
}): IrSdkBridge {
}): IrSdkSourceBridge {
let telemetryInterval: NodeJS.Timeout | null = null;
let sessionInfoInterval: NodeJS.Timeout | null = null;
let runningStateInterval: NodeJS.Timeout | null = null;
Expand Down
55 changes: 49 additions & 6 deletions src/app/bridge/iracingSdk/mock-data/mockSdkBridge.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IrSdkBridge, Session, Telemetry } from '@irdashies/types';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { IrSdkSourceBridge, Session, Telemetry } from '@irdashies/types';
import { ChannelBus } from '../../channelBridge';

const callbacks = vi.hoisted(() => ({
Expand All @@ -9,7 +9,7 @@ const callbacks = vi.hoisted(() => ({
const stop = vi.hoisted(() => vi.fn());

vi.mock('./generateMockData', () => ({
generateMockData: (): IrSdkBridge => ({
generateMockData: (): IrSdkSourceBridge => ({
onTelemetry(callback) {
callbacks.telemetry = callback;
return () => undefined;
Expand Down Expand Up @@ -51,6 +51,36 @@ describe('mockSdkBridge processor channels', () => {
stop.mockReset();
});

afterEach(() => vi.restoreAllMocks());

it('publishes subscribed Inspector telemetry at no more than 10 Hz', async () => {
const publishMessage = vi.fn();
const now = vi
.spyOn(performance, 'now')
.mockReturnValueOnce(0)
.mockReturnValueOnce(50)
.mockReturnValueOnce(100);
const bridge = await publishIRacingSDKEvents({
publishMessage,
hasTelemetryInspectorSubscribers: () => true,
} as never);

try {
callbacks.telemetry?.(telemetry(0.1, 1));
callbacks.telemetry?.(telemetry(0.11, 1.05));
callbacks.telemetry?.(telemetry(0.12, 1.1));
expect(now).toHaveBeenCalledTimes(3);
expect(publishMessage).toHaveBeenCalledTimes(2);
expect(publishMessage).toHaveBeenNthCalledWith(
1,
'telemetryInspector:telemetry',
expect.anything()
);
} finally {
bridge.stop();
}
});

it('feeds mock session and telemetry through the car-speed runtime', async () => {
const bus = new ChannelBus();
const publish = vi.spyOn(bus, 'publish');
Expand All @@ -61,7 +91,10 @@ describe('mockSdkBridge processor channels', () => {
send: vi.fn(),
};
bus.subscribe(target, 'car-speeds.snapshot');
const overlayManager = { publishMessage: vi.fn() };
const overlayManager = {
publishMessage: vi.fn(),
hasTelemetryInspectorSubscribers: () => false,
};
const bridge = await publishIRacingSDKEvents(
overlayManager as never,
undefined,
Expand All @@ -79,6 +112,10 @@ describe('mockSdkBridge processor channels', () => {
'car-speeds.snapshot',
expect.objectContaining({ carSpeeds: [360] })
);
expect(overlayManager.publishMessage).not.toHaveBeenCalledWith(
'telemetryInspector:telemetry',
expect.anything()
);
} finally {
bridge.stop();
}
Expand All @@ -101,7 +138,10 @@ describe('mockSdkBridge processor channels', () => {
'session-timing.snapshot'
);
const bridge = await publishIRacingSDKEvents(
{ publishMessage: vi.fn() } as never,
{
publishMessage: vi.fn(),
hasTelemetryInspectorSubscribers: () => false,
} as never,
undefined,
bus
);
Expand Down Expand Up @@ -148,7 +188,10 @@ describe('mockSdkBridge processor channels', () => {
'relative-gaps.snapshot'
);
const bridge = await publishIRacingSDKEvents(
{ publishMessage: vi.fn() } as never,
{
publishMessage: vi.fn(),
hasTelemetryInspectorSubscribers: () => false,
} as never,
undefined,
bus
);
Expand Down
17 changes: 14 additions & 3 deletions src/app/bridge/iracingSdk/mock-data/mockSdkBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
import { DriverControlsRuntime } from '../../../processors/driverControlsRuntime';
import { TrackStateRuntime } from '../../../processors/trackStateRuntime';
import { LapLogRuntime } from '../../../processors/lapLogRuntime';
import { TELEMETRY_INSPECTOR_RATE_HZ } from '@irdashies/types';

export async function publishIRacingSDKEvents(
overlayManager: OverlayManager,
lifecycle?: SessionLifecycle,
channelBus?: ChannelBus
) {
const perfMetrics = new TelemetryPerfMetrics();
let lastInspectorTelemetryPublishTime = Number.NEGATIVE_INFINITY;
perfMetrics.startReporting();

const bridge = generateMockData();
Expand Down Expand Up @@ -105,9 +107,17 @@ export async function publishIRacingSDKEvents(
driverControlsRuntime?.onFrame(telemetry);
trackStateRuntime?.onFrame(telemetry);
lapLogRuntime?.onFrame(telemetry);
perfMetrics.markStart('broadcast');
overlayManager.publishMessage('telemetry', telemetry);
perfMetrics.markEnd('broadcast');
const tickTime = performance.now();
if (
overlayManager.hasTelemetryInspectorSubscribers() &&
tickTime - lastInspectorTelemetryPublishTime >=
1000 / TELEMETRY_INSPECTOR_RATE_HZ
) {
lastInspectorTelemetryPublishTime = tickTime;
perfMetrics.markStart('broadcast');
overlayManager.publishMessage('telemetryInspector:telemetry', telemetry);
perfMetrics.markEnd('broadcast');
}
perfMetrics.markEnd('processTelemetry');
perfMetrics.tick(telemetry);
});
Expand All @@ -120,6 +130,7 @@ export async function publishIRacingSDKEvents(
return {
...bridge,
stop: () => {
overlayManager.clearLatestSessionData?.();
carSpeedsRuntime?.dispose();
lapTimesRuntime?.dispose();
relativeGapRuntime?.dispose();
Expand Down
10 changes: 5 additions & 5 deletions src/app/bridge/iracingSdk/setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OverlayManager } from '../../overlayManager';
import { ipcMain } from 'electron';
import type { IrSdkBridge } from '@irdashies/types';
import type { IrSdkSourceBridge } from '@irdashies/types';
import logger from '../../logger';
import {
createSessionLifecycle,
Expand All @@ -9,8 +9,8 @@ import {
import type { ChannelBus } from '../channelBridge';

let isDemoMode = false;
let currentBridge: IrSdkBridge | undefined;
const onBridgeChangedCallbacks = new Set<(bridge: IrSdkBridge) => void>();
let currentBridge: IrSdkSourceBridge | undefined;
const onBridgeChangedCallbacks = new Set<(bridge: IrSdkSourceBridge) => void>();

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

export function getCurrentBridge(): IrSdkBridge | undefined {
export function getCurrentBridge(): IrSdkSourceBridge | undefined {
return currentBridge;
}

export function getIsDemoMode(): boolean {
return isDemoMode;
}

export function onBridgeChanged(callback: (bridge: IrSdkBridge) => void) {
export function onBridgeChanged(callback: (bridge: IrSdkSourceBridge) => void) {
onBridgeChangedCallbacks.add(callback);
return () => onBridgeChangedCallbacks.delete(callback);
}
Expand Down
16 changes: 0 additions & 16 deletions src/app/bridge/legacyRendererSubscriptions.ts

This file was deleted.

16 changes: 16 additions & 0 deletions src/app/bridge/rendererDataSubscriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineRendererSubscriptionBridge } from './defineBridge';

export const RENDERER_DATA_SUBSCRIPTION_BRIDGE = 'renderer-data';

export type RendererDataStream = 'sessionData' | 'telemetryInspector';

export const isRendererDataStream = (
value: unknown
): value is RendererDataStream =>
value === 'sessionData' || value === 'telemetryInspector';

export const setupRendererDataSubscriptions = () =>
defineRendererSubscriptionBridge<RendererDataStream>({
name: RENDERER_DATA_SUBSCRIPTION_BRIDGE,
isValidKey: isRendererDataStream,
});
Loading