feat: display 1% low FPS in presets, stats API, and game details
This commit is contained in:
@@ -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
|
||||
</span>
|
||||
{preset.fpsLow !== null && (
|
||||
{preset.fpsOnePercentLow !== null && (
|
||||
<span className='text-text/40'>
|
||||
{" "}
|
||||
({preset.fpsLow}–
|
||||
{preset.fpsHigh})
|
||||
· {preset.fpsOnePercentLow} 1% low
|
||||
</span>
|
||||
)}
|
||||
{preset.fpsLow !== null && preset.fpsHigh !== null && !preset.fpsOnePercentLow && (
|
||||
<span className='text-text/40'>
|
||||
{" "}
|
||||
({preset.fpsLow}–{preset.fpsHigh})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -953,6 +964,13 @@ export function GamePageClient({
|
||||
)}
|
||||
icon={DatabaseIcon}
|
||||
/>
|
||||
{filteredStats.summary.avgStability !== null && (
|
||||
<StatCard
|
||||
label='Avg Stability'
|
||||
value={`${Math.round(filteredStats.summary.avgStability * 100)}%`}
|
||||
icon={GaugeIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{preset.fpsOnePercentLow !== null && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] text-text/50 uppercase tracking-wider">1% Low FPS</span>
|
||||
<span className="text-sm font-semibold tabular-nums text-text">
|
||||
{preset.fpsOnePercentLow} fps
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user