Repository navigation
Conversation
|
This pull request targets the Please change the base branch of this pull request to |
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a persistent program cache for compiled GL programs on Windows to improve startup performance. It implements the ProgramCache and ProgramCacheLocation classes under the egl namespace, leveraging the EGL_ANDROID_blob_cache extension to store ANGLE-compiled shaders on disk. The changes expose a program_cache_path configuration option across FlutterDesktopEngineProperties, DartProject, and FlutterProjectBundle, and integrate cache initialization into FlutterWindowsEngine. Comprehensive unit tests are also added to verify the caching behavior and location resolution. There are no review comments to address, and I have no feedback to provide.
On Windows, Impeller draws through ANGLE, which compiles every GL program with the Direct3D shader compiler when it is linked. The engine links its built-in programs at startup, so every launch spends most of its startup time in that compiler, and apps pay again for each runtime effect. ANGLE supports persistent program binaries through EGL_ANDROID_blob_cache when the application registers the callbacks. The Windows embedder now registers a disk-backed cache right after eglInitialize, before any context exists. - ANGLE's keys cover the shader sources, its program format version, the GPU and driver version (GL_RENDERER) and bindings, and it rejects a binary whose adapter or feature level differs. A rejected or undecodable entry is relinked and replaced. - One file per entry, with the key, sizes and a checksum, written through a temporary file and an atomic rename on a background thread. A failing file reads as a miss and is deleted. - The default folder is %LOCALAPPDATA%\<CompanyName>\<ProductName>\flutter_program_cache, named from the executable's version resource like path_provider's application cache folder. FlutterDesktopEngineProperties::program_cache_path and DartProject::set_program_cache_path() move it, or turn it off when empty; --program-cache-path=<folder> overrides it in debug and profile builds. - Entries live in a folder per engine version; the first write of a new version removes other versions' cache files. The total is bounded to 32 MiB, least recently used first. Part of flutter#191860.
- Pass the 64-bit value to swprintf_s without a cast to unsigned long long (google-runtime-int); uint64_t is that type on Windows. - Reserve the concurrency test's thread vector before filling it (performance-inefficient-vector-operation).
3cc50eb to
a58a060
Compare
|
In my tests with caching in original issue i ended up with a lof of unresolved decisions especially for cleanups, corruptions, sharing between different apps and paths. Hope all those will be covered Personal questions after reading the PR (not tested, just from a look at the code):
I'm not from flutter team, do not request to change/fix that, just asking for curiosity |
gaaclarke
left a comment
There was a problem hiding this comment.
@0nilinkz you'll have to sign the CLA to land this PR.
Adding a cache comes with a lot of bug vectors. It's not the sort of decision we'd take lightly. I'd like to see the measurement for this after #193066 since we are almost certainly going to land that and that may change the calculus for deciding to accept this additional risk or not.
Please respond to #193419 (comment). @krll-kov 's opinion on this matter is respected and he raises some valid questions =)
…folder Follow-ups from review of the program cache: - Builds on different engine versions can run side by side. The first write of a process removed the other versions' folders, so two builds of one app kept wiping each other's cache. A version's folder is now removed only once all of its entries and temporary files are older than 30 days. A hit refreshes an entry at most daily, and a temporary file is a write in progress, so a version in use keeps its folder. - A cache folder deleted while the process runs, for example when the app clears its cache folder, is created again by the next write. Before, nothing reached the disk until the process restarted. - Test engines keep no program cache. WindowsConfigBuilder and four test helpers created engines with the default cache. The engines of a process share one cache through the EGL display, so later tests' engines wrote programs to %LOCALAPPDATA%\flutter_windows_unittests, where a later run could load them instead of compiling them. A full run of flutter_windows_unittests now leaves no folder. - The public headers say that the engines of a process share one cache.
An instance-local index misses writes from another instance and access times refreshed after its first write. Rescan the current version before trimming and count an entry as removed only when Windows confirms deletion. Rejected: Maintain a process-local eviction index | misses other processes and refreshed file timestamps Confidence: high Scope-risk: narrow Directive: Eviction remains best effort when files cannot be deleted; do not claim a transactional cross-process size bound Tested: Three new regression tests fail on ac45a70 and pass with the fix; 430 Windows embedder tests and 29 client-wrapper tests pass; debug and profile builds; clang-format; git diff --check Not-tested: Fresh first-frame timings after the eviction change; maintainer CI and multi-process simultaneous-write stress
|
I think ReadEntry asks for DELETE access, which then fails with if (handle == INVALID_HANDLE_VALUE &&
- ::GetLastError() == ERROR_ACCESS_DENIED) {
+ (::GetLastError() == ERROR_ACCESS_DENIED ||
+ ::GetLastError() == ERROR_SHARING_VIOLATION)) {
|
|
@gaaclarke @krll-kov — following up, and thanks for the detailed feedback. The PR description now includes comparisons with #193066 applied: the recorded first-frame medians were 507 → 144 ms for flutter_gallery and 1,212 → 329 ms for the real app with a warm cache. Those measurements are from ac45a70, before the eviction follow-up; they are not fresh timings for the current head or the latest revision of #193066. The description also summarises the earlier changes for retaining recently used engine-version folders, recreating a deleted cache directory, and disabling the cache in test engines. The later ERROR_SHARING_VIOLATION fallback and reader/replacement comment feedback remain outstanding. The deliberately mismatched-blob result is also a useful recovery concern, even though it was not reproduced through ordinary corruption or concurrent use. Would an explicitly opt-in initial implementation, with the outstanding fixes and refreshed measurements, be an acceptable direction to pursue? Or would you prefer to defer the cache decision until #193066 has landed? The CLA check and merge conflicts are still outstanding too; this is a request for guidance on the design direction, not a claim that the current PR is ready to land. |
Resolve the EGL manager and Windows engine conflicts from flutter#193133 by passing both the upstream surface-orientation flag and the optional program cache. Keep cache installation after eglInitialize and before context creation. Validation: clang-format checks on resolved ranges, whitespace checks, call-site review, and unchanged cache implementation/tests. Native Windows build and unit tests were not run on this macOS host.
…e-level cap master added an optional D3D11 feature-level cap to egl::Manager::Create, its constructor and InitializeDisplay, in the same place as this change's program cache parameter, and a matching FlutterProjectBundle setting next to the cache's. Keep both: the cap comes before the cache in the parameter lists, so master's call sites are unchanged, and the engine passes the project's cap and its cache together.
A virus scanner or a backup can hold an entry open without sharing write or delete access. Opening the entry with the rights to refresh or delete it then fails with ERROR_SHARING_VIOLATION, which read as a miss and made ANGLE compile the program again. Read it without those rights instead, as already happens when access is denied. Also correct the comment on the readers' sharing mode: a reader does not block eviction, but replacing an entry fails while a reader has it open, and the old entry stays. A test checks that such a failed replace keeps a readable entry and leaves no temporary file.


On Windows, Impeller draws through ANGLE on Direct3D 11, and linking a GL
program runs the Direct3D shader compiler (
D3DCompile). The engine createsits default pipelines when it starts, so every launch compiles the same
programs again: about 1.5 s of
D3DCompilecritical path on a Ryzen 7 2700Xwith a GTX 1080. On 3.47 this runs before
main(); onmasterit runs on theIO thread, and the first frame waits for about half of it.
ANGLE supports persistent program binaries through
EGL_ANDROID_blob_cachewhen the application registers the callbacks. This PR registers a disk-backed
cache in the Windows embedder right after
eglInitialize, before any contextexists:
program format version,
GL_RENDERER(GPU, device id, driver version) andbindings, and loading rejects a binary whose adapter or feature level
differs. A rejected or undecodable entry is relinked and replaced.
a temporary file and an atomic rename on a background thread (started by
the first write, so a warm launch starts no thread). A failing file reads
as a miss and is deleted.
%LOCALAPPDATA%\<CompanyName>\<ProductName>\flutter_program_cache, namedfrom the executable's version resource like
path_provider's applicationcache folder, falling back to the executable name. It is per user.
FlutterDesktopEngineProperties::program_cache_pathandDartProject::set_program_cache_path()move it, or turn it off whenempty;
--program-cache-path=<folder>overrides it in debug and profilebuilds. The callbacks carry no context, so the engines of a process share
one cache; the headers say to give every engine the same setting.
versions can run side by side. The first write of a process removes
another version's folder once all of its files are older than 30 days
(only files the cache writes, and a folder only once it is empty). A
folder deleted while the app runs is created again by the next write. Each version targets 32 MiB, least recently used first, and a single entry is limited to 4 MiB. Eviction rescans current entries and hit timestamps, and only counts successful deletions; it is best effort under filesystem failures and concurrent writers.
The following measurements were captured at cache commit ac45a70, before the eviction rescan in 191ae6e. Fresh first-frame timings for 191ae6e remain unverified; these are earlier measurements, not a current-head timing guarantee. The real-app runs included an App Check banner in all variants and establish renderer comparison only, not app readiness.
Measured with #193066 (parallel links), which this complements:
master2abcfdc6with #193066's head applied, with and without this change, each appbuilt once with only
flutter_windows.dllswapped, alternating profilelaunches on a Ryzen 7 2700X with a GTX 1080 (driver 32.0.15.8266), Windows 11.
Medians: the window of 10 untraced launches, and engine start to first frame
rasterized (
start_up_info.json) of 5 traced ones. "Empty" clears the cachebefore the launch.
flutter_gallerywindowflutter_galleryfirst frameWith a filled cache the app also uses about 2.0 s (gallery) and 2.7 s (the
real app) less CPU in its first seconds: #193066 spreads the compiles across
cores, and the cache removes them. Ranges, CPU and the cache without #193066
are in the review thread.
Without #193066, on
master2abcfdc6, profile builds of the real app,alternating fresh launches, same machine:
2abcfdc6(Three alternating launches each; the app holds its first frame for one extra
program of its own.
--trace-startupstops at the first frame, which onmastercomes after 34–43 of the 72 links; the rest link on the IO threadafterwards.)
On 3.47.2 with the same change, engine start to
main()fell from1,702–1,760 ms to 222–228 ms on warm launches and the window appeared
1.7 s sooner; a probe counted 146
D3DCompilecalls stock and none warm.The first launch after an install, an update or a driver change costs
~0.1 s more (ANGLE serialising and compressing each program).
Part of #191860.
Tests: 26 store and location tests in
egl/program_cache_unittests.cc(round trip across instances, the size-then-copy protocol, corrupted,
truncated and mismatched files, size limits, eviction order, versions kept
side by side and removed after 30 days unused, cleanup that never touches
foreign files, stale temporary files, a folder deleted while running,
concurrent use, folder resolution, interleaved cache instances, hits after the first write, and locked entries that cannot be deleted), and pass-through tests for the project
bundle,
DartProjectandFlutterEngine. Every engine the embedder's testscreate keeps no cache (
WindowsTest.TestEnginesKeepNoProgramCachechecks theconfig builder). In
host_debug_unopt,flutter_windows_unittests(430tests; 6 more are disabled upstream) and
client_wrapper_windows_unittests(29) pass, and a full run leaves no cache folder. clang-tidy with the
engine's
.clang-tidyreports no diagnostics on the changed lines. Two diagnostics originate in MSVC standard-library exception/filesystem internals reached by existing tests; they are not suppressed by this patch. Debug and profile Windows builds pass. The three new eviction tests were run against ac45a70 first and all three failed, then all passed with 191ae6e.Review follow-up
... An absolute path is preferable for an explicitly external location. This GPU/driver-specific cache is not intended for roaming between machines.Pre-launch Checklist
///).AI-assisted with Claude Code and Codex. Human-review checklist remains for the contributor to complete.