feat: display 1% low FPS in presets, stats API, and game details
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
ThumbsUpIcon,
|
ThumbsUpIcon,
|
||||||
ThumbsDownIcon,
|
ThumbsDownIcon,
|
||||||
|
GaugeIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useSession } from "@/lib/auth-client"
|
import { useSession } from "@/lib/auth-client"
|
||||||
@@ -81,6 +82,7 @@ interface Preset {
|
|||||||
fpsAvg: number | null
|
fpsAvg: number | null
|
||||||
fpsLow: number | null
|
fpsLow: number | null
|
||||||
fpsHigh: number | null
|
fpsHigh: number | null
|
||||||
|
fpsOnePercentLow: number | null
|
||||||
upscalerType: string | null
|
upscalerType: string | null
|
||||||
upscalerVersion: string | null
|
upscalerVersion: string | null
|
||||||
frameGenMethod: string | null
|
frameGenMethod: string | null
|
||||||
@@ -102,6 +104,8 @@ interface StatsResponse {
|
|||||||
bestDevice: string
|
bestDevice: string
|
||||||
verifiedCount: number
|
verifiedCount: number
|
||||||
versionCount: number
|
versionCount: number
|
||||||
|
avgStability: number | null
|
||||||
|
bestOnePercentLow: number | null
|
||||||
}
|
}
|
||||||
isRawPerformer: boolean
|
isRawPerformer: boolean
|
||||||
isPoorPerformance: boolean
|
isPoorPerformance: boolean
|
||||||
@@ -111,6 +115,7 @@ interface StatsResponse {
|
|||||||
min: number
|
min: number
|
||||||
q1: number
|
q1: number
|
||||||
median: number
|
median: number
|
||||||
|
onePercentLow?: number
|
||||||
q3: number
|
q3: number
|
||||||
max: number
|
max: number
|
||||||
}>
|
}>
|
||||||
@@ -136,6 +141,7 @@ interface StatsResponse {
|
|||||||
fpsLow: number
|
fpsLow: number
|
||||||
fpsAvg: number
|
fpsAvg: number
|
||||||
fpsHigh: number
|
fpsHigh: number
|
||||||
|
fpsOnePercentLow: number | null
|
||||||
isRawPerformer: boolean
|
isRawPerformer: boolean
|
||||||
}>
|
}>
|
||||||
deviceBreakdown: Array<{
|
deviceBreakdown: Array<{
|
||||||
@@ -840,11 +846,16 @@ export function GamePageClient({
|
|||||||
{" "}
|
{" "}
|
||||||
avg
|
avg
|
||||||
</span>
|
</span>
|
||||||
{preset.fpsLow !== null && (
|
{preset.fpsOnePercentLow !== null && (
|
||||||
<span className='text-text/40'>
|
<span className='text-text/40'>
|
||||||
{" "}
|
{" "}
|
||||||
({preset.fpsLow}–
|
· {preset.fpsOnePercentLow} 1% low
|
||||||
{preset.fpsHigh})
|
</span>
|
||||||
|
)}
|
||||||
|
{preset.fpsLow !== null && preset.fpsHigh !== null && !preset.fpsOnePercentLow && (
|
||||||
|
<span className='text-text/40'>
|
||||||
|
{" "}
|
||||||
|
({preset.fpsLow}–{preset.fpsHigh})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -953,6 +964,13 @@ export function GamePageClient({
|
|||||||
)}
|
)}
|
||||||
icon={DatabaseIcon}
|
icon={DatabaseIcon}
|
||||||
/>
|
/>
|
||||||
|
{filteredStats.summary.avgStability !== null && (
|
||||||
|
<StatCard
|
||||||
|
label='Avg Stability'
|
||||||
|
value={`${Math.round(filteredStats.summary.avgStability * 100)}%`}
|
||||||
|
icon={GaugeIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export default async function GamePage({
|
|||||||
fpsAvg: performanceEntries.fpsAvg,
|
fpsAvg: performanceEntries.fpsAvg,
|
||||||
fpsLow: performanceEntries.fpsLow,
|
fpsLow: performanceEntries.fpsLow,
|
||||||
fpsHigh: performanceEntries.fpsHigh,
|
fpsHigh: performanceEntries.fpsHigh,
|
||||||
|
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
|
||||||
upscalerType: performanceEntries.upscalerType,
|
upscalerType: performanceEntries.upscalerType,
|
||||||
upscalerVersion: performanceEntries.upscalerVersion,
|
upscalerVersion: performanceEntries.upscalerVersion,
|
||||||
frameGenMethod: performanceEntries.frameGenMethod,
|
frameGenMethod: performanceEntries.frameGenMethod,
|
||||||
@@ -292,6 +293,7 @@ export default async function GamePage({
|
|||||||
fpsAvg: p.fpsAvg,
|
fpsAvg: p.fpsAvg,
|
||||||
fpsLow: p.fpsLow,
|
fpsLow: p.fpsLow,
|
||||||
fpsHigh: p.fpsHigh,
|
fpsHigh: p.fpsHigh,
|
||||||
|
fpsOnePercentLow: p.fpsOnePercentLow ?? null,
|
||||||
upscalerType: p.upscalerType,
|
upscalerType: p.upscalerType,
|
||||||
upscalerVersion: p.upscalerVersion,
|
upscalerVersion: p.upscalerVersion,
|
||||||
frameGenMethod: p.frameGenMethod,
|
frameGenMethod: p.frameGenMethod,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface Preset {
|
|||||||
fpsAvg: number | null
|
fpsAvg: number | null
|
||||||
fpsLow: number | null
|
fpsLow: number | null
|
||||||
fpsHigh: number | null
|
fpsHigh: number | null
|
||||||
|
fpsOnePercentLow: number | null
|
||||||
upscalerType: string | null
|
upscalerType: string | null
|
||||||
upscalerVersion: string | null
|
upscalerVersion: string | null
|
||||||
frameGenMethod: string | null
|
frameGenMethod: string | null
|
||||||
@@ -295,6 +296,14 @@ export function PresetDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
</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" />
|
<div className="h-px bg-border" />
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get(
|
|||||||
fpsAvg: performanceEntries.fpsAvg,
|
fpsAvg: performanceEntries.fpsAvg,
|
||||||
fpsLow: performanceEntries.fpsLow,
|
fpsLow: performanceEntries.fpsLow,
|
||||||
fpsHigh: performanceEntries.fpsHigh,
|
fpsHigh: performanceEntries.fpsHigh,
|
||||||
|
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
|
||||||
upscalerType: performanceEntries.upscalerType,
|
upscalerType: performanceEntries.upscalerType,
|
||||||
upscalerVersion: performanceEntries.upscalerVersion,
|
upscalerVersion: performanceEntries.upscalerVersion,
|
||||||
frameGenMethod: performanceEntries.frameGenMethod,
|
frameGenMethod: performanceEntries.frameGenMethod,
|
||||||
@@ -140,12 +141,14 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get(
|
|||||||
const q1Idx = Math.floor(n * 0.25)
|
const q1Idx = Math.floor(n * 0.25)
|
||||||
const medIdx = Math.floor(n * 0.5)
|
const medIdx = Math.floor(n * 0.5)
|
||||||
const q3Idx = Math.floor(n * 0.75)
|
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 {
|
return {
|
||||||
hardwareSlug: slug,
|
hardwareSlug: slug,
|
||||||
hardwareName,
|
hardwareName,
|
||||||
min: sorted[0],
|
min: sorted[0],
|
||||||
q1: sorted[q1Idx],
|
q1: sorted[q1Idx],
|
||||||
median: sorted[medIdx],
|
median: sorted[medIdx],
|
||||||
|
onePercentLow,
|
||||||
q3: sorted[q3Idx],
|
q3: sorted[q3Idx],
|
||||||
max: sorted[n - 1],
|
max: sorted[n - 1],
|
||||||
}
|
}
|
||||||
@@ -222,6 +225,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get(
|
|||||||
fpsLow: e.fpsLow!,
|
fpsLow: e.fpsLow!,
|
||||||
fpsAvg: e.fpsAvg ?? 0,
|
fpsAvg: e.fpsAvg ?? 0,
|
||||||
fpsHigh: e.fpsHigh!,
|
fpsHigh: e.fpsHigh!,
|
||||||
|
fpsOnePercentLow: e.fpsOnePercentLow ?? null,
|
||||||
isRawPerformer:
|
isRawPerformer:
|
||||||
(e.fpsAvg ?? 0) >= 60 &&
|
(e.fpsAvg ?? 0) >= 60 &&
|
||||||
e.upscalerType === "none" &&
|
e.upscalerType === "none" &&
|
||||||
@@ -229,6 +233,16 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get(
|
|||||||
isPoorPerformer: (e.fpsAvg ?? 0) < 30,
|
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 ───────────────────────────────────────
|
// ── 8. Device breakdown ───────────────────────────────────────
|
||||||
const deviceBreakdown = Array.from(boxplotMap.entries()).map(
|
const deviceBreakdown = Array.from(boxplotMap.entries()).map(
|
||||||
([slug, { hardwareName, values }]) => ({
|
([slug, { hardwareName, values }]) => ({
|
||||||
@@ -253,6 +267,8 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get(
|
|||||||
bestDevice,
|
bestDevice,
|
||||||
verifiedCount,
|
verifiedCount,
|
||||||
versionCount,
|
versionCount,
|
||||||
|
avgStability,
|
||||||
|
bestOnePercentLow,
|
||||||
},
|
},
|
||||||
isRawPerformer,
|
isRawPerformer,
|
||||||
isPoorPerformance,
|
isPoorPerformance,
|
||||||
|
|||||||
Reference in New Issue
Block a user