GitHub Actions commited on
Commit
55229e0
·
1 Parent(s): a474e4b

sync from abhijitramesh/webgpu-bench@62e3120604

Browse files
Files changed (1) hide show
  1. js/run/source.js +76 -24
js/run/source.js CHANGED
@@ -42,6 +42,39 @@ async function getOpfsFileHandle(repo, file, { create }) {
42
  return dir.getFileHandle(file, { create });
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  export function ggufSource() {
46
  return {
47
  async isCached(repo, file) {
@@ -62,13 +95,19 @@ export function ggufSource() {
62
  // distinguishes a fresh download from a cache hit so the caller can
63
  // decide whether to evict the variant after the run.
64
  async opfsHandleForModel(repo, file, onProgress, signal) {
65
- const cached = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
 
 
 
 
 
 
 
 
 
66
  if (cached) {
67
- const f = await cached.getFile();
68
- if (f.size > 0) {
69
- onProgress?.(1, f.size, f.size);
70
- return { handle: cached, size: f.size, wasDownloaded: false };
71
- }
72
  }
73
 
74
  // Cache miss — download from HF straight into a writable OPFS stream.
@@ -81,30 +120,43 @@ export function ggufSource() {
81
  }
82
  const contentLength = parseInt(resp.headers.get('content-length') || '0', 10);
83
 
84
- const handle = await getOpfsFileHandle(repo, file, { create: true });
85
- const writable = await handle.createWritable({ keepExistingData: false });
86
-
87
  // Opportunistically request persistent storage so eviction is less
88
  // likely once we commit to pulling large files. Best-effort — ignore
89
  // rejection (some browsers only grant on user gesture).
90
  navigator.storage?.persist?.().catch(() => {});
91
 
92
- try {
93
- const reader = resp.body.getReader();
94
- let downloaded = 0;
95
- while (true) {
96
- const { done, value } = await reader.read();
97
- if (done) break;
98
- await writable.write(value);
99
- downloaded += value.byteLength;
100
- if (contentLength > 0) onProgress?.(downloaded / contentLength, downloaded, contentLength);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
- await writable.close();
103
- return { handle, size: downloaded, wasDownloaded: true };
104
- } catch (err) {
105
- try { await writable.abort(err); } catch { /* ignore */ }
106
- throw err;
107
- }
108
  },
109
 
110
  async evictModel(repo, file) {
 
42
  return dir.getFileHandle(file, { create });
43
  }
44
 
45
+ // WebKit (iOS Safari) returns one of these strings/names when the OPFS
46
+ // operation fails because something else (typically a stuck
47
+ // FileSystemSyncAccessHandle from a worker that was Jetsam-killed before
48
+ // it could close cleanly) is still holding the file. The handle is
49
+ // usually released within a few seconds, so retrying with backoff is the
50
+ // documented mitigation. Other "real" errors (NotFoundError, QuotaExceeded)
51
+ // are not transient and shouldn't be retried.
52
+ function isOpfsTransientError(err) {
53
+ if (!err) return false;
54
+ const msg = String(err.message || err);
55
+ if (/unknown transient/i.test(msg)) return true;
56
+ if (/no modification allowed/i.test(msg)) return true;
57
+ if (err.name === 'InvalidStateError') return true;
58
+ if (err.name === 'NoModificationAllowedError') return true;
59
+ return false;
60
+ }
61
+
62
+ async function withOpfsRetry(fn) {
63
+ const delays = [500, 2_000, 5_000];
64
+ let lastErr;
65
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
66
+ try {
67
+ return await fn(attempt);
68
+ } catch (err) {
69
+ lastErr = err;
70
+ if (!isOpfsTransientError(err)) throw err;
71
+ if (attempt === delays.length) break;
72
+ await new Promise((r) => setTimeout(r, delays[attempt]));
73
+ }
74
+ }
75
+ throw lastErr;
76
+ }
77
+
78
  export function ggufSource() {
79
  return {
80
  async isCached(repo, file) {
 
95
  // distinguishes a fresh download from a cache hit so the caller can
96
  // decide whether to evict the variant after the run.
97
  async opfsHandleForModel(repo, file, onProgress, signal) {
98
+ // Cache lookup wrapped in retry because getFile() can also hit
99
+ // the WebKit transient (a sync access handle from a previous
100
+ // worker that was Jetsam-killed mid-run blocks this for a few
101
+ // seconds until WebKit's GC reaps it).
102
+ const cached = await withOpfsRetry(async () => {
103
+ const handle = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
104
+ if (!handle) return null;
105
+ const f = await handle.getFile();
106
+ return f.size > 0 ? { handle, size: f.size } : null;
107
+ });
108
  if (cached) {
109
+ onProgress?.(1, cached.size, cached.size);
110
+ return { handle: cached.handle, size: cached.size, wasDownloaded: false };
 
 
 
111
  }
112
 
113
  // Cache miss — download from HF straight into a writable OPFS stream.
 
120
  }
121
  const contentLength = parseInt(resp.headers.get('content-length') || '0', 10);
122
 
 
 
 
123
  // Opportunistically request persistent storage so eviction is less
124
  // likely once we commit to pulling large files. Best-effort — ignore
125
  // rejection (some browsers only grant on user gesture).
126
  navigator.storage?.persist?.().catch(() => {});
127
 
128
+ // Retry the createWritable + drain loop on the WebKit transient.
129
+ // Each retry restarts the download from byte 0; for streamed writes
130
+ // we can't resume mid-file without re-issuing the fetch, and the
131
+ // transient typically only fires on createWritable so retrying is
132
+ // usually a no-op past attempt 0. Fresh fetch per attempt is the
133
+ // simplest correct thing.
134
+ return await withOpfsRetry(async (attempt) => {
135
+ const handle = await getOpfsFileHandle(repo, file, { create: true });
136
+ const writable = await handle.createWritable({ keepExistingData: false });
137
+
138
+ // On retry we need a fresh response body — the original reader
139
+ // was consumed (or aborted) by the previous attempt. Use the
140
+ // already-fetched response on attempt 0; re-fetch on retries.
141
+ const body = attempt === 0 ? resp.body : (await fetch(url, { signal })).body;
142
+
143
+ try {
144
+ const reader = body.getReader();
145
+ let downloaded = 0;
146
+ while (true) {
147
+ const { done, value } = await reader.read();
148
+ if (done) break;
149
+ await writable.write(value);
150
+ downloaded += value.byteLength;
151
+ if (contentLength > 0) onProgress?.(downloaded / contentLength, downloaded, contentLength);
152
+ }
153
+ await writable.close();
154
+ return { handle, size: downloaded, wasDownloaded: true };
155
+ } catch (err) {
156
+ try { await writable.abort(err); } catch { /* ignore */ }
157
+ throw err;
158
  }
159
+ });
 
 
 
 
 
160
  },
161
 
162
  async evictModel(repo, file) {