From 741c19e840a575d0346f072fc4219199de84c2e1 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Tue, 28 Apr 2026 22:56:43 +0800 Subject: [PATCH] feat: display 1% low FPS in presets, stats API, and game details --- app/game/[id]/game-page-client.tsx | 24 +++++++++++++++++++++--- app/game/[id]/page.tsx | 2 ++ app/game/[id]/preset-detail-modal.tsx | 9 +++++++++ lib/api/game-stats.ts | 16 ++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx index a6558f3..1f08d4d 100644 --- a/app/game/[id]/game-page-client.tsx +++ b/app/game/[id]/game-page-client.tsx @@ -15,6 +15,7 @@ import { Plus, ThumbsUpIcon, ThumbsDownIcon, + GaugeIcon, } from "lucide-react" import Link from "next/link" import { useSession } from "@/lib/auth-client" @@ -81,6 +82,7 @@ interface Preset { fpsAvg: number | null fpsLow: number | null fpsHigh: number | null + fpsOnePercentLow: number | null upscalerType: string | null upscalerVersion: string | null frameGenMethod: string | null @@ -102,6 +104,8 @@ interface StatsResponse { bestDevice: string verifiedCount: number versionCount: number + avgStability: number | null + bestOnePercentLow: number | null } isRawPerformer: boolean isPoorPerformance: boolean @@ -111,6 +115,7 @@ interface StatsResponse { min: number q1: number median: number + onePercentLow?: number q3: number max: number }> @@ -136,6 +141,7 @@ interface StatsResponse { fpsLow: number fpsAvg: number fpsHigh: number + fpsOnePercentLow: number | null isRawPerformer: boolean }> deviceBreakdown: Array<{ @@ -840,11 +846,16 @@ export function GamePageClient({ {" "} avg - {preset.fpsLow !== null && ( + {preset.fpsOnePercentLow !== null && ( {" "} - ({preset.fpsLow}– - {preset.fpsHigh}) + · {preset.fpsOnePercentLow} 1% low + + )} + {preset.fpsLow !== null && preset.fpsHigh !== null && !preset.fpsOnePercentLow && ( + + {" "} + ({preset.fpsLow}–{preset.fpsHigh}) )} @@ -953,6 +964,13 @@ export function GamePageClient({ )} icon={DatabaseIcon} /> + {filteredStats.summary.avgStability !== null && ( + + )} )} diff --git a/app/game/[id]/page.tsx b/app/game/[id]/page.tsx index 429ae05..4c957c7 100644 --- a/app/game/[id]/page.tsx +++ b/app/game/[id]/page.tsx @@ -219,6 +219,7 @@ export default async function GamePage({ fpsAvg: performanceEntries.fpsAvg, fpsLow: performanceEntries.fpsLow, fpsHigh: performanceEntries.fpsHigh, + fpsOnePercentLow: performanceEntries.fpsOnePercentLow, upscalerType: performanceEntries.upscalerType, upscalerVersion: performanceEntries.upscalerVersion, frameGenMethod: performanceEntries.frameGenMethod, @@ -292,6 +293,7 @@ export default async function GamePage({ fpsAvg: p.fpsAvg, fpsLow: p.fpsLow, fpsHigh: p.fpsHigh, + fpsOnePercentLow: p.fpsOnePercentLow ?? null, upscalerType: p.upscalerType, upscalerVersion: p.upscalerVersion, frameGenMethod: p.frameGenMethod, diff --git a/app/game/[id]/preset-detail-modal.tsx b/app/game/[id]/preset-detail-modal.tsx index de50faf..5209d4a 100644 --- a/app/game/[id]/preset-detail-modal.tsx +++ b/app/game/[id]/preset-detail-modal.tsx @@ -33,6 +33,7 @@ interface Preset { fpsAvg: number | null fpsLow: number | null fpsHigh: number | null + fpsOnePercentLow: number | null upscalerType: string | null upscalerVersion: string | null frameGenMethod: string | null @@ -295,6 +296,14 @@ export function PresetDetailModal({ )} + {preset.fpsOnePercentLow !== null && ( +
+ 1% Low FPS + + {preset.fpsOnePercentLow} fps + +
+ )}
diff --git a/lib/api/game-stats.ts b/lib/api/game-stats.ts index 82e154b..3c6a0c6 100644 --- a/lib/api/game-stats.ts +++ b/lib/api/game-stats.ts @@ -34,6 +34,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( fpsAvg: performanceEntries.fpsAvg, fpsLow: performanceEntries.fpsLow, fpsHigh: performanceEntries.fpsHigh, + fpsOnePercentLow: performanceEntries.fpsOnePercentLow, upscalerType: performanceEntries.upscalerType, upscalerVersion: performanceEntries.upscalerVersion, frameGenMethod: performanceEntries.frameGenMethod, @@ -140,12 +141,14 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( const q1Idx = Math.floor(n * 0.25) const medIdx = Math.floor(n * 0.5) const q3Idx = Math.floor(n * 0.75) + const onePercentLow = sorted.length > 0 ? sorted[Math.max(0, Math.floor(sorted.length * 0.01))] : sorted[0] return { hardwareSlug: slug, hardwareName, min: sorted[0], q1: sorted[q1Idx], median: sorted[medIdx], + onePercentLow, q3: sorted[q3Idx], max: sorted[n - 1], } @@ -222,6 +225,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( fpsLow: e.fpsLow!, fpsAvg: e.fpsAvg ?? 0, fpsHigh: e.fpsHigh!, + fpsOnePercentLow: e.fpsOnePercentLow ?? null, isRawPerformer: (e.fpsAvg ?? 0) >= 60 && e.upscalerType === "none" && @@ -229,6 +233,16 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( isPoorPerformer: (e.fpsAvg ?? 0) < 30, })) + // Stability score computation + const stabilityScores = entries + .filter(e => e.fpsOnePercentLow != null && (e.fpsAvg ?? 0) > 0) + .map(e => Math.min(1, e.fpsOnePercentLow! / (e.fpsAvg ?? 1))) + const avgStability = stabilityScores.length > 0 + ? Math.round((stabilityScores.reduce((a, b) => a + b, 0) / stabilityScores.length) * 100) / 100 + : null + const bestOnePercentLow = entries.reduce((best, e) => + e.fpsOnePercentLow != null && e.fpsOnePercentLow > (best ?? 0) ? e.fpsOnePercentLow : best, null as number | null) + // ── 8. Device breakdown ─────────────────────────────────────── const deviceBreakdown = Array.from(boxplotMap.entries()).map( ([slug, { hardwareName, values }]) => ({ @@ -253,6 +267,8 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( bestDevice, verifiedCount, versionCount, + avgStability, + bestOnePercentLow, }, isRawPerformer, isPoorPerformance,