Skip to content

Commit d6ade97

Browse files
committed
fx
1 parent 70f82c4 commit d6ade97

2 files changed

Lines changed: 103 additions & 95 deletions

File tree

resources/main.mjs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class ResourcePreloader {
1212
this._registration = null;
1313
this._sw = null;
1414
this._preloadParams = "";
15+
this._clientId = typeof crypto?.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36);
1516
}
1617

1718
isCached() {
@@ -21,9 +22,8 @@ export class ResourcePreloader {
2122
}
2223

2324
async setup() {
24-
await this._unregisterOldServiceWorkers();
25-
2625
if (!params.preload) {
26+
await this._unregisterOldServiceWorkers();
2727
this._registration = null;
2828
this._sw = null;
2929
return false;
@@ -58,13 +58,20 @@ export class ResourcePreloader {
5858
if (!this._sw)
5959
return Promise.resolve();
6060

61+
messageData.clientId = this._clientId;
62+
6163
return new Promise((resolve, reject) => {
6264
const channel = new MessageChannel();
6365
const port = channel.port1;
6466
let timeoutId = null;
6567

68+
// Prevent GC in Safari
69+
this._activeChannels = this._activeChannels || new Set();
70+
this._activeChannels.add(channel);
71+
6672
const cleanup = () => {
6773
port.onmessage = null;
74+
this._activeChannels.delete(channel);
6875
if (timeoutId)
6976
clearTimeout(timeoutId);
7077
};
@@ -95,6 +102,7 @@ export class ResourcePreloader {
95102

96103
resolve(data);
97104
};
105+
port.start();
98106

99107
this._sw.postMessage(messageData, [channel.port2]);
100108
});
@@ -115,9 +123,6 @@ export class ResourcePreloader {
115123
}
116124

117125
async preloadSuites(suites, resourceLoadDelay, clearCache = true, onProgress) {
118-
if (!this._sw || suites.length === 0)
119-
return undefined;
120-
121126
const suitesData = suites
122127
.filter((s) => s.resources)
123128
.map((s) => ({
@@ -126,7 +131,7 @@ export class ResourcePreloader {
126131
resources: new URL(s.resources, window.location.href).href,
127132
}));
128133

129-
if (suitesData.length === 0)
134+
if (!this._sw || suitesData.length === 0)
130135
return undefined;
131136

132137
const startTime = performance.now();
@@ -546,9 +551,13 @@ class MainBenchmarkClient {
546551
}
547552

548553
await this._resourcePreloader.resetPreloading();
549-
const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator);
550-
if (preloadResult === "ABORTED")
551-
return;
554+
if (params.developerMode) {
555+
this._enableStartButtons();
556+
} else {
557+
const preloadResult = await this._setupResourcePreloader(benchmarkConfigurator);
558+
if (preloadResult === "ABORTED")
559+
return;
560+
}
552561

553562
if (params.startAutomatically)
554563
this.start();
@@ -596,6 +605,7 @@ class MainBenchmarkClient {
596605

597606
_resetPreloadUI() {
598607
this._latestProgressData = null;
608+
this._progressUpdateScheduled = false;
599609
document.getElementById("preload-progress-completed").value = 0;
600610
document.getElementById("preload-info-label").textContent = "";
601611
document.getElementById("preload-info-progress").textContent = "";

sw.mjs

Lines changed: 84 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -31,48 +31,32 @@ class LockStore {
3131
return this.dbPromise;
3232
}
3333

34-
async getOwner() {
34+
async _runTransaction(mode, callback) {
3535
try {
3636
const db = await this._openDB();
37-
const data = await new Promise((resolve, reject) => {
38-
const tx = db.transaction(STORE_NAME, "readonly");
39-
const req = tx.objectStore(STORE_NAME).get("stateData");
37+
return await new Promise((resolve, reject) => {
38+
const tx = db.transaction(STORE_NAME, mode);
39+
const req = callback(tx.objectStore(STORE_NAME));
4040
req.onsuccess = () => resolve(req.result);
4141
req.onerror = () => reject(req.error);
4242
});
43-
return data?.clientId || null;
4443
} catch (e) {
45-
console.warn("IndexedDB read failed", e);
44+
console.warn(`IndexedDB ${mode} failed`, e);
4645
return null;
4746
}
4847
}
4948

49+
async getOwner() {
50+
const data = await this._runTransaction("readonly", (store) => store.get("stateData"));
51+
return data?.clientId || null;
52+
}
53+
5054
async setOwner(clientId) {
51-
try {
52-
const db = await this._openDB();
53-
await new Promise((resolve, reject) => {
54-
const tx = db.transaction(STORE_NAME, "readwrite");
55-
const req = tx.objectStore(STORE_NAME).put({ clientId }, "stateData");
56-
req.onsuccess = () => resolve();
57-
req.onerror = () => reject(req.error);
58-
});
59-
} catch (e) {
60-
console.warn("IndexedDB write failed", e);
61-
}
55+
await this._runTransaction("readwrite", (store) => store.put({ clientId }, "stateData"));
6256
}
6357

6458
async clear() {
65-
try {
66-
const db = await this._openDB();
67-
await new Promise((resolve, reject) => {
68-
const tx = db.transaction(STORE_NAME, "readwrite");
69-
const req = tx.objectStore(STORE_NAME).delete("stateData");
70-
req.onsuccess = () => resolve();
71-
req.onerror = () => reject(req.error);
72-
});
73-
} catch (e) {
74-
console.warn("IndexedDB clear failed", e);
75-
}
59+
await this._runTransaction("readwrite", (store) => store.delete("stateData"));
7660
}
7761

7862
async hasLock(clientId) {
@@ -86,7 +70,10 @@ class LockStore {
8670
const STORE = new LockStore();
8771

8872
function replyToClient(event, type, msg = {}) {
89-
event.ports?.[0]?.postMessage({ type, ...msg });
73+
if (event.ports?.[0]) {
74+
event.ports[0].start?.();
75+
event.ports[0].postMessage({ type, ...msg });
76+
}
9077
}
9178

9279
function replyError(event, message) {
@@ -114,7 +101,7 @@ class RequestLimiter {
114101
} catch (e) {
115102
// Individual task errors are handled by their respective promises
116103
}
117-
await new Promise((r) => setTimeout(r, 1));
104+
await delayAsync(1); // Yield to event loop
118105
}
119106
this.active--;
120107
}
@@ -127,18 +114,16 @@ class RequestLimiter {
127114

128115
if (this.active < this.limit) {
129116
this.active++;
130-
this._processQueue();
117+
this._processQueue(); // Intentionally not awaited
131118
}
132119
});
133120
}
134121

135122
clear() {
136-
for (const task of this.queue) {
137-
if (task.resolve)
138-
task.resolve(0);
139-
}
123+
for (const task of this.queue)
124+
task.resolve?.(0);
140125

141-
this.queue = [];
126+
this.queue.length = 0;
142127
}
143128
}
144129

@@ -159,56 +144,64 @@ function handleResetPreloadingMessage(event) {
159144
}
160145

161146
async function handlePreloadSuitesMessage(event, clientId, { suites = [], delay = 0, clearCache = true }) {
162-
await STORE.getOwner();
163-
await updateActiveClient(clientId);
164-
165-
handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel!
166-
167-
currentPreloadEvent = event;
168-
const preloadId = currentPreloadId;
169-
170-
failedRequests.clear();
171-
172-
if (clearCache)
173-
await caches.delete(CACHE_NAME);
174-
175-
const cache = await caches.open(CACHE_NAME);
176-
let loaded = 0;
177-
let transferredSize = 0;
178-
const urlsToCache = [];
147+
try {
148+
await STORE.getOwner();
149+
await updateActiveClient(clientId);
150+
151+
handleResetPreloadingMessage(); // Call it without event to avoid replying to the PRELOAD_SUITES channel!
152+
153+
currentPreloadEvent = event;
154+
const preloadId = currentPreloadId;
155+
156+
failedRequests.clear();
157+
if (clearCache)
158+
await caches.delete(CACHE_NAME);
159+
160+
const cache = await caches.open(CACHE_NAME);
161+
const urlsToCache = await getUrlsToCache(suites);
162+
const total = urlsToCache.length;
163+
164+
let loaded = 0;
165+
let transferredSize = 0;
166+
167+
const promises = urlsToCache.map(async (item, index) => {
168+
if (preloadId !== currentPreloadId)
169+
return;
170+
const size = await fetchAndCache(cache, item.url, delay * index);
171+
if (preloadId !== currentPreloadId)
172+
return;
173+
transferredSize += size;
174+
loaded++;
175+
replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName });
176+
});
179177

180-
for (const suite of suites) {
181-
if (!suite.resources)
182-
continue;
183-
urlsToCache.push(...await parseSuiteResources(suite));
184-
}
178+
await Promise.all(promises);
185179

186-
const total = urlsToCache.length;
187-
const promises = urlsToCache.map(async (item, index) => {
188-
if (preloadId !== currentPreloadId)
189-
return;
190-
const size = await fetchAndCache(cache, item.url, delay * index);
191180
if (preloadId !== currentPreloadId)
192181
return;
193-
transferredSize += size;
194-
loaded++;
195-
replyToClient(event, SW_MESSAGES.PRELOAD_PROGRESS, { loaded, total, url: item.url, suiteName: item.suiteName });
196-
});
197-
198-
await Promise.all(promises);
199182

200-
if (preloadId !== currentPreloadId)
201-
return;
202-
203-
if (!await STORE.hasLock(clientId)) {
204-
replyError(event, "Speedometer aborted: Another tab took over.");
183+
if (!await STORE.hasLock(clientId)) {
184+
replyError(event, "Speedometer aborted: Another tab took over.");
185+
return;
186+
}
187+
replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length });
188+
} catch (error) {
189+
console.error("Error during preload:", error);
190+
replyError(event, error.message || "Failed to preload resources.");
191+
} finally {
205192
if (currentPreloadEvent === event)
206193
currentPreloadEvent = null;
207-
return;
208194
}
209-
replyToClient(event, SW_MESSAGES.PRELOAD_DONE, { transferredSize, count: urlsToCache.length });
210-
if (currentPreloadEvent === event)
211-
currentPreloadEvent = null;
195+
}
196+
197+
async function getUrlsToCache(suites) {
198+
const urlsToCache = [];
199+
for (const suite of suites) {
200+
if (suite.resources)
201+
urlsToCache.push(...await parseSuiteResources(suite));
202+
}
203+
204+
return urlsToCache;
212205
}
213206

214207
async function parseSuiteResources(suite) {
@@ -237,7 +230,7 @@ async function fetchAndCache(cache, url, delayMs) {
237230
await delayAsync(delayMs);
238231
return requestLimiter.schedule(async () => {
239232
const request = new Request(url, { cache: "no-cache" });
240-
const existing = await cache.match(request, { ignoreSearch: true, ignoreVary: true });
233+
const existing = await cache.match(request, { ignoreSearch: true });
241234
if (existing)
242235
return getResponseSize(existing);
243236

@@ -278,12 +271,16 @@ async function updateActiveClient(newClientId) {
278271
}
279272

280273
async function handleClearCacheMessage(event, clientId) {
281-
if (!await STORE.hasLock(clientId)) {
282-
replyError(event, "Cannot clear SW: You do not own the lock.");
283-
return;
274+
try {
275+
if (!await STORE.hasLock(clientId)) {
276+
replyError(event, "Cannot clear SW: You do not own the lock.");
277+
return;
278+
}
279+
await STORE.clear();
280+
replyToClient(event, "SUCCESS");
281+
} catch (e) {
282+
replyError(event, e.message || "Failed to clear cache");
284283
}
285-
await STORE.clear();
286-
replyToClient(event, "SUCCESS");
287284
}
288285

289286
async function handleGetFailedRequestsMessage(event, clientId) {
@@ -301,9 +298,10 @@ self.addEventListener("message", (event) => {
301298
const { data } = event;
302299
if (!data)
303300
return;
301+
const clientId = data.clientId || event.source?.id;
304302
const handler = MESSAGE_HANDLERS[data.type];
305303
if (handler)
306-
event.waitUntil(handler(event, event.source?.id, data));
304+
event.waitUntil(handler(event, clientId, data));
307305
else
308306
console.error("Unknown service worker message type:", data.type);
309307
});
@@ -321,7 +319,7 @@ self.addEventListener("fetch", (event) => {
321319
requestToMatch = urlObj.href;
322320
}
323321

324-
const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true, ignoreVary: true });
322+
const cachedResponse = await caches.match(requestToMatch, { cacheName: CACHE_NAME, ignoreSearch: true });
325323
if (cachedResponse)
326324
return cachedResponse;
327325

0 commit comments

Comments
 (0)