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
12 changes: 12 additions & 0 deletions src/app/bridge/rendererExposeBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import type {
ChromiumFlagsBridge,
ChromiumFlagsType,
TelemetryInspectorBridge,
RendererPerfBridge,
} from '@irdashies/types';
import {
isRendererPerfMetricsEnabled,
recordTelemetryCallback,
recordRendererMeasure,
} from '../rendererPerfMetrics';
import {
RENDERER_DATA_SUBSCRIPTION_BRIDGE,
Expand All @@ -29,6 +31,16 @@ import {
import { createSubscriptionBridgeClient, defineBridge } from './defineBridge';

export function exposeBridge() {
if (isRendererPerfMetricsEnabled()) {
defineBridge<RendererPerfBridge>('rendererPerfBridge', {
recordMeasure: (name, durationMs) => {
if (!isRendererPerfMetricsEnabled()) return;
if (name !== 'trackMapAnimationFrame') return;
if (!Number.isFinite(durationMs) || durationMs < 0) return;
recordRendererMeasure(name, durationMs);
},
});
}
const rendererDataSubscriptions =
createSubscriptionBridgeClient<RendererDataStream>(
RENDERER_DATA_SUBSCRIPTION_BRIDGE
Expand Down
20 changes: 19 additions & 1 deletion src/app/rendererPerfMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ipcRenderer } from 'electron';
import type { RendererPerfSample } from '@irdashies/types';
import type {
RendererPerfMeasureName,
RendererPerfSample,
} from '@irdashies/types';
import { FixedSampleBuffer } from '../shared/performanceSamples';
import { readRendererPerfArguments } from './perfRendererArguments';

export const PERF_RENDERER_LOG_PREFIX = '[PerfRenderer:JSON] ';

let telemetryCallbackTimes: FixedSampleBuffer | undefined;
let channelCallbackTimes: FixedSampleBuffer | undefined;
let trackMapAnimationFrameTimes: FixedSampleBuffer | undefined;

export function isRendererPerfMetricsEnabled(): boolean {
return telemetryCallbackTimes !== undefined;
Expand All @@ -20,6 +24,15 @@ export function recordChannelCallback(durationMs: number): void {
channelCallbackTimes?.add(durationMs);
}

export function recordRendererMeasure(
name: RendererPerfMeasureName,
durationMs: number
): void {
if (name === 'trackMapAnimationFrame') {
trackMapAnimationFrameTimes?.add(durationMs);
}
}

export function startRendererPerfMetrics(): void {
const config = readRendererPerfArguments();
if (!config.enabled) return;
Expand All @@ -34,6 +47,8 @@ export function startRendererPerfMetrics(): void {
telemetryCallbackTimes = callbackTimes;
const channelTimes = new FixedSampleBuffer(4096);
channelCallbackTimes = channelTimes;
const trackMapFrameTimes = new FixedSampleBuffer(4096);
trackMapAnimationFrameTimes = trackMapFrameTimes;
let intervalStart = performance.now();
let previousFrameTime = 0;
let framesOver25Ms = 0;
Expand All @@ -60,6 +75,7 @@ export function startRendererPerfMetrics(): void {
previousFrameTime = 0;
callbackTimes.reset();
channelTimes.reset();
trackMapFrameTimes.reset();
framesOver25Ms = 0;
framesOver50Ms = 0;
return;
Expand All @@ -77,6 +93,7 @@ export function startRendererPerfMetrics(): void {
frameTimeMs: stats,
telemetryCallbackMs: callbackTimes.summarize(),
channelCallbackMs: channelTimes.summarize(),
trackMapAnimationFrameMs: trackMapFrameTimes.summarize(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface the track-map metric in performance reports

Although the RAF is now wrapped and this field is emitted into the raw renderer sample, tools/perf/analyze.ts never consumes trackMapAnimationFrameMs: its renderer summary and generated table still only report frame time and telemetry callbacks. Consequently normal perf:analyze output cannot display or compare the newly instrumented hot path, so the R13.1 observability requirement remains incomplete. This unchanged analyzer path is fresh evidence beyond the earlier instrumentation comment; add the metric to the analyzer summary/report and its tests.

AGENTS.md reference: AGENTS.md:L12-L18

Useful? React with 👍 / 👎.

telemetryWakeups: callbackTimes.summarize().count,
channelWakeups: channelTimes.summarize().count,
framesOver25Ms,
Expand All @@ -93,6 +110,7 @@ export function startRendererPerfMetrics(): void {
frameTimes.reset();
callbackTimes.reset();
channelTimes.reset();
trackMapFrameTimes.reset();
framesOver25Ms = 0;
framesOver50Ms = 0;
}, reportIntervalMs);
Expand Down
179 changes: 94 additions & 85 deletions src/frontend/components/TrackMap/FlatTrackMapCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { TrackDriver, TrackDrawing } from './TrackCanvas';
import { getColor, getTailwindStyle } from '@irdashies/utils/colors';
import { useCarIdxOffTrack } from '@irdashies/context';
import { progressToFlatX, useProgressAnimation } from './useProgressAnimation';
import { getCachedTextVisualOffset } from './trackDrawingUtils';

export interface FlatTrackMapCanvasProps {
trackDrawing: TrackDrawing;
Expand Down Expand Up @@ -74,6 +76,18 @@ export const FlatTrackMapCanvas = ({
return colors;
}, [drivers, isMultiClass, highlightColor]);

const orderedDrivers = useMemo(
() =>
drivers
.map((entry, interpolationIndex) => ({
...entry,
interpolationIndex,
textMetricsCache: { font: '', text: '', visualOffset: 0 },
}))
.sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer)),
[drivers]
);

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
Expand Down Expand Up @@ -121,7 +135,7 @@ export const FlatTrackMapCanvas = ({
};
}, []);

useLayoutEffect(() => {
useProgressAnimation(drivers, (progressValues, count) => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx || canvasSize.width === 0) return;
Expand Down Expand Up @@ -193,93 +207,88 @@ export const FlatTrackMapCanvas = ({

// Draw drivers
// Apply scale factor to match curved track map proportions
[...drivers]
.sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer))
.forEach(({ driver, progress, isPlayer, classPosition }) => {
let color = driverColors[driver.CarIdx];
if (!color) return;

const x = HORIZONTAL_PADDING + progress * usableWidth;
const radius =
(isPlayer ? playerCircleSize : driverCircleSize) * circleScale;
const fontSize = radius * (trackmapFontSize / 100);
const originalColor = color.fill;
const livePosition =
driverLivePositions[driver.CarIdx] ?? classPosition;

// highlight leader?
if (!isPlayer && invertLeaderColor && livePosition === 1) {
color = { fill: 'white', text: originalColor };
}
for (const orderedDriver of orderedDrivers) {
const { driver, isPlayer, classPosition, interpolationIndex } =
orderedDriver;
if (interpolationIndex >= count) continue;
const color = driverColors[driver.CarIdx];
if (!color) continue;

// on pit road?
const onPitRoad = !!carIdxIsOnPitRoad?.[driver.CarIdx];
if (onPitRoad) {
color = { fill: '#999999', text: 'white' };
}
const x = progressToFlatX(
progressValues[interpolationIndex],
HORIZONTAL_PADDING,
usableWidth
);
const radius =
(isPlayer ? playerCircleSize : driverCircleSize) * circleScale;
const fontSize = radius * (trackmapFontSize / 100);
const originalColor = color.fill;
let fillColor = color.fill;
let textColor = color.text;
const livePosition = driverLivePositions[driver.CarIdx] ?? classPosition;

ctx.fillStyle = color.fill;
ctx.beginPath();
ctx.arc(x, centerY, radius, 0, 2 * Math.PI);
ctx.fill();

// draw a border?
if (driversOffTrack[driver.CarIdx]) {
ctx.strokeStyle = getColor('yellow', 400);
ctx.lineWidth = 4;
ctx.stroke();
} else if (!isPlayer && invertLeaderColor && livePosition === 1) {
ctx.strokeStyle = originalColor;
ctx.lineWidth = 2;
ctx.stroke();
}
// highlight leader?
if (!isPlayer && invertLeaderColor && livePosition === 1) {
fillColor = 'white';
textColor = originalColor;
}

// on pit road?
const onPitRoad = !!carIdxIsOnPitRoad?.[driver.CarIdx];
if (onPitRoad) {
fillColor = '#999999';
textColor = 'white';
}

if (showCarNumbers) {
ctx.fillStyle = color.text;
ctx.font = `${fontSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let displayText;
if (onPitRoad) {
displayText = 'P';
} else if (displayMode === 'livePosition') {
displayText =
livePosition !== undefined && livePosition > 0
? livePosition.toString()
: '';
} else if (displayMode === 'sessionPosition') {
displayText =
classPosition !== undefined && classPosition > 0
? classPosition.toString()
: '';
} else {
displayText = driver.CarNumber;
}
if (displayText) {
const m = ctx.measureText(displayText);
const visualOffset =
(m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2;
ctx.fillText(displayText, x, centerY + visualOffset);
}
ctx.fillStyle = fillColor;
ctx.beginPath();
ctx.arc(x, centerY, radius, 0, 2 * Math.PI);
ctx.fill();

// draw a border?
if (driversOffTrack[driver.CarIdx]) {
ctx.strokeStyle = getColor('yellow', 400);
ctx.lineWidth = 4;
ctx.stroke();
} else if (!isPlayer && invertLeaderColor && livePosition === 1) {
ctx.strokeStyle = originalColor;
ctx.lineWidth = 2;
ctx.stroke();
}

if (showCarNumbers) {
ctx.fillStyle = textColor;
ctx.font = `${fontSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let displayText;
if (onPitRoad) {
displayText = 'P';
} else if (displayMode === 'livePosition') {
displayText =
livePosition !== undefined && livePosition > 0
? livePosition.toString()
: '';
} else if (displayMode === 'sessionPosition') {
displayText =
classPosition !== undefined && classPosition > 0
? classPosition.toString()
: '';
} else {
displayText = driver.CarNumber;
}
if (displayText) {
const cache = orderedDriver.textMetricsCache;
const visualOffset = getCachedTextVisualOffset(
ctx,
displayText,
cache
);
ctx.fillText(displayText, x, centerY + visualOffset);
}
});
}, [
canvasSize,
drivers,
driverColors,
invertLeaderColor,
driversOffTrack,
showCarNumbers,
displayMode,
driverCircleSize,
playerCircleSize,
trackmapFontSize,
trackLineWidth,
trackOutlineWidth,
invertTrackColors,
driverLivePositions,
carIdxIsOnPitRoad,
]);
}
}
});

return (
<div className="w-full h-full">
Expand Down
Loading