REOPENED Bug 271756
Can new Worker() be made to properly operate on the background (maybe when the URL is an in-memory Blob)?
https://bugs.webkit.org/show_bug.cgi?id=271756
Summary Can new Worker() be made to properly operate on the background (maybe when th...
jujjyl
Reported 2024-03-27 04:30:59 PDT
Would it be possible to make the following code not deadlock the browser? `a.html` ```html <html><body><script> fetch('a.js').then(response => response.blob()).then(blob => { let worker = new Worker(URL.createObjectURL(blob)); let sab = new Uint8Array(new SharedArrayBuffer(16)); worker.postMessage(sab); console.log('Waiting for Worker to finish'); while(sab[0] != 1) /*wait to join with the result*/; console.log(`Worker finished. SAB: ${sab[0]}`); }); </script></body></html> ``` `a.js` ```js onmessage = (e) => { console.log('Received SAB'); e.data[0] = 1; } ``` Observed: browser hangs on the `while()` loop. Expected: `new Worker()` would be great to make progress asynchronously, and the code would print out ``` Worker finished. SAB: 1 ``` Some background: This deadlock is causing headaches to users of SharedArrayBuffer/WebAssembly, and leads to poor startup time and over-subscription of web resources on shipped web sites. If it was possible to make the above code work and not deadlock, it would greatly improve startup time and performance of SharedArrayBuffer-utilizing web sites. Note that we are not asking `new Worker(arbitraryUrl)` to necessarily have this forward-progress guarantee, but that at least that `new Worker(blobUrlInMemory)` would do so (like illustrated in above code). Would that be feasible?
Attachments
jujjyl
Comment 1 2024-03-27 04:35:01 PDT
Note that when testing the above code, COOP+COEP HTTP headers are required, or otherwise the SharedArrayBuffer object will not be available. A quick way to get those headers is to download an ad hoc [emrun.py](https://raw.githubusercontent.com/emscripten-core/emscripten/main/emrun.py) web server, and run ``` emrun.py --no_browser --port 8000 . ``` in the directory of `a.html` and `a.js`. That will launch a web server that includes the relevant COOP+COEP headers.
Alexey Proskuryakov
Comment 2 2024-03-27 17:07:34 PDT
This is an interesting idea, but probably not. This would be quite tricky, as Worker code is generally expecting main thread to be available, and there can be quite a bit or work happening behind the scenes (Web Inspector delegates is one example that comes to mind quickly). Perhaps more importantly, I don't think that we'd be encouraging blocking main thread while waiting for background thread operation, the whole point of workers is that they work without blocking the main thread.
jujjyl
Comment 3 2024-03-28 01:26:25 PDT
> I don't think that we'd be encouraging blocking main thread while waiting for background thread operation, the whole point of workers is that they work without blocking the main thread. This is a fine sentiment, although it is good to recognize that there are several real-world use cases, where blocking the main thread on worker threads is not just the correct thing to do, but also the only possible thing to do. One example of this is the multithreaded WebGL/WebGPU scene traversal and update. A typical interactive real-time rendering application will be processing through frames in its requestAnimationFrame() callback. To improve performance of such operation, a common technique is to hand off the scene traversal and update to a number of background Workers, and then wait until these Workers finish, to render the scene contents. There is no way to achieve the same otherwise, than to make the main thread synchronously block on the Worker threads. This is a hardened battle-tested algorithm in hundreds of native game and 3D applications. As a second example where synchronously blocking the main thread is the right and most performant thing to do can be found in multithreaded Mark-and-Sweep garbage collection. In https://github.com/juj/emgc you can find an example of a multithreaded GC, to be used for example in compiling a C#, Java or Python VM into WebAssembly. In such scenario, when the heap runs out of memory on a malloc, the system may need to trigger an on-demand GC to reclaim memory. To improve overall performance of the GC, instead of doing the GC marking phase just on the main thread, it is desirable to do the GC marking phase with the help of multiple Workers. But the inability to launch such GC Workers on demand means that the GC Workers must have been preallocated already at site startup, which is pessimistic for several reasons: 1) the site startup time will be slowed down, 2) the site may not 100% in all cases even need to GC (yet, the GC Workers need to be there and consume memory) 3) the site won't have a way to let go of the GC workers and free up page memory (the GC workers may need to be reused suddenly in the future depending on user access patterns) 4) when composing software from multiple libraries, each library will independently need to pre-create their Workers for their purpose, since coordinating needed Worker counts across unrelated libraries is impossible (would essentially require developer knowing ahead of time how many pthread_create()s their application would ever do in worst case) If there was a way to launch Workers synchronously from an in-memory Blob URL, then all of the above inefficiencies would be gone: multithreaded sites could launch faster, they wouldn't have to pool up Workers in advance, and performance of abovementioned use cases would be improved. I can appreciate the concern that it might be complex to implement, though the rationale of "sync blocking is bad" is not at all accurate - in many scenarios sync blocking the main thread can improve both the throughput and responsiveness of a site.
Alexey Proskuryakov
Comment 4 2024-03-29 11:33:18 PDT
Adding a few other folks for visibility, so that your feedback is well considered. Detecting frozen main thread is one of the things we do to help users out of misbehaving websites, and while your use cases don't directly contradict the current implementation, encouraging blocking the main thread may limit what can be done to further improve.
jujjyl
Comment 5 2024-03-29 12:31:02 PDT
> Adding a few other folks for visibility, so that your feedback is well considered. Thanks! > encouraging blocking the main thread may limit what can be done to further improve. I would stress here that this issue is not about blocking the main thread per se. Rather, this issue is about enabling a site to allocate resources only when they are actually needed, instead of needing to (wastefully) preallocate Workers earlier. To illustrate, already today one can write code like this: `b.html` ```html <html><body><script> fetch('b.js').then(response => response.blob()).then(blob => { let worker = new Worker(URL.createObjectURL(blob)); worker.postMessage('init'); worker.onmessage = () => { let sab = new Uint8Array(new SharedArrayBuffer(16)); worker.postMessage(sab); console.log('Waiting for Worker to finish'); while(sab[0] != 1) /*wait to join with the result*/; console.log(`Worker finished. SAB: ${sab[0]}`); }; }); </script></body></html> ``` `b.js` ```js onmessage = (e) => { if (e.data == 'init') { console.log('Worker received SAB'); postMessage(0); } else { console.log('Received SAB'); e.data[0] = 1; } } ``` The above code example does not hang, but works correctly. Both a.html and b.html do block the main thread equally, in other words, the root issue at the heart of this problem is not the "blocking the main thread" part. The workaround scheme shown by b.html is what WebAssembly/SharedArrayBuffer users use today since a.html does not work. The difference between b.html and a.html is that in b.html, "worker.postMessage()" **is** able to make forward progress even while the main thread is spinwaiting for the worker, whereas "new Worker()" in a.html is not able to. But the troubling affairs with the workaround presented by b.html is that in that example, one must preallocate the Worker up front. In real world programs, this must happen before the code necessarily knows if it is going to need the Worker in the first place. For example, in https://github.com/juj/emgc I have implemented a multithreaded garbage collector, to be used in C#/Java/Python VMs compiled to multithreaded WebAssembly. In that GC, I would like to perform the GC marking step quicker by using a pool of background Workers. But I would also like to spawn that GC marking Worker pool only on-demand when necessary, instead of requiring the whole WebAssembly site to have to delay its page startup until I first manage to spin up all the GC Workers (that may or may not even ever fire, depending on what the user does on the site!). If the code example in a.html worked, then I would be able to only ever spawn the GC Workers synchronously at the first occassion that I need to GC, which would lead to a kind of "only-pay-if-you-use-it" type of allocation of site resources. This is just one example. Similar needs occur in Emscripten multithreaded WebAssembly users also in other scenarios, e.g. when implementing multithreaded WebGPU rendering, or multithreaded parallel for() constructs, and similar. So ideally, if "new Worker(inMemoryBlob)" was able to complete without needing to yield back to the main JS event loop, all of that wasteful preallocation of "new Worker()"s could be avoided, and multithreaded WebAssembly sites would not need to start up by creating an avalanche of Workers that they might only potentially need to ever use - that would be a big help to WebAssembly site startup performance overall!
Anne van Kesteren
Comment 6 2024-04-10 07:49:34 PDT
Reopening this for further consideration given https://github.com/web-platform-tests/wpt/pull/45502 and the discussion in the HTML issue. In particular, there is nothing in the specification that says comment 0 should not work. It shouldn't necessarily happen synchronously, but the worker should be able to make forward progress independently of what happens on the main thread. Meanwhile the main thread can either hang and decide to stop running script with a "slow script dialog" or some such or run the script long enough for everything to succeed.
Note You need to log in before you can comment on or make changes to this bug.