mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
Only collect telemetry when search param
This commit is contained in:
@@ -116,8 +116,10 @@ sequenceDiagram
|
||||
the gate. `checkIntervalS` hard-caps both phases; `attachFrame: false`
|
||||
keeps continuously-firing events from storing a frame PNG each. Frames no
|
||||
detector is due for skip canvas readback, and everything is counted in
|
||||
`core/detectors/telemetry.ts` (VoD tab's telemetry panel). A match's
|
||||
objective reads render as one step-line timeline
|
||||
`core/detectors/telemetry.ts` — but only when the VoD tab is opened with
|
||||
`?telemetry=true` (nothing links there); otherwise the workers skip
|
||||
collection and the panel stays hidden. A match's objective reads render
|
||||
as one step-line timeline
|
||||
(`~/components/ObjectiveTimeline.tsx`, shared with the match page).
|
||||
- VoD scans (`components/VodPage.tsx`): on the WebCodecs path each worker
|
||||
demuxes + decodes its own contiguous slice (mediabunny in the worker — no
|
||||
|
||||
@@ -22,6 +22,7 @@ import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouMenu, SendouMenuItem } from "~/components/elements/Menu";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { ObjectiveTimeline } from "~/components/ObjectiveTimeline";
|
||||
import { useSearchParam } from "~/modules/search-params/hooks";
|
||||
import { openSeekScan, probeWebCodecs } from "../capture/vod-frames";
|
||||
import { connectAbilities } from "../core/ability-harvest";
|
||||
import { OBJECTIVE_EVENT_TYPE } from "../core/detectors/objective/index";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
invalidObjectiveEvents,
|
||||
} from "../core/match-builder";
|
||||
import { TimelineBuilder } from "../core/timeline/index";
|
||||
import { scannerSearchParams } from "../scanner-search-params";
|
||||
import type { SendStatus } from "../store/events";
|
||||
import {
|
||||
deleteVod,
|
||||
@@ -133,6 +135,9 @@ export function VodPage({
|
||||
// in-flight thumbnail work; awaited before persisting a finished scan
|
||||
const sideWorkRef = useRef<Promise<void>[]>([]);
|
||||
const nextMatchKeyRef = useRef(0);
|
||||
// telemetry collection is baked into the workers at init, so a change of
|
||||
// the search param needs a fresh pool
|
||||
const clientsCollectTelemetryRef = useRef(false);
|
||||
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
/** live scan vs. reopened saved VoD (no video element for the latter) */
|
||||
@@ -149,6 +154,9 @@ export function VodPage({
|
||||
const [eventsOpen, setEventsOpen] = useState(false);
|
||||
const [resultsSend, setResultsSend] = useState<ResultsSend | null>(null);
|
||||
|
||||
// opt-in via ?telemetry=true only; nothing in the UI links to it
|
||||
const [collectTelemetry] = useSearchParam(scannerSearchParams, "telemetry");
|
||||
|
||||
const formatSavedAt = useEventDateTimeFormatter();
|
||||
|
||||
const abilityMap = connectAbilities(matches.map((m) => m.event));
|
||||
@@ -259,6 +267,11 @@ export function VodPage({
|
||||
urlRef.current = URL.createObjectURL(file);
|
||||
video.src = urlRef.current;
|
||||
|
||||
if (clientsCollectTelemetryRef.current !== collectTelemetry) {
|
||||
for (const client of clientsRef.current) client.dispose();
|
||||
clientsRef.current = [];
|
||||
clientsCollectTelemetryRef.current = collectTelemetry;
|
||||
}
|
||||
if (clientsRef.current.length === 0) {
|
||||
clientsRef.current = Array.from(
|
||||
{ length: defaultScanWorkerCount() },
|
||||
@@ -310,6 +323,7 @@ export function VodPage({
|
||||
frameDoneRef.current?.();
|
||||
frameDoneRef.current = null;
|
||||
},
|
||||
{ collectTelemetry },
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -365,10 +379,12 @@ export function VodPage({
|
||||
abortScanRef.current = () => {
|
||||
for (const client of clients) client.abortChunk();
|
||||
};
|
||||
const mergedTelemetry = () =>
|
||||
mergeScanTelemetry(
|
||||
chunks.flatMap((c) => (c.telemetry ? [c.telemetry] : [])),
|
||||
const mergedTelemetry = () => {
|
||||
const parts = chunks.flatMap((c) =>
|
||||
c.telemetry ? [c.telemetry] : [],
|
||||
);
|
||||
return parts.length > 0 ? mergeScanTelemetry(parts) : null;
|
||||
};
|
||||
let lastUiUpdate = Number.NEGATIVE_INFINITY;
|
||||
const pushUiUpdate = () => {
|
||||
const now = performance.now();
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("scannerSearchParams", () => {
|
||||
assertRoundTrips(scannerSearchParams, {
|
||||
tab: ["live", "screenshot", "vod"],
|
||||
inspect: ["1723456789012-abc123", null],
|
||||
telemetry: [true, false],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,4 +10,9 @@ export const scannerSearchParams = SearchParams.define({
|
||||
tab: SP.param(z.enum(SCANNER_TABS), { default: "live", loader: false }),
|
||||
/** Inspect handoff key: the screenshot tab claims this frame on load */
|
||||
inspect: SP.param(z.string().max(100).nullable(), { loader: false }),
|
||||
/**
|
||||
* Opt-in scan telemetry: counters are accumulated and the panel is shown
|
||||
* only when this is set by hand in the URL (no link points at it)
|
||||
*/
|
||||
telemetry: SP.param(z.boolean(), { default: false, loader: false }),
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { DetectorScheduler } from "../core/detectors/scheduler";
|
||||
import {
|
||||
createScanTelemetry,
|
||||
detectorTelemetry,
|
||||
type ScanTelemetry,
|
||||
} from "../core/detectors/telemetry";
|
||||
import type { Detector } from "../core/detectors/types";
|
||||
import { normalizeFrame, toMat } from "../core/image";
|
||||
@@ -53,7 +54,9 @@ const PREVIEW_HEIGHT = 270;
|
||||
|
||||
let detectors: Detector<unknown>[] = [];
|
||||
let scheduler: DetectorScheduler | null = null;
|
||||
let telemetry = createScanTelemetry();
|
||||
/** null unless the init message asked for telemetry */
|
||||
let telemetry: ScanTelemetry | null = null;
|
||||
let collectTelemetry = false;
|
||||
let chunkAborted = false;
|
||||
/** last per-frame t, to reset telemetry when a new session rewinds the clock */
|
||||
let lastFrameT = Number.NEGATIVE_INFINITY;
|
||||
@@ -65,6 +68,7 @@ function post(message: WorkerResponse, transfer: Transferable[] = []): void {
|
||||
async function init({
|
||||
assetsBaseUrl,
|
||||
suppressSteadyFrames = true,
|
||||
collectTelemetry: collect = false,
|
||||
}: InitRequest): Promise<void> {
|
||||
try {
|
||||
await loadOpenCV();
|
||||
@@ -75,7 +79,8 @@ async function init({
|
||||
matchOpeningTypes: [MAP_START_EVENT_TYPE],
|
||||
matchClosingTypes: SCOREBOARD_EVENT_TYPES,
|
||||
});
|
||||
telemetry = createScanTelemetry();
|
||||
collectTelemetry = collect;
|
||||
telemetry = freshTelemetry();
|
||||
post({ kind: "ready" });
|
||||
} catch (error) {
|
||||
post({ kind: "error", message: `init failed: ${String(error)}` });
|
||||
@@ -115,7 +120,7 @@ async function analyzeFrame(
|
||||
} finally {
|
||||
src.delete();
|
||||
}
|
||||
telemetry.analyzedFrames++;
|
||||
if (telemetry) telemetry.analyzedFrames++;
|
||||
|
||||
// On detection, ship back the exact analyzed pixels (lossless, at capture
|
||||
// resolution) so the UI never has to re-grab a later frame — encoded at
|
||||
@@ -127,21 +132,27 @@ async function analyzeFrame(
|
||||
try {
|
||||
for (const detector of detectors) {
|
||||
if (!due.includes(detector.id)) continue;
|
||||
const counters = detectorTelemetry(telemetry, detector.id);
|
||||
counters.checks++;
|
||||
const gateStart = performance.now();
|
||||
const counters = telemetry
|
||||
? detectorTelemetry(telemetry, detector.id)
|
||||
: null;
|
||||
const gateStart = counters ? performance.now() : 0;
|
||||
const gate = detector.gate(frame);
|
||||
counters.gateMs += performance.now() - gateStart;
|
||||
if (counters) {
|
||||
counters.checks++;
|
||||
counters.gateMs += performance.now() - gateStart;
|
||||
}
|
||||
scheduler!.recordGate(detector.id, t, gate.pass, gate.signature);
|
||||
if (gate.pass) counters.gatePasses++;
|
||||
if (counters && gate.pass) counters.gatePasses++;
|
||||
const runParse = gate.pass && scheduler!.shouldParse(detector.id, t);
|
||||
if (gate.pass && !runParse) counters.suppressedParses++;
|
||||
if (counters && gate.pass && !runParse) counters.suppressedParses++;
|
||||
let events: ReturnType<typeof detector.parse> = [];
|
||||
if (runParse) {
|
||||
const parseStart = performance.now();
|
||||
const parseStart = counters ? performance.now() : 0;
|
||||
events = detector.parse(frame, t, gate);
|
||||
counters.parses++;
|
||||
counters.parseMs += performance.now() - parseStart;
|
||||
if (counters) {
|
||||
counters.parses++;
|
||||
counters.parseMs += performance.now() - parseStart;
|
||||
}
|
||||
scheduler!.recordParse(detector.id, t, events);
|
||||
}
|
||||
const blob =
|
||||
@@ -163,7 +174,7 @@ async function analyzeFrame(
|
||||
}
|
||||
|
||||
async function analyze({ bitmap, t }: AnalyzeRequest): Promise<void> {
|
||||
if (t + 5 < lastFrameT) telemetry = createScanTelemetry();
|
||||
if (t + 5 < lastFrameT) telemetry = freshTelemetry();
|
||||
lastFrameT = t;
|
||||
try {
|
||||
await analyzeFrame(bitmap, t);
|
||||
@@ -181,7 +192,7 @@ async function scanChunk({
|
||||
}: ScanChunkRequest): Promise<void> {
|
||||
chunkAborted = false;
|
||||
scheduler!.reset(tStart);
|
||||
telemetry = createScanTelemetry();
|
||||
telemetry = freshTelemetry();
|
||||
const wallStart = performance.now();
|
||||
let lastProgressAt = 0;
|
||||
let lastPreviewAt = 0;
|
||||
@@ -201,11 +212,13 @@ async function scanChunk({
|
||||
const packets = new EncodedPacketSink(track);
|
||||
|
||||
const handleSample = async (sample: VideoSample): Promise<void> => {
|
||||
telemetry.decodedFrames++;
|
||||
const t = sample.timestamp;
|
||||
const span = Math.max(0, t - cursor);
|
||||
if (mode === "active") telemetry.activeVideoS += span;
|
||||
else telemetry.skimVideoS += span;
|
||||
if (telemetry) {
|
||||
telemetry.decodedFrames++;
|
||||
const span = Math.max(0, t - cursor);
|
||||
if (mode === "active") telemetry.activeVideoS += span;
|
||||
else telemetry.skimVideoS += span;
|
||||
}
|
||||
cursor = Math.max(cursor, t);
|
||||
const frame = sample.toVideoFrame();
|
||||
sample.close();
|
||||
@@ -225,7 +238,7 @@ async function scanChunk({
|
||||
}
|
||||
if (preview || now - lastProgressAt >= PROGRESS_POST_INTERVAL_MS) {
|
||||
lastProgressAt = now;
|
||||
telemetry.wallMs = performance.now() - wallStart;
|
||||
if (telemetry) telemetry.wallMs = performance.now() - wallStart;
|
||||
post(
|
||||
{
|
||||
kind: "chunkProgress",
|
||||
@@ -282,7 +295,7 @@ async function scanChunk({
|
||||
}
|
||||
}
|
||||
|
||||
telemetry.wallMs = performance.now() - wallStart;
|
||||
if (telemetry) telemetry.wallMs = performance.now() - wallStart;
|
||||
post({ kind: "chunkDone", chunkIndex, telemetry });
|
||||
} catch (error) {
|
||||
post({
|
||||
@@ -294,6 +307,10 @@ async function scanChunk({
|
||||
}
|
||||
}
|
||||
|
||||
function freshTelemetry(): ScanTelemetry | null {
|
||||
return collectTelemetry ? createScanTelemetry() : null;
|
||||
}
|
||||
|
||||
self.onmessage = (e: MessageEvent) => {
|
||||
const msg = e.data as WorkerRequest;
|
||||
if (msg.kind === "init") void init(msg);
|
||||
|
||||
@@ -16,7 +16,8 @@ export type ResultHandler = (
|
||||
export type ErrorHandler = (message: string) => void;
|
||||
export interface DoneInfo {
|
||||
calm: boolean;
|
||||
telemetry: ScanTelemetry;
|
||||
/** null unless the client was created with collectTelemetry */
|
||||
telemetry: ScanTelemetry | null;
|
||||
}
|
||||
export type DoneHandler = (t: number, info: DoneInfo) => void;
|
||||
export type ChunkProgress = Extract<WorkerResponse, { kind: "chunkProgress" }>;
|
||||
@@ -29,7 +30,7 @@ export function defaultScanWorkerCount(): number {
|
||||
}
|
||||
|
||||
interface PendingChunk {
|
||||
resolve(telemetry: ScanTelemetry): void;
|
||||
resolve(telemetry: ScanTelemetry | null): void;
|
||||
reject(error: Error): void;
|
||||
onProgress?: ChunkProgressHandler;
|
||||
}
|
||||
@@ -51,7 +52,10 @@ export class AnalyzerClient {
|
||||
// biome-ignore lint/suspicious/noConsole: default sink for worker errors when no handler is passed
|
||||
onError: ErrorHandler = console.error,
|
||||
onDone?: DoneHandler,
|
||||
options: { suppressSteadyFrames?: boolean } = {},
|
||||
options: {
|
||||
suppressSteadyFrames?: boolean;
|
||||
collectTelemetry?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
this.#onResult = onResult;
|
||||
this.#onError = onError;
|
||||
@@ -104,6 +108,7 @@ export class AnalyzerClient {
|
||||
kind: "init",
|
||||
assetsBaseUrl: Config.staticAssetsUrl,
|
||||
suppressSteadyFrames: options.suppressSteadyFrames ?? true,
|
||||
collectTelemetry: options.collectTelemetry ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ export class AnalyzerClient {
|
||||
scanChunk(
|
||||
request: { file: File; chunkIndex: number; tStart: number; tEnd: number },
|
||||
onProgress?: ChunkProgressHandler,
|
||||
): Promise<ScanTelemetry> {
|
||||
): Promise<ScanTelemetry | null> {
|
||||
if (this.busy) {
|
||||
return Promise.reject(new Error("analyzer is busy"));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ export interface InitRequest {
|
||||
* off to get every detector on every frame
|
||||
*/
|
||||
suppressSteadyFrames?: boolean;
|
||||
/**
|
||||
* accumulate scan telemetry counters (and time the detectors) so they can
|
||||
* be reported back with progress and done messages; default false — the
|
||||
* VoD tab only asks for them when the telemetry panel is opted into
|
||||
*/
|
||||
collectTelemetry?: boolean;
|
||||
}
|
||||
|
||||
export interface AnalyzeRequest {
|
||||
@@ -70,7 +76,8 @@ export type WorkerResponse =
|
||||
t: number;
|
||||
/** scheduler sees dead air — the caller may widen its sampling stride */
|
||||
calm: boolean;
|
||||
telemetry: ScanTelemetry;
|
||||
/** null when the worker was not asked to collect telemetry */
|
||||
telemetry: ScanTelemetry | null;
|
||||
}
|
||||
| {
|
||||
kind: "chunkProgress";
|
||||
@@ -78,9 +85,9 @@ export type WorkerResponse =
|
||||
/** seconds of video the chunk scan has reached */
|
||||
t: number;
|
||||
mode: "active" | "skim";
|
||||
telemetry: ScanTelemetry;
|
||||
telemetry: ScanTelemetry | null;
|
||||
/** small bitmap of the latest decoded frame, for the preview canvas */
|
||||
preview?: ImageBitmap;
|
||||
}
|
||||
| { kind: "chunkDone"; chunkIndex: number; telemetry: ScanTelemetry }
|
||||
| { kind: "chunkDone"; chunkIndex: number; telemetry: ScanTelemetry | null }
|
||||
| { kind: "error"; message: string };
|
||||
|
||||
Reference in New Issue
Block a user