Skip to content

Commit dc23a29

Browse files
committed
feat: add BENCHMARK_FETCH_FAILED TierType
Previously getGPUTier() silently degraded to 'FALLBACK' tier 1 whenever the benchmark fetch failed (unpkg outage, CSP, CORS on strict enterprise networks, offline). Consumers couldn't distinguish "GPU unknown" from "network blocked the benchmarks", so fast hardware was misreported as slow with no way to detect the cause. See #121. A new 'BENCHMARK_FETCH_FAILED' TierType is returned when loadBenchmarks rejects. Consumers that only switch on `tier` keep working unchanged; those that want to retry or surface the condition can opt in. The underlying OutdatedBenchmarksError still re-throws so semver-style incompatibility surfaces loudly, not as a fetch failure.
1 parent 03b681a commit dc23a29

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

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)