I mentioned on Twitter it might be nice to get back in to blogging while the world's in lockdown. So here we go, the first in hopefully a few new blog posts!
As of Construct 3 r189, the new Worker mode is enabled by default, running the entire Construct game engine in a Web Worker. This has been literally years in the making, so I'm pretty pleased it's now rolled out to everyone as the default mode. However it's one of these under-the-hood changes that if done properly means nobody will notice, despite having been a huge upgrade! So, what is worker mode and why is it good? That is what I hope to explain in this blog post.
First, a little theory. An important concept for worker mode are threads. In computing, a thread represents work that can happen concurrently. Imagine there are two tasks that need to be done, like compressing two files. If the two tasks are done in the same thread they must be done sequentially, meaning one after the other. If they are done in different threads they can run concurrently, meaning both tasks can make progress at the same time. This is especially important for multi-core CPUs, which are now ubiquitous: when in different threads, each task can be assigned to a different CPU core, and therefore both tasks run in parallel. This means both tasks get completed much more quickly.
Another benefit of using different threads for each task is that if for some reason work in one task gets stuck or takes unexpectedly long, the other task can still make progress. It's a bit like if one app hangs, you can still use other apps - it doesn't hang the entire system. That's because each app is running in its own thread. This is performance isolation - whether or not an app responds does not depend on other apps. The same is true with threads: a thread can keep going even if other threads are stuck.
The browser main thread
Modern browsers do a great job offloading various internal tasks to other threads, but many things simply have to be done on the main thread for technical reasons. If you have a lot of browser tabs open then the browser can also start running multiple tabs in the same thread in order to save resources - but this compounds the problem, making the main thread busier. Some other features like same-origin iframes also have to share the same thread in order to work correctly.
Web Workers and OffscreenCanvas
Our goal was to run the entire Construct game engine in a Web Worker. This brings all the benefits of threading to the entire Construct runtime: no matter what the browser is doing or how busy the main thread is, the game can carry on running smoothly if it's in its own Web Worker. This is what "Worker mode" (short for "Web Worker mode") does in Construct 3. It didn't become possible until 2018, when Chrome 69 came out with a key new feature: OffscreenCanvas. This made it possible to render to a canvas, including with full support for WebGL and WebGL 2, directly from a Web Worker. Finally it was feasible to run entire Construct games in their own thread!
The C3 runtime
We knew that OffscreenCanvas was coming, and planned accordingly. Throughout 2017 we rewrote the entire Construct runtime for Construct 3 with loads of huge upgrades, calling it the C3 runtime, to take over from the old C2 runtime we were still using from Construct 2. It's very difficult to retro-fit existing code to work in a Web Worker - it's one of the architectural considerations you have to plan for right from the start. So while writing the C3 runtime from scratch, it was carefully designed to be able to run in a Web Worker. The main consideration is to make sure any features that aren't available in a Web Worker are delegated back to the main thread. In many cases this is very complicated. The best example of that is the the Audio plugin, which has a complex feature set, none of which can be done directly in a Web Worker, and proved particularly challenging to get right. However with the benefit of OffscreenCanvas, the vast majority of the engine can run directly in a Web Worker!
We launched the new C3 runtime at the end of 2018, but with worker mode off by default. Why wasn't it on by default and why did it take so long - until March 2020 - to change that?
The journey to on-by-default
The answer is over a year of testing and bug fixing. Experience has taught us to launch major new features cautiously. This was a huge architectural change to the engine, and as with any major upgrade it's difficult to predict the outcome. Will it actually work once deployed across thousands of different devices running thousands of different games? Are there unexpected bugs that will turn up? Is it compatible with all existing features?
It was a wise decision to move slowly. With it off-by-default, everyone's games keep working exactly as before (but without the benefits of worker mode). We encouraged some testers to try turning it on and seeing how their games work, and reporting back any issues. And the bugs started rolling in!
It's worth mentioning that computer graphics is hard, and programming with threads is hard. OffscreenCanvas involves both computer graphics and threads. This means it must be very hard to get right! So the fact there are bugs is not because anyone's done a bad job, it's just a very difficult problem and takes time to get right.
Our own testing during development turned up several issues in Chrome (696222, 696223, 696224, 793667). These tended to be fairly straightforward crashes which are obvious if you run in to them. After release, user reports helped us identify further issues which were a little less obvious, such as: a scheduling problem that meant input events stopped working under load (863962); rendering stopping after resizing the window (866855); and a GPU memory leak (897733).
It's worth mentioning during this time we fixed a number of issues on Construct's side too (where our code was wrong, not Chrome's). One example was physics not working in worker mode (#2928). Once we got all these issues sorted out, we actually released a stable release last year (r164) with worker mode enabled by default - yes, we actually already released this in the past! But once more people were using it, we found yet another layer of even more subtle and difficult bugs. It's actually typical in software development that the more users you have, the more bugs are rooted out. These were complicated and were taking time to fix, so we had to take the unfortunate step of making worker mode off by default again. Some of the further issues we dug up included: tab crash on specific Chrome OS devices (1004723), tab crash that only happened with WebGL 1 and not WebGL 2 (1010877, which turned out to be a variant of 793667 which we found and reported back in 2017), and inaccurate frame timestamps (995235). These only all got fixed by December 2019. Once all those were out the way, we tried again enabling worker mode by default for a following stable release, which subsequently came out this month (March 2020).
And - so far, so good! It's been out for a few weeks and everything seems to be working smoothly. So I'm feeling confident enough to say that finally, it's done, it's out and it works!
Now all Construct games run in a Web Worker by default, it has the following benefits:
- Nothing that the browser, other tabs, or other frames do, can jank the game any more. The entire game is running in its own thread separately to everything else. This is the performance isolation aspect, and it's probably the biggest benefit. For example the game can keep running smoothly even if it is iframed on a page with several obnoxiously janky CPU-intensive adverts running.
- Some tasks that can only be done on the main thread, such as SVG rasterization, can now be done in parallel with the game still running. This means things which had to jank the game before don't any more. In general the browser should be able to run more tasks in parallel to the game, improving performance and reducing jank.
- Construct's preview previously also shared the main thread with the editor. This meant if you accidentally made the game hang, it could lock up the editor too, which was pretty inconvenient. With worker mode, even if the game hangs it can still be closed and the editor remains responsive, since only the game's own thread has hung.
- Construct's debugger is quite performance intensive, especially with updating lots of detailed information in the page, which the browser must then calculate the layout for. This can now happen in parallel to the game, improving the debugger's performance.
There might be further benefits we haven't discovered yet. It's a major architectural change for the better, which should have better performance characteristics in many situations. And some benefits work in reverse too, like making sure Construct games don't jank other tabs.
The easiest way to show the benefit of worker mode is to deliberately jank the browser for 1 second and see the impact that has. I've created a quick demo that does just that here. (Note this only works in Chrome since other browsers don't support worker mode yet.)
If you click "Jank for 1 second", the page will hang for 1 second, and you can see the timer next to the button stop as it janks.
Click "Load DOM test", and an iframe loads an automated test based on Space Blaster, which runs on the main thread (i.e. worker mode is off, also known as DOM mode). Once that's running, click "Jank for 1 second". Note the game in the iframe pauses too. The browser is sharing the main thread of the top page with the iframe, so both stop.
Click "Load worker test", which loads exactly the same test but with worker mode enabled. Once that's running, click "Jank for 1 second". Note the top page freezes and the timer stops - but the game in the iframe keeps running perfectly smoothly! The parent page completely hanging for a whole second does not interrupt the smooth performance of the game in any way.
Getting worker mode to work reliably and be enabled by default has been a major project spanning years. It involved completely rearchitecting the engine to support it, lots of testing by both us and our users filing lots of bugs, and lots of work by the Chrome graphics developers who helped sort out the various OffscreenCanvas-related bugs we ran in to. I'm really pleased that we've finally reached the point we've got it enabled by default for everyone! And thanks to everyone who was involved in helping get it working.
I'll point out just a few caveats: it's not enabled for existing projects, since despite the fact it should be fully compatible for almost all projects, there may be some that do unusual things and will be broken by worker mode. Therefore existing projects have to opt-in to it: open the Advanced category in Project Properties, and tick Use worker. You should see "[worker]" appear in the preview window title. Also if you create a project and choose to start with script instead of an event sheet, worker mode is disabled on the assumption you'll want to use DOM calls in your scripts, which aren't available in web workers. Finally it's not yet supported in Android apps or non-Chromium browsers yet, but hopefully those will all follow in due course.
All the work we've put in to this is also a sign of our commitment to make Construct games as robust and high-performance as possible. We aim not just to match the performance of native games, but exceed them. Taking advantage of significant technology upgrades like OffscreenCanvas are an important way of achieving that. Compatibility is important too, so if you don't see any change in how your gameplay works, that means we did a good job! However it does make it worth highlighting the effort and improvement that has gone in to this. Construct's engine is always improving internally and we have other significant technology upgrades on the horizon too, which hopefully I'll be writing about more in future as well. So rest assured we are always working hard behind the scenes to make Construct the most advanced web game engine on the market, and as time goes by it will continue to get better often without you having to do anything at all!