Merge pull request #1807 from TomzxForks/requests-in-flight
feat(debug): add fetch requests-in-flight tracker with age percentiles
This commit is contained in:
@@ -7,6 +7,7 @@ import { Toaster } from '@/components/ui/sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
||||
import { setStreamPerfEnabled } from '@/stores/utils/streamDebug';
|
||||
import { setRequestsInFlightTrackingEnabled } from '@/stores/utils/requestsInFlight';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
// useEventStream removed — replaced by SyncProvider + SyncBridge
|
||||
import { useMenuActions } from '@/hooks/useMenuActions';
|
||||
@@ -279,6 +280,13 @@ function App({ apis }: AppProps) {
|
||||
};
|
||||
}, [showMemoryDebug]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setRequestsInFlightTrackingEnabled(showMemoryDebug);
|
||||
return () => {
|
||||
setRequestsInFlightTrackingEnabled(false);
|
||||
};
|
||||
}, [showMemoryDebug]);
|
||||
|
||||
React.useEffect(() => {
|
||||
applyMobileKeyboardMode(mobileKeyboardMode);
|
||||
}, [mobileKeyboardMode]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MEMORY_LIMITS } from '@/stores/types/sessionTypes';
|
||||
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||
import { getBackgroundTrimLimit } from '@/stores/types/sessionTypes';
|
||||
import { getStreamPerfSnapshot, getVsCodeStreamPerfSnapshot, resetStreamPerf, type StreamPerfSnapshot } from '@/stores/utils/streamDebug';
|
||||
import { getRequestsInFlightSnapshot, resetRequestsInFlight, type RequestsInFlightSnapshot } from '@/stores/utils/requestsInFlight';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
@@ -18,7 +19,7 @@ interface DebugPanelProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
type DebugTab = 'memory' | 'streaming';
|
||||
type DebugTab = 'memory' | 'streaming' | 'requests';
|
||||
|
||||
const formatDuration = (durationMs: number): string => {
|
||||
if (durationMs < 1000) {
|
||||
@@ -35,6 +36,10 @@ const formatDuration = (durationMs: number): string => {
|
||||
return `${minutes}m ${remainderSeconds}s`;
|
||||
};
|
||||
|
||||
// Fixed-width seconds format ("XX.XX s") for the percentile series so the
|
||||
// legend/labels don't jitter as values change. Pair with `tabular-nums`.
|
||||
const formatSeconds = (durationMs: number): string => `${(durationMs / 1000).toFixed(2)} s`;
|
||||
|
||||
const MetricCard: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => {
|
||||
return (
|
||||
<div
|
||||
@@ -99,7 +104,75 @@ const PerfSection: React.FC<{ title: string; snapshot: StreamPerfSnapshot; empty
|
||||
);
|
||||
};
|
||||
|
||||
const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
type LineSeries = { samples: number[]; color: string; filled?: boolean };
|
||||
|
||||
const LineChart: React.FC<{
|
||||
series: LineSeries[];
|
||||
peak: number;
|
||||
windowSeconds: number;
|
||||
ariaLabel: string;
|
||||
maxLabel: string;
|
||||
}> = ({ series, peak, windowSeconds, ariaLabel, maxLabel }) => {
|
||||
const width = windowSeconds;
|
||||
const height = 56;
|
||||
const padTop = 4;
|
||||
const n = series.reduce((max, s) => Math.max(max, s.samples.length), 0);
|
||||
const scale = peak > 0 ? (height - padTop) / peak : 0;
|
||||
const xFor = (i: number): number => width - n + i;
|
||||
const yFor = (v: number): number => height - v * scale;
|
||||
const baseline = height;
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<span className="pointer-events-none absolute left-0 top-0 typography-meta text-[var(--surface-muted-foreground)]">{maxLabel}</span>
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
preserveAspectRatio="none"
|
||||
className="h-14 w-full"
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<line
|
||||
x1={0}
|
||||
y1={baseline}
|
||||
x2={width}
|
||||
y2={baseline}
|
||||
stroke="var(--interactive-border)"
|
||||
strokeWidth={1}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
{series.map((s, si) => {
|
||||
const sn = s.samples.length;
|
||||
if (sn === 0) return null;
|
||||
const points = s.samples.map((v, i) => `${xFor(i)},${yFor(v).toFixed(2)}`);
|
||||
const linePath = `M ${points.join(' L ')}`;
|
||||
return (
|
||||
<React.Fragment key={si}>
|
||||
{s.filled ? (
|
||||
<path
|
||||
d={`M ${xFor(0)},${baseline} L ${points.join(' L ')} L ${xFor(sn - 1)},${baseline} Z`}
|
||||
fill={`color-mix(in srgb, ${s.color} 18%, transparent)`}
|
||||
stroke="none"
|
||||
/>
|
||||
) : null}
|
||||
<path
|
||||
d={linePath}
|
||||
fill="none"
|
||||
stroke={s.color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
const { t } = useI18n();
|
||||
const [activeTab, setActiveTab] = React.useState<DebugTab>('memory');
|
||||
const [copyState, setCopyState] = React.useState<'idle' | 'copied' | 'error'>('idle');
|
||||
@@ -110,6 +183,15 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
const totalGitHubRequests = useGitHubPrStatusStore((state) => state.totalRequestCount);
|
||||
const [streamSnapshot, setStreamSnapshot] = React.useState<StreamPerfSnapshot>(() => getStreamPerfSnapshot());
|
||||
const [vscodeStreamSnapshot, setVsCodeStreamSnapshot] = React.useState<StreamPerfSnapshot>(() => getVsCodeStreamPerfSnapshot());
|
||||
const [requestsSnapshot, setRequestsSnapshot] = React.useState<RequestsInFlightSnapshot>(() => getRequestsInFlightSnapshot());
|
||||
const ageLines = [
|
||||
{ label: 'p50', current: requestsSnapshot.ageP50, samples: requestsSnapshot.p50Samples, color: 'var(--status-success)' },
|
||||
{ label: 'p90', current: requestsSnapshot.ageP90, samples: requestsSnapshot.p90Samples, color: 'var(--status-info)' },
|
||||
{ label: 'p99', current: requestsSnapshot.ageP99, samples: requestsSnapshot.p99Samples, color: 'var(--status-warning)' },
|
||||
{ label: 'max', current: requestsSnapshot.ageMax, samples: requestsSnapshot.maxSamples, color: 'var(--status-error)' },
|
||||
];
|
||||
const countMax = requestsSnapshot.samples.reduce((m, v) => Math.max(m, v), 0);
|
||||
const percentileMax = ageLines.reduce((m, l) => l.samples.reduce((mm, v) => Math.max(mm, v), m), 0);
|
||||
const streamMetricCounts = React.useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
streamSnapshot.entries.forEach((entry) => {
|
||||
@@ -130,6 +212,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
const refresh = () => {
|
||||
setStreamSnapshot(getStreamPerfSnapshot());
|
||||
setVsCodeStreamSnapshot(getVsCodeStreamPerfSnapshot());
|
||||
setRequestsSnapshot(getRequestsInFlightSnapshot());
|
||||
};
|
||||
|
||||
refresh();
|
||||
@@ -218,11 +301,10 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeTab === 'memory' ? (
|
||||
<Icon name="database-2" className="h-4 w-4 text-[var(--surface-foreground)]" />
|
||||
) : (
|
||||
<Icon name="bar-chart-box" className="h-4 w-4 text-[var(--surface-foreground)]" />
|
||||
)}
|
||||
<Icon
|
||||
name={activeTab === 'memory' ? 'database-2' : activeTab === 'streaming' ? 'bar-chart-box' : 'pulse'}
|
||||
className="h-4 w-4 text-[var(--surface-foreground)]"
|
||||
/>
|
||||
<h3 className="typography-ui-label font-semibold text-[var(--surface-foreground)]">{t('memoryDebugPanel.title')}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -244,6 +326,18 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{activeTab === 'requests' ? (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
resetRequestsInFlight();
|
||||
setRequestsSnapshot(getRequestsInFlightSnapshot());
|
||||
}}
|
||||
>
|
||||
<Icon name="refresh" className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
{onClose ? (
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={onClose}>
|
||||
<Icon name="close" className="h-4 w-4" />
|
||||
@@ -272,6 +366,14 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
>
|
||||
{t('memoryDebugPanel.tabs.streaming')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={activeTab === 'requests' ? 'secondary' : 'ghost'}
|
||||
className="flex-1"
|
||||
onClick={() => setActiveTab('requests')}
|
||||
>
|
||||
{t('memoryDebugPanel.tabs.requests')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'memory' ? (
|
||||
@@ -366,7 +468,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
) : activeTab === 'streaming' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border border-[var(--interactive-border)] px-3 py-2 typography-meta text-[var(--surface-muted-foreground)]">
|
||||
<span>
|
||||
@@ -409,6 +511,70 @@ const DebugPanel: React.FC<DebugPanelProps> = ({ onClose }) => {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 typography-meta">
|
||||
<MetricCard label={t('memoryDebugPanel.requests.totalRequests')} value={`${requestsSnapshot.totalSettled} / ${requestsSnapshot.totalStarted}`} />
|
||||
<MetricCard
|
||||
label={t('memoryDebugPanel.requests.tracking')}
|
||||
value={requestsSnapshot.startedAt ? formatDuration(requestsSnapshot.durationMs) : t('memoryDebugPanel.common.idle')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{requestsSnapshot.samples.length === 0 ? (
|
||||
<div
|
||||
className="rounded-md p-3 typography-meta text-[var(--surface-muted-foreground)]"
|
||||
style={{ backgroundColor: 'color-mix(in srgb, var(--surface-muted) 45%, transparent)' }}
|
||||
>
|
||||
{t('memoryDebugPanel.requests.noSamples')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between typography-meta">
|
||||
<span className="text-[var(--surface-muted-foreground)]">{t('memoryDebugPanel.requests.inFlight')}</span>
|
||||
<span>
|
||||
<span className="font-medium text-[var(--surface-foreground)]">{requestsSnapshot.inFlight}</span>
|
||||
<span className="text-[var(--surface-muted-foreground)]"> · {t('memoryDebugPanel.requests.peak')} </span>
|
||||
<span className="font-medium text-[var(--surface-foreground)]">{requestsSnapshot.peak}</span>
|
||||
</span>
|
||||
</div>
|
||||
<LineChart
|
||||
series={[{ samples: requestsSnapshot.samples, color: 'var(--status-info)', filled: true }]}
|
||||
peak={countMax}
|
||||
windowSeconds={requestsSnapshot.windowSeconds}
|
||||
ariaLabel={t('memoryDebugPanel.requests.chartLabel', { peak: requestsSnapshot.peak })}
|
||||
maxLabel={`${countMax}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between typography-meta">
|
||||
<span className="text-[var(--surface-muted-foreground)]">{t('memoryDebugPanel.requests.duration')}</span>
|
||||
<span className="font-medium tabular-nums text-[var(--surface-foreground)]">{formatSeconds(requestsSnapshot.peakAgeMs)}</span>
|
||||
</div>
|
||||
<LineChart
|
||||
series={ageLines.map((line) => ({ samples: line.samples, color: line.color }))}
|
||||
peak={percentileMax}
|
||||
windowSeconds={requestsSnapshot.windowSeconds}
|
||||
ariaLabel={t('memoryDebugPanel.requests.percentileChartLabel')}
|
||||
maxLabel={formatSeconds(percentileMax)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 typography-meta">
|
||||
{ageLines.map((line) => (
|
||||
<span key={line.label} className="flex items-center gap-1">
|
||||
<span className="inline-block h-2 w-2 rounded-full" style={{ backgroundColor: line.color }} />
|
||||
<span className="text-[var(--surface-muted-foreground)]">{line.label}</span>
|
||||
<span className="font-medium tabular-nums text-[var(--surface-foreground)]">{formatSeconds(line.current)}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between typography-meta text-[var(--surface-muted-foreground)]">
|
||||
<span>{t('memoryDebugPanel.requests.windowHint', { seconds: requestsSnapshot.windowSeconds })}</span>
|
||||
<span>{t('memoryDebugPanel.requests.now')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3011,6 +3011,7 @@ export const dict = {
|
||||
'memoryDebugPanel.title': 'Debug Panel',
|
||||
'memoryDebugPanel.tabs.memory': 'Memory',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Requests',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'Sessions in Memory',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI Streaming Metrics',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code Bridge Metrics',
|
||||
@@ -3048,6 +3049,16 @@ export const dict = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Streaming debug JSON copied',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Failed to copy JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'Copy exports both UI and VS Code streaming metrics as JSON',
|
||||
'memoryDebugPanel.requests.inFlight': 'In flight',
|
||||
'memoryDebugPanel.requests.peak': 'Peak',
|
||||
'memoryDebugPanel.requests.duration': 'Duration',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Total Requests',
|
||||
'memoryDebugPanel.requests.tracking': 'Tracking',
|
||||
'memoryDebugPanel.requests.now': 'now',
|
||||
'memoryDebugPanel.requests.noSamples': 'No requests tracked yet. Keep this panel open to record fetch activity.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Fetch requests in flight over time, peak {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': 'last {seconds}s',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'In-flight request age percentiles (p50, p90, p99, max) over time',
|
||||
'memoryDebugPanel.common.idle': 'idle',
|
||||
'memoryDebugPanel.common.live': 'live',
|
||||
'memoryDebugPanel.common.notAvailable': 'n/a',
|
||||
|
||||
@@ -2977,6 +2977,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Panel de depuración",
|
||||
"memoryDebugPanel.tabs.memory": "Memoria",
|
||||
"memoryDebugPanel.tabs.streaming": "Transmisión",
|
||||
"memoryDebugPanel.tabs.requests": "Solicitudes",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Sesiones en memoria",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Métricas de streaming de UI",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Métricas del puente de VS Code",
|
||||
@@ -3014,6 +3015,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "JSON de depuración en streaming copiado",
|
||||
"memoryDebugPanel.streaming.copy.failed": "No se pudo copiar JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Copia exportaciones de métricas de UI como de métricas de VS Code en formato JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "En curso",
|
||||
"memoryDebugPanel.requests.peak": "Pico",
|
||||
"memoryDebugPanel.requests.duration": "Duración",
|
||||
"memoryDebugPanel.requests.totalRequests": "Solicitudes totales",
|
||||
"memoryDebugPanel.requests.tracking": "Seguimiento",
|
||||
"memoryDebugPanel.requests.now": "ahora",
|
||||
"memoryDebugPanel.requests.noSamples": "Aún no se han registrado solicitudes. Mantén este panel abierto para registrar la actividad de fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Solicitudes fetch en curso a lo largo del tiempo, pico {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "últimos {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Percentiles de antigüedad de solicitudes en curso (p50, p90, p99, máx) a lo largo del tiempo",
|
||||
"memoryDebugPanel.common.idle": "inactivo",
|
||||
"memoryDebugPanel.common.live": "en vivo",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
|
||||
@@ -2703,6 +2703,7 @@ export const dict = {
|
||||
'memoryDebugPanel.title': 'Panneau de débogage',
|
||||
'memoryDebugPanel.tabs.memory': 'Mémoire',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Requêtes',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'Sessions en mémoire',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'Métriques de streaming de l\'interface utilisateur',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'Métriques du pont VS Code',
|
||||
@@ -2740,6 +2741,16 @@ export const dict = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Débogage en streaming JSON copié',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Échec de la copie de JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'La copie exporte les métriques de streaming de l\'interface utilisateur et de VS Code en tant que JSON.',
|
||||
'memoryDebugPanel.requests.inFlight': 'En cours',
|
||||
'memoryDebugPanel.requests.peak': 'Pic',
|
||||
'memoryDebugPanel.requests.duration': 'Durée',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Requêtes totales',
|
||||
'memoryDebugPanel.requests.tracking': 'Suivi',
|
||||
'memoryDebugPanel.requests.now': 'maintenant',
|
||||
'memoryDebugPanel.requests.noSamples': 'Aucune requête enregistrée. Gardez ce panneau ouvert pour enregistrer l\'activité fetch.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Requêtes fetch en cours dans le temps, pic {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '{seconds}s dernières',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'Percentiles d\'âge des requêtes en cours (p50, p90, p99, max) dans le temps',
|
||||
'memoryDebugPanel.common.idle': 'inactif',
|
||||
'memoryDebugPanel.common.live': 'en direct',
|
||||
'memoryDebugPanel.common.notAvailable': 'n / A',
|
||||
|
||||
@@ -3007,6 +3007,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': 'デバッグパネル',
|
||||
'memoryDebugPanel.tabs.memory': 'メモリ',
|
||||
'memoryDebugPanel.tabs.streaming': 'ストリーミング',
|
||||
'memoryDebugPanel.tabs.requests': 'リクエスト',
|
||||
'memoryDebugPanel.section.sessionsInMemory': 'メモリ内のセッション',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UIストリーミングメトリクス',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Codeブリッジメトリクス',
|
||||
@@ -3044,6 +3045,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'ストリーミングデバッグJSONをコピーしました',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'JSONのコピーに失敗しました',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'UIとVS Codeの両方のストリーミングメトリクスをJSONとしてエクスポートします',
|
||||
'memoryDebugPanel.requests.inFlight': '実行中',
|
||||
'memoryDebugPanel.requests.peak': 'ピーク',
|
||||
'memoryDebugPanel.requests.duration': '期間',
|
||||
'memoryDebugPanel.requests.totalRequests': '合計リクエスト',
|
||||
'memoryDebugPanel.requests.tracking': 'トラッキング',
|
||||
'memoryDebugPanel.requests.now': '現在',
|
||||
'memoryDebugPanel.requests.noSamples': 'まだリクエストが記録されていません。fetchアクティビティを記録するには、このパネルを開いたままにしてください。',
|
||||
'memoryDebugPanel.requests.chartLabel': '経時的な実行中fetchリクエスト、ピーク {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '過去 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '経時的な実行中リクエストの経過時間パーセンタイル(p50、p90、p99、最大)',
|
||||
'memoryDebugPanel.common.idle': '待機中',
|
||||
'memoryDebugPanel.common.live': 'ライブ',
|
||||
'memoryDebugPanel.common.notAvailable': 'N/A',
|
||||
|
||||
@@ -3011,6 +3011,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '디버그 패널',
|
||||
'memoryDebugPanel.tabs.memory': '메모리',
|
||||
'memoryDebugPanel.tabs.streaming': '스트리밍',
|
||||
'memoryDebugPanel.tabs.requests': '요청',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '메모리 내 세션',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 스트리밍 지표',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 브리지 지표',
|
||||
@@ -3048,6 +3049,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '스트리밍 디버그 JSON 복사 완료',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'JSON 복사 실패',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'UI와 VS Code 스트리밍 메트릭을 JSON으로 복사합니다',
|
||||
'memoryDebugPanel.requests.inFlight': '진행 중',
|
||||
'memoryDebugPanel.requests.peak': '최대',
|
||||
'memoryDebugPanel.requests.duration': '지속 시간',
|
||||
'memoryDebugPanel.requests.totalRequests': '전체 요청',
|
||||
'memoryDebugPanel.requests.tracking': '추적 중',
|
||||
'memoryDebugPanel.requests.now': '현재',
|
||||
'memoryDebugPanel.requests.noSamples': '아직 기록된 요청이 없습니다. fetch 활동을 기록하려면 이 패널을 열어 두세요.',
|
||||
'memoryDebugPanel.requests.chartLabel': '시간에 따른 진행 중인 fetch 요청, 최대 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '최근 {seconds}초',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '진행 중 요청 수명 백분위수(p50, p90, p99, max)의 시간별 변화',
|
||||
'memoryDebugPanel.common.idle': '유휴',
|
||||
'memoryDebugPanel.common.live': '실시간',
|
||||
'memoryDebugPanel.common.notAvailable': 'n/a',
|
||||
|
||||
@@ -2569,8 +2569,19 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': 'Skopiowano JSON debugowania streamingu',
|
||||
'memoryDebugPanel.streaming.copy.failed': 'Nie udało się skopiować JSON',
|
||||
'memoryDebugPanel.streaming.copy.hint': 'Kopiowanie eksportuje metryki streamingu zarówno UI, jak i VS Code w formacie JSON',
|
||||
'memoryDebugPanel.requests.inFlight': 'W trakcie',
|
||||
'memoryDebugPanel.requests.peak': 'Szczyt',
|
||||
'memoryDebugPanel.requests.duration': 'Czas trwania',
|
||||
'memoryDebugPanel.requests.totalRequests': 'Łączne żądania',
|
||||
'memoryDebugPanel.requests.tracking': 'Śledzenie',
|
||||
'memoryDebugPanel.requests.now': 'teraz',
|
||||
'memoryDebugPanel.requests.noSamples': 'Brak żądań. Utrzymuj ten panel otwarty, aby rejestrować aktywność fetch.',
|
||||
'memoryDebugPanel.requests.chartLabel': 'Żądania fetch w trakcie w czasie, szczyt {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': 'ostatnie {seconds}s',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': 'Percentyle wieku żądań w trakcie (p50, p90, p99, max) w czasie',
|
||||
'memoryDebugPanel.tabs.memory': 'Pamięć',
|
||||
'memoryDebugPanel.tabs.streaming': 'Streaming',
|
||||
'memoryDebugPanel.tabs.requests': 'Żądania',
|
||||
'memoryDebugPanel.title': 'Panel debugowania',
|
||||
'memoryDebugPanel.tooltip.logCurrentState': 'Zaloguj bieżący stan pamięci do konsoli przeglądarki',
|
||||
'openChamberLogo.aria.logo': 'Logo OpenChamber',
|
||||
|
||||
@@ -2977,6 +2977,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Painel de depuração",
|
||||
"memoryDebugPanel.tabs.memory": "Memória",
|
||||
"memoryDebugPanel.tabs.streaming": "Transmissão",
|
||||
"memoryDebugPanel.tabs.requests": "Solicitações",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Sessões em memória",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Métricas de streaming de UI",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Métricas da ponte do VS Code",
|
||||
@@ -3014,6 +3015,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "JSON de depuração em streaming copiado",
|
||||
"memoryDebugPanel.streaming.copy.failed": "Não foi possível copiar JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Copia exportações de métricas da UI e do VS Code em formato JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "Em curso",
|
||||
"memoryDebugPanel.requests.peak": "Pico",
|
||||
"memoryDebugPanel.requests.duration": "Duração",
|
||||
"memoryDebugPanel.requests.totalRequests": "Solicitações totais",
|
||||
"memoryDebugPanel.requests.tracking": "Rastreamento",
|
||||
"memoryDebugPanel.requests.now": "agora",
|
||||
"memoryDebugPanel.requests.noSamples": "Nenhuma solicitação registrada. Mantenha este painel aberto para registrar a atividade de fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Solicitações fetch em curso ao longo do tempo, pico {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "últimos {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Percentis de idade das solicitações em curso (p50, p90, p99, máx) ao longo do tempo",
|
||||
"memoryDebugPanel.common.idle": "inativo",
|
||||
"memoryDebugPanel.common.live": "ao vivo",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
|
||||
@@ -2977,6 +2977,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.title": "Панель налагодження",
|
||||
"memoryDebugPanel.tabs.memory": "Пам'ять",
|
||||
"memoryDebugPanel.tabs.streaming": "Потокове передавання",
|
||||
"memoryDebugPanel.tabs.requests": "Запити",
|
||||
"memoryDebugPanel.section.sessionsInMemory": "Сесії в пам'яті",
|
||||
"memoryDebugPanel.section.uiStreamingMetrics": "Потокові показники інтерфейсу користувача",
|
||||
"memoryDebugPanel.section.vscodeBridgeMetrics": "Метрики мосту VS Code",
|
||||
@@ -3014,6 +3015,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
"memoryDebugPanel.streaming.copy.copied": "Потокове налагодження JSON скопійовано",
|
||||
"memoryDebugPanel.streaming.copy.failed": "Не вдалося скопіювати JSON",
|
||||
"memoryDebugPanel.streaming.copy.hint": "Копіювання експортує метрики потокового інтерфейсу користувача та VS Code як JSON",
|
||||
"memoryDebugPanel.requests.inFlight": "Виконуються",
|
||||
"memoryDebugPanel.requests.peak": "Пік",
|
||||
"memoryDebugPanel.requests.duration": "Тривалість",
|
||||
"memoryDebugPanel.requests.totalRequests": "Усього запитів",
|
||||
"memoryDebugPanel.requests.tracking": "Відстеження",
|
||||
"memoryDebugPanel.requests.now": "зараз",
|
||||
"memoryDebugPanel.requests.noSamples": "Запитів ще немає. Тримайте цю панель відкритою, щоб фіксувати активність fetch.",
|
||||
"memoryDebugPanel.requests.chartLabel": "Запити fetch у виконанні з часом, пік {peak}",
|
||||
"memoryDebugPanel.requests.windowHint": "останні {seconds}s",
|
||||
"memoryDebugPanel.requests.percentileChartLabel": "Перцентилі віку запитів у виконанні (p50, p90, p99, max) з часом",
|
||||
"memoryDebugPanel.common.idle": "очікування",
|
||||
"memoryDebugPanel.common.live": "live",
|
||||
"memoryDebugPanel.common.notAvailable": "n/a",
|
||||
|
||||
@@ -2977,6 +2977,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '调试面板',
|
||||
'memoryDebugPanel.tabs.memory': '内存',
|
||||
'memoryDebugPanel.tabs.streaming': '流式',
|
||||
'memoryDebugPanel.tabs.requests': '请求',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '内存中的会话',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 流式指标',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 桥接指标',
|
||||
@@ -3014,6 +3015,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '流式调试 JSON 已复制',
|
||||
'memoryDebugPanel.streaming.copy.failed': '复制 JSON 失败',
|
||||
'memoryDebugPanel.streaming.copy.hint': '复制会导出 UI 与 VS Code 的流式指标 JSON',
|
||||
'memoryDebugPanel.requests.inFlight': '进行中',
|
||||
'memoryDebugPanel.requests.peak': '峰值',
|
||||
'memoryDebugPanel.requests.duration': '时长',
|
||||
'memoryDebugPanel.requests.totalRequests': '请求总数',
|
||||
'memoryDebugPanel.requests.tracking': '跟踪',
|
||||
'memoryDebugPanel.requests.now': '当前',
|
||||
'memoryDebugPanel.requests.noSamples': '尚未记录请求。保持此面板打开以记录 fetch 活动。',
|
||||
'memoryDebugPanel.requests.chartLabel': '随时间变化的进行中 fetch 请求,峰值 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '最近 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '进行中请求年龄百分位(p50、p90、p99、最大值)随时间的变化',
|
||||
'memoryDebugPanel.common.idle': '空闲',
|
||||
'memoryDebugPanel.common.live': '实时',
|
||||
'memoryDebugPanel.common.notAvailable': '无',
|
||||
|
||||
@@ -2974,6 +2974,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.title': '偵錯面板',
|
||||
'memoryDebugPanel.tabs.memory': '記憶體',
|
||||
'memoryDebugPanel.tabs.streaming': '串流',
|
||||
'memoryDebugPanel.tabs.requests': '請求',
|
||||
'memoryDebugPanel.section.sessionsInMemory': '記憶體中的會話',
|
||||
'memoryDebugPanel.section.uiStreamingMetrics': 'UI 串流指標',
|
||||
'memoryDebugPanel.section.vscodeBridgeMetrics': 'VS Code 橋接指標',
|
||||
@@ -3011,6 +3012,16 @@ export const dict: Record<I18nKey, string> = {
|
||||
'memoryDebugPanel.streaming.copy.copied': '串流偵錯 JSON 已複製',
|
||||
'memoryDebugPanel.streaming.copy.failed': '複製 JSON 失敗',
|
||||
'memoryDebugPanel.streaming.copy.hint': '複製會匯出 UI 與 VS Code 的串流指標 JSON',
|
||||
'memoryDebugPanel.requests.inFlight': '進行中',
|
||||
'memoryDebugPanel.requests.peak': '峰值',
|
||||
'memoryDebugPanel.requests.duration': '時長',
|
||||
'memoryDebugPanel.requests.totalRequests': '請求總數',
|
||||
'memoryDebugPanel.requests.tracking': '追蹤',
|
||||
'memoryDebugPanel.requests.now': '目前',
|
||||
'memoryDebugPanel.requests.noSamples': '尚未記錄請求。保持此面板開啟以記錄 fetch 活動。',
|
||||
'memoryDebugPanel.requests.chartLabel': '隨時間變化的進行中 fetch 請求,峰值 {peak}',
|
||||
'memoryDebugPanel.requests.windowHint': '最近 {seconds}秒',
|
||||
'memoryDebugPanel.requests.percentileChartLabel': '進行中請求年齡百分位(p50、p90、p99、最大值)隨時間的變化',
|
||||
'memoryDebugPanel.common.idle': '閒置',
|
||||
'memoryDebugPanel.common.live': '即時',
|
||||
'memoryDebugPanel.common.notAvailable': '無',
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// Tracks every fetch() request as "in flight" from call to promise settle,
|
||||
// samples two series once per second, and keeps a 5-minute rolling window for
|
||||
// plotting:
|
||||
// 1. in-flight request count
|
||||
// 2. percentile distribution of currently in-flight request ages: p50, p90,
|
||||
// p99, max (ms since each unsettled fetch started; 0 when nothing is in
|
||||
// flight)
|
||||
// Mirrors the streamDebug.ts pattern: collection is gated behind an
|
||||
// enable/disable toggle (driven by the debug panel), state lives on `window`
|
||||
// to survive HMR, and the UI polls a serializable snapshot instead of
|
||||
// subscribing to a store (this is high-frequency debug data, see stores docs).
|
||||
|
||||
const STORAGE_KEY = 'openchamber_requests_in_flight';
|
||||
const SAMPLE_INTERVAL_MS = 1000;
|
||||
const WINDOW_MS = 5 * 60 * 1000;
|
||||
const MAX_SAMPLES = Math.ceil(WINDOW_MS / SAMPLE_INTERVAL_MS);
|
||||
|
||||
type RequestsInFlightState = {
|
||||
enabled: boolean;
|
||||
startedAt: number;
|
||||
inFlight: number;
|
||||
peak: number;
|
||||
totalStarted: number;
|
||||
totalSettled: number;
|
||||
samples: number[];
|
||||
p50Samples: number[];
|
||||
p90Samples: number[];
|
||||
p99Samples: number[];
|
||||
maxSamples: number[];
|
||||
peakAgeMs: number;
|
||||
inFlightStarts: Map<number, number>;
|
||||
sampleCount: number;
|
||||
lastSampleAt: number | null;
|
||||
fetchWrapped: boolean;
|
||||
originalFetch: typeof window.fetch | null;
|
||||
sampleTimer: number | null;
|
||||
};
|
||||
|
||||
export type RequestsInFlightSnapshot = {
|
||||
enabled: boolean;
|
||||
startedAt: number | null;
|
||||
durationMs: number;
|
||||
inFlight: number;
|
||||
peak: number;
|
||||
totalStarted: number;
|
||||
totalSettled: number;
|
||||
samples: number[];
|
||||
ageP50: number;
|
||||
ageP90: number;
|
||||
ageP99: number;
|
||||
ageMax: number;
|
||||
peakAgeMs: number;
|
||||
p50Samples: number[];
|
||||
p90Samples: number[];
|
||||
p99Samples: number[];
|
||||
maxSamples: number[];
|
||||
sampleCount: number;
|
||||
lastSampleAt: number | null;
|
||||
windowSeconds: number;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__openchamberRequestsInFlight__?: RequestsInFlightState;
|
||||
}
|
||||
}
|
||||
|
||||
export const requestsInFlightEnabled = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
return window.localStorage.getItem(STORAGE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const createState = (): RequestsInFlightState => {
|
||||
const startedAt = Date.now();
|
||||
return {
|
||||
enabled: true,
|
||||
startedAt,
|
||||
inFlight: 0,
|
||||
peak: 0,
|
||||
totalStarted: 0,
|
||||
totalSettled: 0,
|
||||
samples: [],
|
||||
p50Samples: [],
|
||||
p90Samples: [],
|
||||
p99Samples: [],
|
||||
maxSamples: [],
|
||||
peakAgeMs: 0,
|
||||
inFlightStarts: new Map<number, number>(),
|
||||
sampleCount: 0,
|
||||
lastSampleAt: null,
|
||||
fetchWrapped: false,
|
||||
originalFetch: null,
|
||||
sampleTimer: null,
|
||||
};
|
||||
};
|
||||
|
||||
let nextRequestId = 1;
|
||||
|
||||
const recordStart = (id: number, startMs: number): void => {
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || !state.enabled) return;
|
||||
state.inFlight += 1;
|
||||
state.totalStarted += 1;
|
||||
if (state.inFlight > state.peak) state.peak = state.inFlight;
|
||||
state.inFlightStarts.set(id, startMs);
|
||||
};
|
||||
|
||||
const recordSettle = (id: number): void => {
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || !state.enabled) return;
|
||||
state.inFlight = Math.max(0, state.inFlight - 1);
|
||||
state.totalSettled += 1;
|
||||
state.inFlightStarts.delete(id);
|
||||
};
|
||||
|
||||
// Sorted ages (ms) of every currently in-flight request. Empty when nothing
|
||||
// is in flight. Used both for live snapshot reporting and per-second sampling.
|
||||
const currentAges = (state: RequestsInFlightState): number[] => {
|
||||
if (state.inFlightStarts.size === 0) return [];
|
||||
const now = Date.now();
|
||||
const ages: number[] = [];
|
||||
for (const start of state.inFlightStarts.values()) {
|
||||
ages.push(Math.max(0, now - start));
|
||||
}
|
||||
ages.sort((a, b) => a - b);
|
||||
return ages;
|
||||
};
|
||||
|
||||
// Linear-interpolation percentile of a pre-sorted array.
|
||||
const percentile = (sorted: number[], p: number): number => {
|
||||
const n = sorted.length;
|
||||
if (n === 0) return 0;
|
||||
if (n === 1) return sorted[0];
|
||||
const rank = (p / 100) * (n - 1);
|
||||
const lo = Math.floor(rank);
|
||||
const hi = Math.ceil(rank);
|
||||
if (lo === hi) return sorted[lo];
|
||||
return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo);
|
||||
};
|
||||
|
||||
const installFetchTracker = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || state.fetchWrapped) return;
|
||||
const original = window.fetch.bind(window);
|
||||
state.originalFetch = original;
|
||||
state.fetchWrapped = true;
|
||||
const tracker = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const id = nextRequestId++;
|
||||
recordStart(id, Date.now());
|
||||
try {
|
||||
return await original(input, init);
|
||||
} finally {
|
||||
recordSettle(id);
|
||||
}
|
||||
};
|
||||
window.fetch = tracker as typeof window.fetch;
|
||||
};
|
||||
|
||||
const uninstallFetchTracker = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || !state.fetchWrapped || !state.originalFetch) return;
|
||||
window.fetch = state.originalFetch;
|
||||
state.fetchWrapped = false;
|
||||
state.originalFetch = null;
|
||||
};
|
||||
|
||||
const trimSamples = (arr: number[]): void => {
|
||||
if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES);
|
||||
};
|
||||
|
||||
const pushSample = (): void => {
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || !state.enabled) return;
|
||||
state.samples.push(state.inFlight);
|
||||
const ages = currentAges(state);
|
||||
const mx = ages.length > 0 ? ages[ages.length - 1] : 0;
|
||||
state.p50Samples.push(percentile(ages, 50));
|
||||
state.p90Samples.push(percentile(ages, 90));
|
||||
state.p99Samples.push(percentile(ages, 99));
|
||||
state.maxSamples.push(mx);
|
||||
if (mx > state.peakAgeMs) state.peakAgeMs = mx;
|
||||
state.sampleCount += 1;
|
||||
trimSamples(state.samples);
|
||||
trimSamples(state.p50Samples);
|
||||
trimSamples(state.p90Samples);
|
||||
trimSamples(state.p99Samples);
|
||||
trimSamples(state.maxSamples);
|
||||
state.lastSampleAt = Date.now();
|
||||
};
|
||||
|
||||
const startSampling = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || state.sampleTimer != null) return;
|
||||
state.sampleTimer = window.setInterval(pushSample, SAMPLE_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const stopSampling = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state || state.sampleTimer == null) return;
|
||||
window.clearInterval(state.sampleTimer);
|
||||
state.sampleTimer = null;
|
||||
};
|
||||
|
||||
export const setRequestsInFlightTrackingEnabled = (enabled: boolean): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
// Idempotent: tear down any prior tracking first so a repeated
|
||||
// enable can never wrap window.fetch twice (which would double-count).
|
||||
stopSampling();
|
||||
uninstallFetchTracker();
|
||||
window.localStorage.setItem(STORAGE_KEY, '1');
|
||||
window.__openchamberRequestsInFlight__ = createState();
|
||||
installFetchTracker();
|
||||
startSampling();
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
stopSampling();
|
||||
uninstallFetchTracker();
|
||||
delete window.__openchamberRequestsInFlight__;
|
||||
} catch {
|
||||
// ignore storage failures in debug helper
|
||||
}
|
||||
};
|
||||
|
||||
export const resetRequestsInFlight = (): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!state) return;
|
||||
const fresh = createState();
|
||||
state.startedAt = fresh.startedAt;
|
||||
state.inFlight = fresh.inFlight;
|
||||
state.peak = fresh.peak;
|
||||
state.totalStarted = fresh.totalStarted;
|
||||
state.totalSettled = fresh.totalSettled;
|
||||
state.samples = fresh.samples;
|
||||
state.p50Samples = fresh.p50Samples;
|
||||
state.p90Samples = fresh.p90Samples;
|
||||
state.p99Samples = fresh.p99Samples;
|
||||
state.maxSamples = fresh.maxSamples;
|
||||
state.peakAgeMs = fresh.peakAgeMs;
|
||||
state.inFlightStarts = fresh.inFlightStarts;
|
||||
state.sampleCount = fresh.sampleCount;
|
||||
state.lastSampleAt = fresh.lastSampleAt;
|
||||
};
|
||||
|
||||
export const getRequestsInFlightSnapshot = (): RequestsInFlightSnapshot => {
|
||||
if (typeof window === 'undefined') {
|
||||
return emptySnapshot();
|
||||
}
|
||||
|
||||
const state = window.__openchamberRequestsInFlight__;
|
||||
if (!requestsInFlightEnabled() || !state) {
|
||||
return emptySnapshot();
|
||||
}
|
||||
|
||||
const ages = currentAges(state);
|
||||
return {
|
||||
enabled: true,
|
||||
startedAt: state.startedAt,
|
||||
durationMs: Math.max(0, Date.now() - state.startedAt),
|
||||
inFlight: state.inFlight,
|
||||
peak: state.peak,
|
||||
totalStarted: state.totalStarted,
|
||||
totalSettled: state.totalSettled,
|
||||
samples: state.samples.slice(),
|
||||
ageP50: percentile(ages, 50),
|
||||
ageP90: percentile(ages, 90),
|
||||
ageP99: percentile(ages, 99),
|
||||
ageMax: ages.length > 0 ? ages[ages.length - 1] : 0,
|
||||
peakAgeMs: state.peakAgeMs,
|
||||
p50Samples: state.p50Samples.slice(),
|
||||
p90Samples: state.p90Samples.slice(),
|
||||
p99Samples: state.p99Samples.slice(),
|
||||
maxSamples: state.maxSamples.slice(),
|
||||
sampleCount: state.sampleCount,
|
||||
lastSampleAt: state.lastSampleAt,
|
||||
windowSeconds: MAX_SAMPLES,
|
||||
};
|
||||
};
|
||||
|
||||
const emptySnapshot = (): RequestsInFlightSnapshot => ({
|
||||
enabled: false,
|
||||
startedAt: null,
|
||||
durationMs: 0,
|
||||
inFlight: 0,
|
||||
peak: 0,
|
||||
totalStarted: 0,
|
||||
totalSettled: 0,
|
||||
samples: [],
|
||||
ageP50: 0,
|
||||
ageP90: 0,
|
||||
ageP99: 0,
|
||||
ageMax: 0,
|
||||
peakAgeMs: 0,
|
||||
p50Samples: [],
|
||||
p90Samples: [],
|
||||
p99Samples: [],
|
||||
maxSamples: [],
|
||||
sampleCount: 0,
|
||||
lastSampleAt: null,
|
||||
windowSeconds: MAX_SAMPLES,
|
||||
});
|
||||
Reference in New Issue
Block a user