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
68 changes: 68 additions & 0 deletions src/components/bus-debug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useSyncExternalStore } from "react";
import { Card } from "@/components/ui/card";
import { useBusStats } from "@/lib/bus/hooks";
import type { Quantiles } from "@/lib/bus/manager";

const never = () => () => undefined;

/** `?debug` only: the numbers the hardware procedure records. */
export function BusDebug() {
const wanted = useSyncExternalStore(
never,
() => new URLSearchParams(location.search).has("debug"),
() => false,
);
return wanted ? <Panel /> : null;
}

function n(value: number, digits = 0): string {
return value.toFixed(digits);
}

function q(value: Quantiles): string {
return `${n(value.p50, 1)}/${n(value.p95, 1)}`;
}

function Panel() {
const stats = useBusStats();
const rows: [string, string][] = [
["exchanges", `${stats.exchanges}`],
["per second", n(stats.perSecond, 1)],
["bytes/s", n(stats.bytesPerSecond)],
["wait p50/p95", q(stats.wait)],
["run small", q(stats.run.small)],
["run medium", q(stats.run.medium)],
["run large", q(stats.run.large)],
["lag p50/p95", q(stats.lag)],
["timeouts", `${stats.timeouts}`],
["stalled", `${stats.stalled}`],
["errors", `${stats.errors}`],
["coalesced", `${stats.coalesced}`],
["grouped", `${stats.grouped}`],
["utilisation", n(stats.utilisation, 2)],
["period fast/slow", `${n(stats.effectivePeriodMs.fast)}/${n(stats.effectivePeriodMs.slow)}`],
];
return (
<Card
size="sm"
role="region"
aria-label="Bus statistics"
className="fixed right-2 bottom-2 z-50 w-56 font-mono text-xs"
>
<dl className="grid grid-cols-[1fr_auto] gap-x-3 gap-y-0.5 px-(--card-spacing)">
{rows.map(([label, value]) => (
<div key={label} className="col-span-2 grid grid-cols-subgrid">
<dt className="text-text-3">{label}</dt>
<dd className="tabular-nums">{value}</dd>
</div>
))}
</dl>
{[...stats.perServo].map(([id, s]) => (
<p key={id} className="px-(--card-spacing) text-text-3">
ID {id} fails {s.consecutiveFailures}
{s.probing ? " probing" : ""}
</p>
))}
</Card>
);
}
2 changes: 1 addition & 1 deletion src/components/calibration-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { CALIBRATION_REGISTERS, editReason, type CalibrationRegister } from "@/lib/calibration";
import { decodeSpan, span } from "@/lib/card-poll";
import { decodeSpan, span } from "@/lib/bus/spans";
import { hexAddr } from "@/lib/format";
import { useSession } from "@/lib/session";
import { calibrationFromTable, calibrationStatus, type Calibration } from "@/lib/units";
Expand Down
144 changes: 144 additions & 0 deletions src/lib/bus/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { OscClient, Value } from "@openservocore/client";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useSyncExternalStore } from "react";
import type { BusManager, BusStats, Rate, Snapshot } from "./manager";
import type { BusStore } from "./store";

export interface BusHost {
manager: BusManager;
store: BusStore;
}

export const BusContext = createContext<BusHost | undefined>(undefined);

export function useBusHost(): BusHost {
const host = useContext(BusContext);
if (host === undefined) throw new Error("useBus outside BusContext");
return host;
}

function message(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}

/** A register list stable across renders that pass an equal one. */
function useNames(registers: readonly string[]): readonly string[] {
const key = registers.join(",");
return useMemo(() => (key === "" ? [] : key.split(",")), [key]);
}

export function useRegisters(
id: number,
registers: readonly string[],
rate: Rate,
): Snapshot | undefined {
const { manager, store } = useBusHost();
const names = useNames(registers);
const bound = useMemo(
() => ({
cell: store.cell<Snapshot | undefined>(undefined),
sub: { id, registers: names, rate },
}),
[store, id, names, rate],
);
useEffect(
() =>
manager.subscribe(bound.sub, (s) => {
bound.cell.set(s);
}),
[manager, bound],
);
return useSyncExternalStore(bound.cell.subscribe, bound.cell.get, bound.cell.get);
}

export function useRing(
id: number,
registers: readonly string[],
rate: Rate,
windowS: number,
): readonly Snapshot[] {
const { manager, store } = useBusHost();
const names = useNames(registers);
const bound = useMemo(
() => ({ ring: store.ring(windowS), sub: { id, registers: names, rate } }),
[store, windowS, id, names, rate],
);
useEffect(
() =>
manager.subscribe(bound.sub, (s) => {
bound.ring.push(s);
}),
[manager, bound],
);
return useSyncExternalStore(bound.ring.subscribe, bound.ring.get, bound.ring.get);
}

export interface ReadOnce {
snapshot: Snapshot | undefined;
error: string | undefined;
reload: () => void;
}

export function useReadOnce(
id: number,
registers: readonly string[],
deps: readonly unknown[] = [],
): ReadOnce {
const { manager } = useBusHost();
const names = useNames(registers);
const key = JSON.stringify(deps);
const [generation, setGeneration] = useState(0);
const [result, setResult] = useState<Omit<ReadOnce, "reload">>({
snapshot: undefined,
error: undefined,
});
useEffect(() => {
let live = true;
void manager.readOnce(id, names).then(
(snapshot) => {
if (live) setResult({ snapshot, error: undefined });
},
(e: unknown) => {
if (live) setResult({ snapshot: undefined, error: message(e) });
},
);
return () => {
live = false;
};
}, [manager, id, names, key, generation]);
const reload = useCallback(() => {
setGeneration((g) => g + 1);
}, []);
return { ...result, reload };
}

export interface Bus {
write: (id: number, register: string, value: Value) => Promise<void>;
command: <T>(fn: (client: OscClient) => Promise<T>) => Promise<T>;
exclusive: <T>(fn: (client: OscClient) => Promise<T>) => Promise<T>;
}

export function useBus(): Bus {
const { manager } = useBusHost();
return useMemo<Bus>(
() => ({
write: (id, register, value) => manager.write(id, register, value),
command: (fn) => manager.command(fn),
exclusive: (fn) => manager.exclusive(fn),
}),
[manager],
);
}

export function useBusStats(): BusStats {
const { manager } = useBusHost();
const [stats, setStats] = useState(() => manager.stats());
useEffect(
() =>
manager.onStats(() => {
setStats(manager.stats());
}),
[manager],
);
return stats;
}
Loading
Loading