Repository navigation
Lower the memory of Compose, Flutter, Flet, Gio, Tauri, Dioxus, Wails and Capacitor - #22
Merged
Merged
Conversation
Without a real GPU, the OpenGL that Impeller draws with is Mesa's software rasterizer and its compiler, which take as much memory as the rest of the application. The Linux embedder of Flutter 3.47 draws with Skia on the processor when FLUTTER_LINUX_RENDERER=software, and Impeller has to be off for that: the engine stops with "Impeller DlText cannot be drawn to a Skia canvas" otherwise, which my_application.cc prevents with fl_dart_project_set_enable_impeller. main.cc sets the variable, and GDK_GL=disable so that GDK opens no OpenGL for its windows on Wayland, before GTK starts, unless they are already set. Takes the Home page from 339 to 172 MB of PSS, and the processor time until it is shown from 4 to 5 seconds to 0.6. The engine still creates its OpenGL contexts when it starts (fl_opengl_manager_new in fl_engine.cc), whatever it draws with, so Mesa's compiler is part of the 172 MB. check, e2e and screenshots still pass, and the pages look the same. Measured again with sandbox/sandbox.sh measure.
The Flutter engine of Flet 1.0 (3.44) takes FLUTTER_LINUX_RENDERER=software too, and does not need more for it: it does not stop on the text of Impeller as the engine of 3.47 does. tools/patch_build_template.py, which patches the cached template of flet build for the size of the window, now has main.cc of the template set FLUTTER_LINUX_RENDERER=software and GDK_GL=disable before GTK starts, unless they are already set, as the Flutter implementation does. It checks each patched file on its own, and sees that one is already patched by the new text, not by the absence of the old. Takes the Home page from about 340 to 216 MB of PSS (the same build with OpenGL: 332 to 356 MB in three runs). flet build keeps the project that it generated from the template, so a cache of an older build, such as the vfox-plus-sandbox-flet-build volume of the sandbox, has to be deleted for a changed template to take effect; the README says so. check and e2e still pass. Measured again with sandbox/sandbox.sh measure.
Skiko draws with OpenGL by default, which maps Mesa's software OpenGL and its compiler when there is no real GPU, and the optimizing compiler of the JIT (C2) keeps tens of megabytes of arenas of its own. -Dskiko.renderApi=SOFTWARE has Skia draw on the processor and -XX:TieredStopAtLevel=1 leaves C2 out. Each takes about 50 MB of PSS off the Home page, 253 to 129 MB together, and the processor time until the page is shown halves. The pages look the same. Heap sizes, the class data sharing archive that the jlink runtime of jpackage lacks, and the size of the code cache and of the stacks made no difference, or a worse one. check and e2e still pass (the end-to-end tests run in the test window of Compose, not with these arguments). Measured again with sandbox/sandbox.sh measure.
On top of WEBKIT_DISABLE_COMPOSITING_MODE (#20): WEBKIT_DISABLE_DMABUF_RENDERER=1 keeps WebKit from sharing buffers with the GPU, which maps Mesa's software OpenGL and its compiler in the processes of the web view when there is no real GPU, and JSC_useJIT=false leaves out the JIT compilers of the JavaScript engine, which have memory of their own in the web process. The pages of vfox+ need neither: the versions page opens as fast. GDK_GL=disable keeps GDK from opening OpenGL for its windows on Wayland (Tauri on a headless sway: 242 MB of PSS without it, 207 with it; no difference on X11). All four are set in lib.rs before the webview starts, unless they are already set. Takes the Home page from 262 to 208 MB of PSS; check and both e2e scenarios (fake and real) still pass. Measured again with sandbox/sandbox.sh measure.
Same as Tauri: WEBKIT_DISABLE_DMABUF_RENDERER=1, JSC_useJIT=false and GDK_GL=disable, with WEBKIT_DISABLE_COMPOSITING_MODE=1, set in main.rs before the webview starts, unless they are already set. Takes the Home page from 261 to 203 MB of PSS; check and both e2e scenarios (fake and real) still pass. Measured again with sandbox/sandbox.sh measure.
WEBKIT_DISABLE_DMABUF_RENDERER=1 and JSC_useJIT=false, as in Tauri and Dioxus. GTK 4 also draws its own window with OpenGL (its ngl renderer), which maps Mesa's software OpenGL and its compiler in the application when there is no real GPU: GDK_DISABLE=gl,vulkan keeps GDK from opening OpenGL and Vulkan, and GSK_RENDERER=cairo has GTK draw with Cairo. That was most of what WebKitGTK 6.0 with GTK 4 needed more than WebKitGTK 4.1 with GTK 3: Wails is 15 MB above Tauri now, not 71 (building with the gtk3 tag, the legacy path of Wails 3, would save 9 MB more). All five variables are set in main.go before the application starts, unless they are already set. Takes the Home page from 341 to 223 MB of PSS; check and both e2e scenarios (fake and real) still pass, and on a headless sway the pages are drawn as before. Measured again with sandbox/sandbox.sh measure.
On top of app.disableHardwareAcceleration() (#20): in-process-gpu puts the GPU thread in the browser process, NetworkServiceInProcess2 does the same for the network service, and --js-flags=--lite-mode has V8 leave out its optimizing compiler. Six processes become four, and the Home page goes from 305 to 267 MB of PSS. --no-zygote saved nothing in the test and needs the sandbox of Chromium off, and --single-process does not start the application. The README no longer says that the web view implementations cost more than this one: with the settings of Tauri, Dioxus and Wails they are 203 to 223 MB. The experiment that hosted this front end in the shell of Tauri (422 MB) predates them, and is marked so. check and both e2e scenarios (fake and real) still pass. Measured again with sandbox/sandbox.sh measure.
The heap of Go was 57 MB for 22 MB that were live, 12 of them copies of the same tables. Every one of the three weights of the variable font of the interface parsed the font again, because Gio tells faces apart by their *Font. They now get a Font of their own that is a copy of the one parsed once, which shares its tables, and main.go has the collector run at 25 % growth instead of 100 %, so that the heap stays near what the application holds (10 to 12 MB). The heap of Go goes from 57 to 20 MB, and the Home page from 200 to 165 MB of PSS, in a number that varies by 20 MB from one run to the next with Mesa's llvmpipe, which is most of it: Gio only draws with OpenGL. check and e2e still pass, and the pages look the same. Measured again with sandbox/sandbox.sh measure.
docs/memory.md has the numbers of the commits before, and of mygo and Uno, which did not change (mygo 41 MB of PSS on the Home page, Uno 98), measured on the same day, one after another, with nothing else running (sandbox/sandbox.sh measure of each): Compose 129, Gio 165, Flutter 172, Dioxus 203, Tauri 208, Flet 216, Wails 223 and Capacitor 267. The same code before the commits took 253, 200, 339, 261, 262, about 340, 341 and 305. mygo is still the lowest by far, now by a factor of 2.4 to 6.5. The Results bullets and the No GPU paragraph say what each implementation did and what is left: Gio still draws with OpenGL, the engine of Flutter and the processes of WebKit still create OpenGL contexts, and on Wayland GDK opens OpenGL for the windows of GTK 3 unless told not to. Uno's README has its own numbers of the day (98 and 113 MB); the others are in their own commits.
Memory use at
|
| Implementation | Technology | Processes | Home PSS | Home RSS | Versions PSS | Versions RSS | The list adds (PSS) |
|---|---|---|---|---|---|---|---|
| mygo | mygo 0.5, Go, GTK 3 (purego), CPU renderer | 1 | 42 (0) | 56 | 50 (0) | 65 | +8 |
| Uno | Uno Platform 6, C# on .NET 10, Skia on the CPU, Native AOT | 1 | 101 (0) | 109 | 127 (-1) | 135 | +26 |
| Compose | Compose Multiplatform, Kotlin on the JVM, Skia | 1 | 129 (-128) | 134 | 152 (-116) | 158 | +23 |
| Flutter | Flutter 3.47, Dart, Impeller (OpenGL ES), GTK 3 | 4 | 162 (-159) | 207 | 167 (-159) | 212 | +5 |
| Gio | Gio, Go, OpenGL ES through EGL | 1 | 172 (-21) | 197 | 178 (-18) | 203 | +6 |
| Flet | Flet, Python 3.14 in a Flutter application | 4 | 214 (-87) | 253 | 222 (-107) | 260 | +8 |
| Dioxus | Dioxus 0.7, Rust, WebKitGTK, compositing off | 3 | 227 (-47) | 427 | 244 (-61) | 445 | +17 |
| Tauri | Tauri 2, Rust, WebKitGTK 4.1 (GTK 3), compositing off | 3 | 235 (-51) | 408 | 252 (-56) | 426 | +17 |
| Wails | Wails 3, Go, WebKitGTK 6.0 (GTK 4), compositing off | 3 | 250 (-103) | 422 | 273 (-101) | 437 | +23 |
| Capacitor | Ionic Vue and Capacitor in Electron, no hardware GPU | 4 (-2) | 299 (-24) | 607 | 308 (-23) | 620 | +9 |
In MB, for the application and all of its child processes, sorted by the PSS of the Home page; "Versions" is the SDKs page with the available versions of Node.js listed. A number in parentheses is the difference from main, in bold when it is more than the noise of CI. "(main)" marks an implementation that this pull request did not rebuild: its numbers are those of main.
Details
- Measured by
shared/tools/measure.sh(Flutter:flutter/tool/measure.py) in the workflow of each implementation, on a GitHub-hosted runner with Xvfb and no GPU. These are not the numbers ofdocs/memory.md, which come from another machine: compare the rows of one comment. - The same code does not use the same memory in every CI run, so a difference counts as a change only when it is more than 5 MB and more than 5% of the number on main (8% for Compose and Flet, 12% for Gio, 15% for Flutter, which vary more).
- Runs of this pull request: mygo, Uno, Compose, Flutter, Gio, Flet, Dioxus, Tauri, Wails, Capacitor.
- Latest runs on main: mygo (
79ef675), Uno (79ef675), Compose (79ef675), Flutter (79ef675), Gio (79ef675), Flet (79ef675), Dioxus (79ef675), Tauri (79ef675), Wails (79ef675), Capacitor (79ef675).
linghengqian
marked this pull request as draft
October 23, 2026 14:03
On 2026-10-23 the Ubuntu mirror of the runners was throttled to about 40 KB/s: the index (3 MB) still came at 10 MB/s, but the 280 packages of the Wails workflow, 94 MB, took 45 minutes, the timeout of the job, where the same step took a minute on 2026-10-19. The runs of #22 show it: Wails was cancelled after 45 minutes in "Install the Linux packages", the packaging job of Flutter hit its 25 minutes in the same kind of step, in two attempts, and Uno (36 minutes), Dioxus (30) and Capacitor (21) spent most of their time there. Nothing else of those jobs was slow. The throttle seems to be for each connection: the rate stayed at about 40 KB/s from 12:23 to 12:38 UTC while the number of jobs that were downloading fell from eleven to five. apt downloads from one host over one connection, one file after the other. The workflows install their packages with the new action .github/actions/install-linux-packages: * It keeps the downloaded archives in a cache, for each list of packages and each week, so that a day with a slow mirror costs the first run only. The newest cache of any earlier week starts the next one, and apt-get autoclean removes the archives of superseded versions. * install.sh downloads what the cache lacks eight files at a time with curl (fetch.sh, from the addresses that apt prints with --print-uris, with the address of the mirror file of the runners filled in), then has apt fetch what is left and check every archive, and installs from the archives, which needs no network and no time limit. A download has a time limit of five minutes and three attempts, and keeps what it got. A request that gets no data for 30 seconds fails and is tried again, up to three times; apt waited for two minutes. Where the files cannot be fetched this way, apt does all of it, as before. * A run that cannot finish saves what it downloaded under another key, so that the next run, such as a re-run of the failed jobs, starts from there and gets further each time. The job that runs the end-to-end tests of Flutter no longer installs patchelf, lintian and desktop-file-utils, which only the job that builds the packages uses. Dependabot also looks at the action, which pins actions too. Tested in an ubuntu:26.04 container with the mirror of this network and a mirror file like that of the runners: a cold run, a run from the saved archives (0 B to download), eight connections against one when curl is limited to 150 kB/s each (all of 164 MB by curl in about two and a half minutes, where one connection ran into the time limit after 47 MB), archive names with an epoch (%3a), an attempt that ran out of time, and an installation that fails after its last attempt; the package list of the end-to-end job of Flutter installs its toolchain. actionlint 1.7.12 and ShellCheck 0.11 pass. Not tested on a runner: the cache steps, and whether the throttle is for each connection.
The Wails job of #22 failed with the first version of the action (run 39971828235): "Could not download the packages in 3 attempts of 5m" after 30 minutes, and nothing was saved. The log shows what went wrong: * fetch.sh wrote each file from its first byte, over what the attempt before it had got. libwebkitgtk-6.0-4 is 25.5 MB, 10 minutes at the 43 KB/s that apt got over one connection of that runner, and an attempt had 5, so it could not end however often it was tried. The mirror also cut a connection after a few minutes, which nothing continued. * Eight connections got 138 KB/s together (41.5 MB in 5 minutes), not eight times 43 KB/s, and four downloads did not connect in 10 seconds. In the same minute other jobs downloaded 90 MB from the same mirror in seconds: only some runners are slow. * The save of what had been downloaded failed, because tar cannot read an archive that the user of the runner has no right to (libkdb5-10t64, one of 280), so that the next run would have started from nothing. fetch.sh now: * continues what is in partial/ with a range request, so that a connection that is cut, or dropped, loses nothing; * takes the mirrors of /etc/apt/apt-mirrors.txt in order, drops a connection that gets less than 256 KB/s for 10 seconds, and goes on with the next mirror. When every mirror was slow, it goes on with the best of them and drops only a connection that stalls. A file that no mirror sends in three rounds is given up; * ends at one deadline for the whole download, 15 minutes, instead of attempts of 5 minutes that each started over. install.sh starts the largest files first, so that none is left alone at the end; * checks the SHA256 hash that apt prints (-o Acquire::ForceHash=SHA256) before it moves an archive into the directory. apt takes an archive of the right size that is there for a good one and does not check it: in a test, dpkg failed on one with a changed byte. The archives that are there already, from the cache, are checked too, and replaced when they are wrong. apt prints only the archives that are missing, so install.sh shows it an empty directory to get all of them. The action makes the archives readable before it saves the cache, also when the installation failed. .github/scripts/test_fetch.py runs fetch.sh against local mirrors that are slow, cut their connections, have nothing or send the wrong file; the step of repository.yml that discovers the tests of that directory runs it (15 seconds). In an ubuntu:26.04 container with a file of mirrors like that of the runners, whose first mirror does not answer: a cold run, a warm run, an archive with a changed byte, a download that ends at its deadline and is continued by the next run, and no file of mirrors. actionlint, ShellCheck and ruff pass. Not tested on a runner: the restore of the cache, and the speed of the other mirrors when the first is slow.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
sandbox.sh measureof each on the same day, one after another, before and after,FLUTTER_LINUX_RENDERER=software, and Impeller off in Flutter 3.47, which stops on its own text otherwise). Flutter also uses a tenth of the processor time until the Home page is shown.flet buildkeeps the project it generated from the template, so a cache of an older build has to be deleted for the patched template to take effect, the README says so.