-
Notifications
You must be signed in to change notification settings - Fork 152
Global throttling #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Global throttling #831
Changes from 10 commits
02b2e43
1f3e03e
477c87e
1250ca3
8a86841
7a6b898
5d59294
4ced5e1
5c566fa
b69dc8d
addc50f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* eslint-env worker */ | ||
| /* eslint-disable-next-line spaced-comment */ | ||
| /// <reference lib="webworker" /> | ||
|
|
||
| // This worker won't share the same queue as the main thread, but throttling should be okay | ||
| // as long as we don't use FetchTool and FetchWorkerTool at the same time. | ||
| // TODO: Communicate metadata from the main thread to workers or move the worker boundary "into" `scratchFetch`. | ||
| // Make sure to benchmark any changes to avoid performance regressions, especially for large project loads. | ||
| import {AssetQueueOptions} from './HostQueues'; | ||
| import {scratchFetch} from './scratchFetch'; | ||
|
|
||
| interface JobMessage { | ||
| id: string, | ||
| url: string; | ||
| options: RequestInit | undefined; | ||
| } | ||
|
|
||
| interface CompletionMessage { | ||
| id: string, | ||
cwillisf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| buffer?: ArrayBuffer | null; | ||
| error?: string; | ||
| } | ||
|
|
||
| let jobsActive = 0; | ||
| const complete: CompletionMessage[] = []; | ||
|
|
||
| let intervalId: ReturnType<typeof setInterval> | undefined = void 0; | ||
|
|
||
| /** | ||
| * Register a step function. | ||
| * | ||
| * Step checks if there are completed jobs and if there are sends them to the | ||
| * parent. Then it checks the jobs count. If there are no further jobs, clear | ||
| * the step. | ||
| */ | ||
| const registerStep = function () { | ||
| intervalId = setInterval(() => { | ||
| if (complete.length) { | ||
| // Send our chunk of completed requests and instruct postMessage to | ||
| // transfer the buffers instead of copying them. | ||
| postMessage( | ||
| complete.slice(), | ||
| // Instruct postMessage that these buffers in the sent message | ||
| // should use their Transferable trait. After the postMessage | ||
| // call the "buffers" will still be in complete if you looked, | ||
| // but they will all be length 0 as the data they reference has | ||
| // been sent to the window. This lets us send a lot of data | ||
| // without the normal postMessage behaviour of making a copy of | ||
| // all of the data for the window. | ||
| complete.map(response => response.buffer).filter(Boolean) as Transferable[] | ||
| ); | ||
| complete.length = 0; | ||
| } | ||
| if (jobsActive === 0) { | ||
| clearInterval(intervalId); | ||
| intervalId = void 0; | ||
| } | ||
| }, 1); | ||
| }; | ||
|
|
||
| /** | ||
| * Receive a job from the parent and fetch the requested data. | ||
| * @param message The message from the parent. | ||
|
Check warning on line 63 in src/FetchWorkerTool.worker.ts
|
||
| * @param message.data A job id, url, and options descriptor to perform. | ||
|
Check warning on line 64 in src/FetchWorkerTool.worker.ts
|
||
| */ | ||
| const onMessage = async ({data: job}: MessageEvent<JobMessage>) => { | ||
| if (jobsActive === 0 && !intervalId) { | ||
| registerStep(); | ||
| } | ||
|
|
||
| jobsActive++; | ||
|
|
||
| try { | ||
| const response = await scratchFetch(job.url, job.options, {queueOptions: AssetQueueOptions}); | ||
|
|
||
| const result:CompletionMessage = {id: job.id}; | ||
cwillisf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (response.ok) { | ||
| result.buffer = await response.arrayBuffer(); | ||
| } else if (response.status === 404) { | ||
| result.buffer = null; | ||
| } else { | ||
| throw response.status; | ||
| } | ||
| complete.push(result); | ||
| } catch (error) { | ||
| complete.push({id: job.id, error: ((error as Error)?.message) || `Failed request: ${job.url}`}); | ||
| } finally { | ||
| jobsActive--; | ||
| } | ||
| }; | ||
|
|
||
| // "fetch" is supported in Node.js as of 16.15 and our target browsers as of ~2017 | ||
| postMessage({support: {fetch: true}}); | ||
| self.addEventListener('message', onMessage); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import {QueueManager, type QueueOptions} from '@scratch/task-herder'; | ||
|
|
||
| /** | ||
| * @summary A set of generous limits, for things like downloading assets from CDN. | ||
| * @description | ||
| * In practice, these limits seem to lead to slightly better performance than no limits at all, mostly due to the | ||
| * concurrency limit. For example, on my development computer & my relatively fast residential connection, a | ||
| * concurrency limit of 4 loads a particular test project in 21 seconds, as opposed to 25 seconds when I bypass the | ||
| * queue and call `fetch` directly. In that test, my setup downloads about 50 assets per second, so this set of options | ||
| * only affects concurrency and doesn't actually throttle the downloads. Limiting concurrency also fixes the issue | ||
| * where very large projects (thousands of assets) can lead to browser failures like `net::ERR_INSUFFICIENT_RESOURCES`. | ||
| * The exact concurrency limit doesn't seem to matter much since the browser limits parallel connections itself. It | ||
| * just needs to be high enough to avoid bubbles in the download pipeline and low enough to avoid resource exhaustion. | ||
| * @see {@link https://github.com/scratchfoundation/scratch-gui/issues/7111} | ||
| */ | ||
| export const AssetQueueOptions: QueueOptions = { | ||
| burstLimit: 64, | ||
| sustainRate: 64, | ||
| // WARNING: asset download concurrency >=5 can lead to corrupted buffers on Chrome (December 2025, Chrome 142.0) | ||
| // when using Scratch's bitmap load pipeline. Marking the canvas context as `{willReadFrequently: true}` seems to | ||
| // eliminate that issue, so maybe the problem is related to hardware acceleration. | ||
| concurrency: 64 | ||
| }; | ||
|
|
||
| /** | ||
| * Central registry of per-host queues. | ||
| * Uses strict limits by default. Override these strict limits as needed for specific hosts. | ||
| */ | ||
| export const hostQueueManager = new QueueManager({ | ||
| burstLimit: 5, | ||
| sustainRate: 1, | ||
| concurrency: 1 | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| import _Asset, {AssetData, AssetId} from './Asset'; | ||
| import {AssetType as _AssetType, AssetType} from './AssetType'; | ||
| import {DataFormat as _DataFormat, DataFormat} from './DataFormat'; | ||
| import _scratchFetch from './scratchFetch'; | ||
| import * as _scratchFetch from './scratchFetch'; | ||
| import Helper from './Helper'; | ||
|
|
||
| interface HelperWithPriority { | ||
|
|
@@ -130,7 +130,7 @@ | |
| * @param {DataFormat} dataFormat - The dataFormat of the data for the cached asset. | ||
| * @param {Buffer} data - The data for the cached asset. | ||
| * @param {string} [id] - The id for the cached asset. | ||
| * @param {bool} [generateId] - flag to set id to an md5 hash of data if `id` isn't supplied | ||
| * @returns {Asset} generated Asset with `id` attribute set if not supplied | ||
| */ | ||
| createAsset ( | ||
|
|
@@ -171,7 +171,7 @@ | |
| this.addWebStore(types, urlFunction); | ||
| } | ||
|
|
||
| /** | ||
|
Check warning on line 174 in src/ScratchStorage.ts
|
||
| * TODO: Should this be removed in favor of requesting an asset with `null` as the ID? | ||
| * @param {AssetType} type - Get the default ID for assets of this type. | ||
| * @returns {?string} The ID of the default asset of the given type, if any. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.