Skip to content

Commit 97127e7

Browse files
authored
Merge pull request #161 from pmndrs/feat/benchmark-fetch-failed-type
feat: add BENCHMARK_FETCH_FAILED TierType
2 parents 03b681a + 0c1d968 commit 97127e7

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ const gpuTier = await getGPUTier();
5656

5757
Based on the reported `fps` the GPU is then classified into either `tier: 1 (>= 15 fps)`, `tier: 2 (>= 30 fps)` or `tier: 3 (>= 60 fps)`. The higher the tier the more graphically intensive workload you can offer to the user.
5858

59+
## Result types
60+
61+
`getGPUTier()` returns a `type` field indicating how the result was produced:
62+
63+
| `type` | Meaning |
64+
| ------------------------ | ----------------------------------------------------------------------------------- |
65+
| `BENCHMARK` | Matched a benchmark entry; `fps` reflects the measured framerate for that GPU. |
66+
| `FALLBACK` | Renderer recognised but no benchmark match found. `tier` is a conservative default. |
67+
| `BENCHMARK_FETCH_FAILED` | Benchmark fetch failed (CDN outage, strict CSP, offline, etc.). Safe to retry. |
68+
| `BLOCKLISTED` | Renderer is on a known-bad list (drivers with severe issues). `tier` is always 0. |
69+
| `WEBGL_UNSUPPORTED` | No WebGL context could be created. `tier` is always 0. |
70+
| `SSR` | Running server-side — no `window`, detection skipped. |
71+
5972
## API
6073

6174
```ts

src/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export type TierType =
7777
| 'WEBGL_UNSUPPORTED'
7878
| 'BLOCKLISTED'
7979
| 'FALLBACK'
80-
| 'BENCHMARK';
80+
| 'BENCHMARK'
81+
| 'BENCHMARK_FETCH_FAILED';
8182

8283
export type TierResult = {
8384
tier: number;
@@ -103,6 +104,11 @@ export const getGPUTier = async ({
103104
benchmarksURL = `https://unpkg.com/@pmndrs/detect-gpu@${version}/dist/benchmarks`,
104105
}: GetGPUTier = {}): Promise<TierResult> => {
105106
const queryCache: { [k: string]: Promise<ModelEntry[]> } = {};
107+
// Set when any loadBenchmarks() call rejects with a non-OutdatedBenchmarksError
108+
// (e.g. network failure, CORS, CSP blocking unpkg). Consulted only when the
109+
// benchmark result list is empty, so a successful renderer match trumps a
110+
// failed sibling fetch on a different benchmark file.
111+
let benchmarkFetchFailed = false;
106112
if (isSSR) {
107113
return {
108114
tier: 0,
@@ -178,6 +184,7 @@ export const getGPUTier = async ({
178184
if (error instanceof OutdatedBenchmarksError) {
179185
throw error;
180186
}
187+
benchmarkFetchFailed = true;
181188
debug?.("queryBenchmarks - couldn't load benchmark:", { error });
182189
return;
183190
}
@@ -318,6 +325,18 @@ export const getGPUTier = async ({
318325
);
319326
if (blocklistedModel) return toResult(0, 'BLOCKLISTED', blocklistedModel);
320327

328+
// Distinguish "couldn't reach the benchmark CDN" from "GPU is genuinely
329+
// unknown". Silent tier-1 FALLBACK misrepresents fast hardware as slow
330+
// whenever a network/CSP/CORS issue blocks the benchmarks fetch; this
331+
// branch lets consumers detect the condition and retry.
332+
if (benchmarkFetchFailed) {
333+
return toResult(
334+
1,
335+
'BENCHMARK_FETCH_FAILED',
336+
`${renderer} (${rawRenderer})`
337+
);
338+
}
339+
321340
// Apple Silicon on desktop Safari: the renderer string is the generic
322341
// "Apple GPU" and Safari reports identical WebGL capabilities across
323342
// M1–M5 (verified empirically on M1 Max / M2 / M4), so no web-facing

test/index.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ for (const renderers of [RENDERER_MOBILE, RENDERER_TABLET, RENDERER_DESKTOP]) {
3737
'BLOCKLISTED',
3838
'FALLBACK',
3939
'BENCHMARK',
40+
'BENCHMARK_FETCH_FAILED',
4041
]).toContain(type);
4142
});
4243
}
@@ -339,6 +340,22 @@ test('Apple Silicon desktop Safari — conservative tier-3 FALLBACK', async () =
339340
expect(result.fps).toBe(60);
340341
});
341342

343+
test('benchmark fetch failure surfaces as BENCHMARK_FETCH_FAILED, not silent FALLBACK', async () => {
344+
// Silent degradation to tier-1 FALLBACK misrepresents fast hardware as
345+
// slow whenever the benchmark CDN is blocked (CSP, CORS, unpkg outage).
346+
// Consumers need a way to detect the condition so they can retry.
347+
const result = await getTier({
348+
isMobile: false,
349+
renderer:
350+
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Laptop GPU (0x00002520) Direct3D11 vs_5_0 ps_5_0, D3D11)',
351+
loadBenchmarks: async () => {
352+
throw new Error('simulated network failure');
353+
},
354+
});
355+
expect(result.type).toBe('BENCHMARK_FETCH_FAILED');
356+
expect(result.tier).toBe(1);
357+
});
358+
342359
test('Apple GPU on mobile does NOT take the desktop tier-3 path', async () => {
343360
// iPhone/iPad route through deobfuscateAppleGPU and resolve to specific
344361
// chip benchmarks. The desktop tier-3 fallback must not fire on mobile,
@@ -417,11 +434,11 @@ test('Apple GPU on mobile does NOT take the desktop tier-3 path', async () => {
417434
});
418435
});
419436

420-
test(`When queryBenchmarks throws, FALLBACK is returned`, async () => {
437+
test(`When queryBenchmarks throws, BENCHMARK_FETCH_FAILED is returned`, async () => {
421438
expectGPUResults(
422439
{
423440
tier: 1,
424-
type: 'FALLBACK',
441+
type: 'BENCHMARK_FETCH_FAILED',
425442
},
426443
await getTier({
427444
loadBenchmarks: async (): Promise<ModelEntry[]> => {

0 commit comments

Comments
 (0)