Files
punktfunk/web/src/sections/Stats/Detail.tsx
T
enricobuehlerandClaude Opus 5 9b38b6a22d
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m4s
apple / swift (push) Successful in 5m41s
ci / bench (push) Successful in 7m17s
windows-host / package (push) Failing after 8m32s
windows-host / winget-source (push) Skipped
deb / build-publish-host (push) Successful in 11m4s
decky / build-publish (push) Successful in 20s
deb / build-publish (push) Successful in 11m33s
ci / rust-arm64 (push) Successful in 12m37s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
android / android (push) Successful in 12m59s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
docker / deploy-docs (push) Successful in 32s
deb / build-publish-client-arm64 (push) Successful in 8m45s
docker / build-push-arm64cross (push) Successful in 4m42s
arch / build-publish (push) Successful in 19m34s
ci / rust (push) Successful in 22m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m1s
release / apple (push) Successful in 31m52s
apple / screenshots (push) Successful in 23m26s
feat(stats): a capture records which encoder and GPU produced it
The stage split is the thing an fps-shortfall report is diagnosed from, and
it cannot be read without knowing the backend behind it: a p50 `submit` of
10 ms means "the GPU's CSC+encode throughput is the ceiling" on one backend
and something else entirely on another. The capture's meta carried the mode,
codec and client but neither the encoder nor the GPU, so every report so far
cost a round-trip asking which one it was.

Both come from `pf_gpu::active()` — the record the encoder open itself
writes — so they name the branch that really opened rather than a re-derived
guess that goes stale the moment a backend gains an internal fallback. Read
once per capture, not per frame.

The console shows them next to the mode in the recording's header. Both
fields are `serde(default)`, so a recording made before this still loads and
simply omits them.

Refs #9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:14:10 +02:00

89 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { X } from "lucide-react";
import type { FC } from "react";
import type { Capture } from "@/api/gen/model/capture";
import { useStatsRecordingGet } from "@/api/gen/stats/stats";
import { QueryState } from "@/components/query-state";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { Loadable } from "@/lib/query";
import { m } from "@/paraglide/messages";
import { HealthChart, LatencyChart, ThroughputChart } from "./charts";
import { ChartBlock } from "./helpers";
/** Container: the full graph set for the selected recording — fetched by id. */
export const DetailSection: FC<{ id: string; onClose: () => void }> = ({
id,
onClose,
}) => {
const detail = useStatsRecordingGet(id, { query: { enabled: !!id } });
return <DetailCard detail={detail} onClose={onClose} />;
};
/** Full graph set for one selected recording: latency (p99 toggle) + throughput + health. */
export const DetailCard: FC<{
detail: Loadable<Capture>;
onClose: () => void;
}> = ({ detail, onClose }) => {
const cap = detail.data;
const samples = cap?.samples ?? [];
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between gap-3">
<span>
{m.stats_detail_title()}
{cap && (
// Encoder + GPU ride along with the mode: the stage split below can't be
// read without knowing which backend produced it (a 10 ms `submit` means
// very different things on NVENC and on Vulkan). Older recordings predate
// the fields and simply omit them.
<span className="ml-2 text-sm font-normal text-muted-foreground">
{cap.meta.width}×{cap.meta.height}@{cap.meta.fps} ·{" "}
{cap.meta.codec.toUpperCase()}
{cap.meta.encoder_backend && ` · ${cap.meta.encoder_backend}`}
{cap.meta.gpu && ` · ${cap.meta.gpu}`}
</span>
)}
</span>
<Button
variant="ghost"
size="icon"
aria-label={m.stats_close()}
onClick={onClose}
>
<X className="size-4" />
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<QueryState
isLoading={detail.isLoading}
error={detail.error}
refetch={detail.refetch}
>
{samples.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{m.stats_no_samples()}
</p>
) : (
<div className="space-y-8">
<ChartBlock
title={m.stats_latency_title()}
desc={m.stats_latency_desc()}
>
<LatencyChart samples={samples} toggle />
</ChartBlock>
<ChartBlock title={m.stats_throughput_title()}>
<ThroughputChart samples={samples} />
</ChartBlock>
<ChartBlock title={m.stats_health_title()}>
<HealthChart samples={samples} kind={cap?.meta.kind} />
</ChartBlock>
</div>
)}
</QueryState>
</CardContent>
</Card>
);
};