Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements support for parallel shader compilation and deferred program linking in the Impeller GLES backend. It updates PipelineCompileQueueGLES to track running jobs and notify when the queue is drained, and modifies PipelineLibraryGLES to manage pending pipelines and resolve them asynchronously. Unit tests are added to verify these changes. Feedback is provided regarding a potential dangling pointer issue in the unit tests where the global pointer deferred_links is left pointing to a destroyed local variable, suggesting an RAII helper to reset it.
| DeferredLinkState state; | ||
| state.fail_compile = fail_compile; | ||
| state.fail_link = fail_link; | ||
| deferred_links = &state; |
There was a problem hiding this comment.
The global pointer deferred_links is set to point to the local variable state. When RunDeferredLink exits, state is destroyed, leaving deferred_links as a dangling pointer. This can cause undefined behavior or crashes if subsequent tests or teardown procedures invoke mock GL functions that dereference it. Consider using a local RAII helper to automatically reset deferred_links to nullptr upon exiting the function.
deferred_links = &state;
struct ScopedReset {
~ScopedReset() { deferred_links = nullptr; }
} reset;There was a problem hiding this comment.
Fixed with local variant
fml::ScopedCleanupClosure reset_deferred_links(
[]() { deferred_links = nullptr; });|
@walley892 can you please review this? It has to do with speeding up shader compilation time with ANGLE. |
| if (pending_count >= library.max_pending_links_ || | ||
| !library.compile_queue_->IsProcessingJobs()) { | ||
| library.FinishPendingPipelines(*reactor); | ||
| } |
There was a problem hiding this comment.
I think this batched finishing can cause the following situation:
-
Pipeline A is popped from pending_jobs_ in pipeline_compile_queue.cc, starts linking, and is put into pending_pipelines_
-
Pipeline B is popped from pending_jobs_, starts linking, and is put into pending_pipelines_
-
Pipeline A is requested for immediate compilation and use (e.g.,
Pipeline::WaitAndGet->PipelineCompileQueue::PerformJobEagerly)
In this case, WaitAndGet will call PerformJobEagerly. Before, this would result in the pipeline being scheduled for immediate compilation. With these changes, PerformJobEagerly will return as a no-op, since Pipeline A isn't found in pending_jobs_. The upstream caller of WaitAndGet will block until pending_count >= library.max_pending_links and everything in pending_pipelines_ is finished (e.g., the compilation of a few more pipelines).
I can think of two ways to resolve this - there are maybe better ways :)
-
Wire up
PipelineCompileQueue::PerformJobEagerlyto be able to finish pipelines frompending_pipelines_directly. This seems kind of ugly, but maybe there's a nice way to do it. -
GL_KHR_parallel_shader_compilecomes withGL_COMPLETION_STATUS_KHR, a non-blocking version ofGL_LINK_STATUS. We can use that to scan throughpending_pipelines_each time this function is called, and immediately callFinishProgramon them ifGL_COMPLETION_STATUS_KHR == TRUE.
2 seems nice because faster pipelines and pipelines that have already completed compilation don't have to wait for pending_count >= library.max_pending_links, it may speed things up overall.
thoughts on this?
There was a problem hiding this comment.
Once link is started, the pipeline is no longer in pending_jobs_, so PerformJobEagerly can't help and WaitAndGet waits for the whole batch.
I went with option 2, thanks for this good finding, but it's overall win ended in very random ~-1-10% of startup time, which is 4% faster on average on real mini pc. Before each compile job adds its pipeline, it asks GL_COMPLETION_STATUS_KHR for the pending links and finishes the ones that completed. If no job runs afterwards, the drain check finishes them as before. If a driver rejects the query, the value stays GL_FALSE, and those links are checked at the limit or on drain like before. Tests: FinishesCompletedLinkBeforeLimit, KeepsLinkPendingWhenCompletionQueryFails.
Tested this again on both mini pc and vm:
Same two profile apps, flutter_gallery, timed from process start to its first visible window, and app with 8 FragmentProgram shaders, timed from process start to its first rasterized frame. Every comparison uses one engine binary with environment switches, 9 to 10 rounds per variant in rotating order.
The case you mentioned happens rarely. The raster thread waited on a started link in 2 of 8 gallery runs on the mini PC, 33 to 34 ms each, and in 1 of 5 on the VM, 17 ms. I never saw it in the 8 shader app no matter how many times i launched it (in more than 100 launches).
Results, median first frame o 8 shader app, ms:
| before | with the poll | |
|---|---|---|
| mini PC, 4 sessions | 1750 / 1701 / 1767 / 1665 | 1683 / 1675 / 1587 / 1645 |
| VM, 2 sessions | 1178 / 1152 | 1176 / 1167 |
On mini PC first frame comes 1-10% earlier, depending on the session, and never later. On VM the difference was too small to tell it apart from noise. The gallery start time did not change on either machine. After the first frame both versions run at about 120 FPS.
Also retested (again) closing the window while programs are still linking: 60 runs per machine, some of them with links slowed down on purpose. Every run exited normally with code 0 and no errors in the lo.
One more finding that I did not include. My first draft of this change was 3 to 8% faster than the final version. The reason was in debug log line in every job. Regardless of how ridiculous it sounds, writing it took about 0.4 ms, and that short pause of IO thread made the startup faster. Adding pause on purpose gives the same effect.
| pause when none of the pending links has finished yet | mini PC | VM |
|---|---|---|
| no pause (this PR) | 1638 ms | 1129 ms |
| std::this_thread::yield() | 1562 ms | 1024 ms |
| sleep_for(100us), which Windows turns into about 1.5 ms | 1446 ms | 1042 ms |
My guess why: without a pause, the IO thread checks again before the driver has finished any link. The pending list then reaches the limit, and the IO thread has to wait for the slowest link. With that pause, more links are already done when it checks. With the sleep, on mini PC the time spent waiting at the limit dropped down from 906 ms to 560 ms.
The pause looks awkwardly hacky, so I didn't include it, but I can add it if you want.
My guess is just a guess so I don't fully understand why it helps, and its effect depends on OS: on Windows sleep_for(100us) actually sleeps 1 to 2 ms, and yield() helped more on the VM than on my mini PC. It also makes the first frame faster to appear, while some links are still running, so right after the first frame there is one longer gap: 139 ms instead of 91 on the mini PC. The same gap appears without the pause whenever the first frame happens to come too early. In total more frames are shown by 3 s with the pause, 162 instead of 138.
Mini PC and VM are the same mentioned in description Ryzen AI 9 HX 370 with Radeon 890M, and Windows VM on Parallels on Macbook M4 Max
There was a problem hiding this comment.
Nice! I'm glad that this sped things up.
@gaaclarke, Thoughts on the behavior change in WaitAndGet -> PerformJobEagerly? IMO with the addition of the continuous polling, the behavior change is negligible.
walley892
left a comment
There was a problem hiding this comment.
Just a few nits, thanks for addressing the feedback!
|
@gaaclarke, LGTM - can I get a second set of eyes? |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements deferred pipeline compilation and linking for the GLES backend in Impeller using the GL_KHR_parallel_shader_compile extension. It updates PipelineCompileQueueGLES to track running jobs and support drain notifications, and modifies PipelineLibraryGLES to defer program linking and check status asynchronously. Comprehensive unit tests are added to verify these behaviors. The review feedback suggests simplifying the initialization of weak_compile_queue in PipelineLibraryGLES by replacing a ternary operator with a conditional assignment to improve readability.
| std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue = | ||
| async && supports_parallel_shader_compile_ | ||
| ? compile_queue_ | ||
| : std::shared_ptr<PipelineCompileQueueGLES>(); |
There was a problem hiding this comment.
The initialization of weak_compile_queue can be simplified and made more readable by default-constructing the std::weak_ptr and conditionally assigning it, rather than using a ternary operator with an explicit temporary std::shared_ptr construction. This avoids unnecessary temporary object creation and improves code clarity.
std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue;
if (async && supports_parallel_shader_compile_) {
weak_compile_queue = compile_queue_;
}There was a problem hiding this comment.
Didn't know the ternary makes a temporary shared_ptr copy 3 atomic ops instead of 1 with if / else
There was a problem hiding this comment.
It's because your last term is std::shared_ptr<PipelineCompileQueueGLES>(), not std::weak_ptr<PipelineCompileQueueGLES>(). Otherwise they are the same: https://godbolt.org/z/967Wo81nc
|
@walley892 cicd label dropped after push with requested changes, can you add it again? |
walley892
left a comment
There was a problem hiding this comment.
LGTM pending clarke's call on the WaitAndGet change and the CI run
gaaclarke
left a comment
There was a problem hiding this comment.
Okay, i've looked through all the code. This is looking really cool. In the future it would be helpful if something this level of complexity was preceeded with a small design doc. Wally and I both ended up having to draw up stuff to make sure we are good. I still have to do the design review part of the review.
| // deferred link also flags the shaders for deletion right away. GL frees them | ||
| // when they are detached or when the program is deleted, so they do not leak | ||
| // if FinishProgramLink never runs. | ||
| static bool StartProgramLink( |
There was a problem hiding this comment.
Switch this to return absl::StatusOr please.
| // deferred link also flags the shaders for deletion right away. GL frees them | ||
| // when they are detached or when the program is deleted, so they do not leak | ||
| // if FinishProgramLink never runs. | ||
| static bool StartProgramLink( |
There was a problem hiding this comment.
Can you switch the comment to standard docstring format. Spelling out what "deferred" means would be helpful.
| if (deferred) { | ||
| gl.DeleteShader(vert_shader); | ||
| gl.DeleteShader(frag_shader); | ||
| } | ||
| delete_vert_shader.Release(); | ||
| delete_frag_shader.Release(); |
There was a problem hiding this comment.
A comment about where these are deleted is worth mentioning.
|
|
||
| // Checks the result of StartProgramLink, then detaches the shaders. A shader | ||
| // of a deferred link is already flagged for deletion, so detaching frees it. | ||
| static bool FinishProgramLink( |
| return true; | ||
| } | ||
|
|
||
| static bool LinkProgram( |
| if (async && supports_parallel_shader_compile_) { | ||
| weak_compile_queue = compile_queue_; | ||
| } else { | ||
| weak_compile_queue = std::shared_ptr<PipelineCompileQueueGLES>(); |
There was a problem hiding this comment.
nit: based on what we learned previously from godbolt
| weak_compile_queue = std::shared_ptr<PipelineCompileQueueGLES>(); | |
| weak_compile_queue = std::weak_ptr<PipelineCompileQueueGLES>(); |
| // The queue whose job this thread runs right now. Two engines in one process | ||
| // have a queue each. With a flag instead of the queue, a job of one queue | ||
| // would make IsRunningJobOnCurrentThread return true for the other queue. | ||
| thread_local const PipelineCompileQueueGLES* tls_running_queue = nullptr; |
There was a problem hiding this comment.
No, I was thinking that we have the information we need, it just isn't getting plumbed down to the callback and we are recreating that information with the thread_local storage.
I think if we change the callback to PostJobForDescriptor to get called with a bool eager argument we'll have the same information.
using Job = std::function<void(bool eager)>;
bool PostJobForDescriptor(const PipelineDescriptor& desc, const Job& job);| // which is what makes a link expensive, and it is the only GLES driver the | ||
| // change was measured on. |
There was a problem hiding this comment.
Nit: Can you remove the specific reference to "the change" here. Comments should just reflect the current state of the codebase
| }; | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
| /// @brief Compiles both shaders and starts the program link. |
There was a problem hiding this comment.
Nit: drop the word "both"
There was a problem hiding this comment.
Rephrased "both" into what it means in 2 places
| std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue; | ||
| if (async && supports_parallel_shader_compile_) { | ||
| weak_compile_queue = compile_queue_; | ||
| } else { | ||
| weak_compile_queue = std::weak_ptr<PipelineCompileQueueGLES>(); | ||
| } | ||
| auto generation_task = [promise, weak_this, descriptor, vert_function, | ||
| frag_function, threadsafe, reactor]() { | ||
| frag_function, threadsafe, reactor, | ||
| weak_compile_queue](bool eager) { | ||
| auto thiz = weak_this.lock(); | ||
| if (!thiz) { | ||
| promise->set_value(nullptr); | ||
| return; | ||
| } | ||
| const bool result = reactor->AddOperation([promise, // | ||
| weak_this, // | ||
| descriptor, // | ||
| vert_function, // | ||
| frag_function, // | ||
| threadsafe // | ||
| const bool result = reactor->AddOperation([promise, // | ||
| weak_this, // | ||
| descriptor, // | ||
| vert_function, // | ||
| frag_function, // | ||
| threadsafe, // | ||
| weak_compile_queue, // | ||
| eager // | ||
| ](const ReactorGLES& reactor) { | ||
| // The job that runs this holds the queue, so the lock succeeds while a | ||
| // job of it runs on this thread. | ||
| auto compile_queue = weak_compile_queue.lock(); | ||
| if (!eager && compile_queue) { | ||
| // The promise is set later unless the pipeline fails right away. | ||
| if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function, | ||
| threadsafe, promise)) { | ||
| promise->set_value(nullptr); | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
I think the logic here and in CreatePipeline is difficult to follow because of the dependencies between variables and the particular logic of std::weak_ptr that makes this work.
IIUC, (!eager && compile_queue) checks that we're (1) not eagerly compiling and
(2a): - We have the lock on the compile_queue_
(2b): - We support_parallel_shader_compile
2a is immediately derivable from the surrounding code, but 2b only works because we've set weak_compile_queue ~30 lines before this (because async && supports_parallel_shader_compile_ == true) and because calling .lock() on an empty std::weak_ptr returns nullptr.
Then, in CreatePipeline, the logic that actually does the async linking is only active if deferred_promise != nullptr - which in turn depends on the complicated chain above.
I recommend breaking the logic here up a bit to be more clear to reason about what logic is being executed when we support parallel shader compile vs not. I'd also recommend adding a check and comment inside of CreatePipeline that ensures deferred_promise == true only if we support parallel shader compile.
There was a problem hiding this comment.
Actually, do we still need compile_queue now that we have eager if not, the check inside the job lambda could just be:
const bool defer_link =
!eager && library.SupportsParallelShaderCompile();
if (defer_link) {
// The promise is resolved later in FinishCompletedPipelines or
// FinishPendingPipelines unless starting the link fails right away.
if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function,
threadsafe, promise)) {
promise->set_value(nullptr);
}
return;
}
That + the comments and checks in CreatePipeline would make this a lot clearer
There was a problem hiding this comment.
Actually, do we still need
compile_queuenow that we haveeagerif not, the check inside the job lambda could just be:const bool defer_link = !eager && library.SupportsParallelShaderCompile(); if (defer_link) { // The promise is resolved later in FinishCompletedPipelines or // FinishPendingPipelines unless starting the link fails right away. if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function, threadsafe, promise)) { promise->set_value(nullptr); } return; }That + the comments and checks in CreatePipeline would make this a lot clearer
The lock only fails once the queue is gone, which shouldn't happen while the library lives, but we'd rather keep it as a guard than rely on that, i'll add a commit soon with requested changes + extra test
There was a problem hiding this comment.
Since design doc was requested, I kept this small so we don't change the same code twice, the extension check is now an explicit may_defer_link in the condition, CreatePipeline DCHECK it with comment, and AsyncJobWithoutExtensionLinksSynchronously covers this scenario
The confusing part was left over from the eager change, the weak_ptr lock used to tell whether we were inside a queue job
walley892
left a comment
There was a problem hiding this comment.
Oops, accidental approve
gaaclarke
left a comment
There was a problem hiding this comment.
Okay @krll-kov I've looked through this and I believe I'm beginning to have an understanding of what's going on. My initial instincts on how to handle this are slightly different. This is a valuable idea and has a good payoff. Ultimately though I don't want to land something into the engine that is complex to the level that it takes people multiple hours to digest what is happening. It just isn't clear from the code. Sometimes things are just complex too, there is no good way to write them in code to correct that.
I think we are going to have to step back and request a design doc. That serves 2 purposes:
- That allows us to make sure we are solving this problem the best way possible. (maybe we are already there)
- It documents the process to future developers.
The important sections to include for me in the design doc are:
- Alternatives considered
- Testing
Here's information on writing design docs: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md
Okay, I'll work on the design document, thanks for the guide |
This is my first design doc, correct me if something is wrong or missing so that i fix it. I added what was asked for, all the tests explanations and how i verified them, alternatives and what i tried within this approach, how to code works, explanation of terms, basic visualization, numbers, why something was chosen the way it's in code, WaitAndGet cost, etc. Do i need to create doc related issue and pr for it or you asked it for internal usage and it's fine like this? |
We can just link to it in #191860 |
|
Thanks @krll-kov, I've given it a look and gave some feedback. My hope is that the design doc will give us a higher level description that will inform how we can structure the code to make it more clear, or if we can't, provide guidance on it. I'm pretty sure this is technically correct. I have to make sure everything is clear though, not just technically correct. Let's give this design doc a chance, if this gets to the limit of what you are able to contribute at this time, maybe one of us on the team can take it over. Just let me know. You've done valuable work already pointing this out and prototyping it, I'd like to find a way to get a PR by you landed, but it just may become easier to pass it off so we can iterate on it quicker inside the team. |
Thanks for feedback, i've fixed what you asked for in docs and added code structure block as well that you mentioned here. It also includes pseudo-code for this solution and easier navigation with headings. As for the second part, I understand that this change may look big, and the team may see a simpler way to do it, since you know the engine much better than I do. My goal was never to get my own PR landed. I assumed this is not the top priority for the team right now, so I just wanted to help get the startup problem fixed sooner and reliably. I am ready to keep working on it through as many review rounds as it takes. But if I am slowing this down, or if reviewing code from someone outside the team takes you more time than writing it yourselves, I am completely fine with you taking it over, even if this solution will be used in the end with some tweaks. Authorship does not matter much to me, the result does. In that case I still will be able to answer questions about the design doc and test any version on my machines, if that's needed. |
|
Okay I spent some time thinking about how we can make this more clear and correct. Here's what I came up with. class
sequence
overview
I plan on implementing these changes in a separate PR forked from this one. I don't want to start editing this without being sure I'm happy with the final results (cc @walley892 ) |
Looks interesting, how would this work for flutter_gpu by the way? lib/gpu/render_pass.cc requests the pipeline with async=true and waits on the future directly, without PerformJobEagerly. If no other job is queued after it, nothing checks that link and the raster thread keeps waiting |
Yea, I think there are cases we'll have to route through the PerformEagerly logic. I haven't pulled on that thread yet but I don't think there is a problem unifying access in that way. |
|
Okay, I have an alternative queued up and hopefully we can get landed. It's split between a design doc, a refactor PR and the actual implementation. Thanks for all your help, we couldn't have done it without your investigative work. Give it a check.
|
This PR is just a refactor that lays down the groundwork for the parallel linking work as started in flutter#193066 Refactors: 1) Deleted PipelineLibraryQueue class, unifying the logic into their implementations. 1) Split out caching programs from creating pipelines, clearing up the logic. 1) Created a PendingProgram class to manage programs that are in the process of being linked. 1) Made queues private implementation details to the pipeline libraries, so they can be modified freely. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
I'm glad this helped and shaders problem is improved! |
> [!NOTE] > This PR depended on flutter#193985 which refactored code to make way for this change. This PR was authored in lieu of flutter#193066 and was informed by @krll-kov's great investigative work. design doc: https://github.com/flutter/rfc/blob/main/rfc/210.0001-parallel-gles-linking.md issue: flutter#191860 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This PR is just a refactor that lays down the groundwork for the parallel linking work as started in flutter#193066 Refactors: 1) Deleted PipelineLibraryQueue class, unifying the logic into their implementations. 1) Split out caching programs from creating pipelines, clearing up the logic. 1) Created a PendingProgram class to manage programs that are in the process of being linked. 1) Made queues private implementation details to the pipeline libraries, so they can be modified freely. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
> [!NOTE] > This PR depended on flutter#193985 which refactored code to make way for this change. This PR was authored in lieu of flutter#193066 and was informed by @krll-kov's great investigative work. design doc: https://github.com/flutter/rfc/blob/main/rfc/210.0001-parallel-gles-linking.md issue: flutter#191860 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md




Design doc and testing: https://docs.google.com/document/d/1eLrXEWoyWm8AZQBioqcHrQeVM2NAPDYbaEB4z_HOQs0
Windows starts Impeller on ANGLE, and ANGLE cross compiles every program to HLSL, so linking the built in programs dominates startup.
PipelineLibraryGLESlinks them one at a time on the IO task runner: each job callsglLinkProgramand then asks forGL_LINK_STATUS, and that query blocks until the driver finishes. The driver can link several programs at once when it advertisesGL_KHR_parallel_shader_compile, and this change lets it.The link is split in two. A compile queue job compiles both shaders and calls
glLinkProgramwithout asking for any status, and a later job on the same task runner checks the statuses and finishes the pipelines. The deferred path runs only inside aPipelineCompileQueueGLESjob, only for asynchronous requests, and only when the driver is ANGLE and advertises the extension. Every other case links as before, including synchronousGetPipeline. The compile queue itself changes on every GLES backend: it counts the jobs it runs and reports when it runs out of them, which costs one weak pointer lock per job.Two things bound the work. At most
std::clamp(hardware_concurrency / 2, 1, 4)links are started before the library checks them, the formulaContextVK::ChooseThreadCountForWorkersuses for the same reason (#143540). The checks run when the compile queue reports that it has no jobs left, so the programs still queued reach the driver before the first status query waits for one, and earlier when that limit is reached.Towards #191860. Also, this does not fix 193048, where a
FragmentProgramthat an application preloads is linked after the window is shown: the links run in parallel here, and the work still happens where it happens now, not before the first frame. Program linking is one part of the startup gap: on the mini PC the application with 8 shaders reaches Dartmainin 159 to 162 ms with this change and in 163 to 166 ms with the artifact, so whatever happens before the first pipeline request is untouched.Results
flutter_galleryand a local application with 8FragmentProgramshaders, both in profile, on two machines: a mini PC with a Ryzen AI 9 HX 370 and a Radeon 890M, 24 logical cores, Windows 11 at 120 Hz, and a Windows 11 virtual machine on Parallels on Apple silicon, where x64 runs through emulation and the GPU is virtual.Artifactis the engine artifact of the same commit. The median of 15 rounds unless the row says otherwise:The builds of this change are instrumented lab builds, see How it is measured. The gallery row comes from 5 rounds. On the mini PC it is the median time to the first visible window, which the Windows runner shows with the first frame. On the VM it is
timeToFirstFrameRasterizedfromstart_up_info.json, averaged the way devicelab averagesflutter_gallery_win_desktop__start_up, and counted from the engine start, so the two machines are not comparable with each other in that row.The mini PC shows the effect most clearly. The artifact starts in two modes there: 3 of 15 launches reach the first frame in 1619 to 1772 ms and the other 12 in 2540 to 2669 ms. With this change 13 of 15 launches take the fast mode. The VM, where x64 runs through emulation and the GPU is virtual, shows a smaller effect with a wider spread.
The Dart side pays for the change: on the VM
frameworkInitrises by 8 ms andtimeToFirstFrameby 30 ms, most likely because the driver threads now run next to the isolate while it starts.The fallback path shows no clear regression, though a small one is not ruled out. With the new path disabled, the same binary reaches the first frame before the artifact on both machines. Counted from the process start on the mini PC, it draws slightly fewer frames than the artifact in 5 of 7 paired rounds, 57 against 61 by 3.5 s and 238 against 245 by 5 s at the median, and two of those 5 rounds are fast launches of the artifact.
How it is measured
this changeandoffare the same lab build: it adds timing logs and diagnostic switches that do nothing unless their environment variable is set, and in the Windows compositor it carries the OpenGL ES 2 backing store format of [Windows] Use an unsized texture format for backing stores on OpenGL ES 2 #192917 and extra framebuffer andglGetErrorchecks on the first 5 presents. All runs here got an OpenGL ES 3 context.offdisables the new path through an environment variable, sothis changeagainstoffisolates the parallel link, while a comparison with the artifact also includes those extras.baseis a local build of the same tree without the change. Variants alternate inside every round, and the first launch after a dll swap is discarded.start_up_info.json:flutter run --profile --trace-startup --use-application-binary, the way devicelab'sStartupTestruns the startup benchmark, 5 iterations averaged.GetProcessTimesand places every frame by itsFramePhase.rasterFinishtimestamp relative to it. Counting from the process start and not from the first frame keeps a variant that moves work across the first frame from looking better or worse than it is.Per round results
Gallery, first frame, milliseconds. The mini PC columns are the time to the first visible window, the VM columns
timeToFirstFrameRasterized:On the VM the variants without the change scatter from 441 to 800 ms, and a few of their rounds come as early as the new path. The other fields of
start_up_info.jsonthere areframeworkInit73, 67, 64 and 65 ms andtimeToFirstFrame276, 250, 247 and 246 ms for this change, off, base and the artifact.The 8 shader application, 15 rounds, the median with the middle half of the rounds in brackets:
The same application through
start_up_info.jsonon the VM, 5 rounds averaged: 1190 ms with this change, 1614 ms with the path off and 1683 ms with the artifact. The rounds of the new path, 1135 to 1322 ms, do not overlap the rounds of the other two, 1587 to 1787 ms.The fallback path against the artifact on the mini PC, 7 rounds: the first frame at 2252 ms against 2569 ms, 57 against 61 frames drawn by 3.5 s and 238 against 245 by 5 s. On the VM its
timeToFirstFrameRasterizedis 630 ms against 734 ms for the base build and 680 ms for the artifact.Correctness
flutter_galleryand 8 scenes of a local bench (shader, multiply, savelayer, backdrop, imagefilter, colorfilter, atlas, text) render pixel identical screenshots before and after,compare -metric RMSEis 0 with the animation frozen at a fixed phase.Closing the window while programs are still linking exits cleanly: 30 runs of the gallery on each machine, 10 of them with every link slowed by 300 ms so that the close lands on a pending link, and every run exits by itself with code 0 and a clean log.
Shutdown runs
10 runs per variant, the close moment sweeping 100 to 1000 ms across the ten iterations:
closed inis the median time from the close message until the harness has seen the process exit and saved its log, so it includes that bookkeeping.pendingis the iterations that closed with links still unchecked and how many links they abandoned. The slowed variant closes later only because every pending link sleeps 300 ms before it is checked. On the mini PC the window also closes sooner with the new path; on the VM the difference is within the spread of its runs, 153 to 662 ms.A promise is always resolved. The library fails its pending pipelines when the reactor refuses the operation, when the reactor accepted it but could not react on this thread, and in the destructor. That matters because
PipelineFuture::WaitAndGetblocks the raster thread, so a promise nobody sets would hang it.ReactorGLESkeeps an operation on the thread that added it, and a thread that has given up its context may never react again, which is why the list of pending pipelines is checked rather than the return value ofAddOperation. The job a request posts holds the queue weakly, so a task runner that drops its tasks lets the queue go with the engine, and the queue runs what it still holds from its destructor throughFinishAllJobs, which resolves those promises.A program that is still linking is already in the cache.
CreatePipelinestores it under its key right afterglLinkProgram, so a second pipeline with the same shaders reuses it instead of compiling its own copy. Such a pipeline owns no shaders and checks no compile status: it asks for theGL_LINK_STATUSof the shared program, and that query waits for the link when it is still running. The synchronous path asks the same question, since a cached program can be linking in a compile queue job at that moment. When the link fails, or when the library gives up on a link it can no longer check, the pipeline that started it removes the entry, and only when the entry still holds that program, so a failed program stays out of the cache and every pipeline that reused it fails with it.The job counters live in the queue instance, not in globals, so two engines in one process do not see each other's jobs.
Tests
impeller_unittests, OpenGL ES set without the playground suites: 106 tests, 104 passed, 2 skipped, the same 2 that are skipped without this change. These tests drive the changed code through the GLES mock, so they need no GL context. Every wait in the new queue and deferred library tests has a 30 second timeout, so a regression fails instead of holding a bot. The two new playground tests wait on the pipeline future as the existing ones do.New tests
PipelineCompileQueueGLESTest.ReportsProcessingAndCallsOnDrainedcovers the drain notification and that a cleared callback is not called again.PipelineCompileQueueGLESTest.ReportsRunningJobOnlyInsideJobscovers the job flag inside jobs, inside descriptor jobs and in a plain task on the same thread.PipelineCompileQueueGLESTest.JobsOfOneQueueAreInvisibleToAnothercovers two queues in one process. Against a build with the counters in globals it fails on both assertions.PipelineLibraryGLESDeferredTest.FailsPendingPipelinesWhenTheReactorCannotReactcovers the pending pipeline whose link nobody can check. Against a build without that guard it hangs and fails on its timeout.PipelineLibraryGLESDeferredTest.DefersStatusAndSharesPendingProgramdrives two descriptors that share a program through the deferred path and counts the status queries, so the deferred path is covered through the public API and not only through the mock.PipelineLibraryGLESDeferredTest.FailedCompileRejectsBothSharedPipelinesandFailedLinkRejectsBothSharedPipelinescover a failure of each kind reaching both pipelines that share the program.PipelineLibraryGLESDeferredTest.AbandonedFailedLinkDoesNotPoisonProgramCachecovers a link that nobody checks because the reactor refused, and then a later request for the same shaders. Against a build where the abandoned pipelines keep their cache entry, the later request gets the failed program and returns null.PipelineLibraryGLESDeferredTest.ResolvesUnstartedPipelinesWhenTheRunnerDiscardsJobscovers a task runner that drops its tasks, which is whatConditionalBasicTaskRunnerdoes once the IO manager is gone. Against a build where the job holds its queue, the queue outlives the engine and the future is never resolved.The four tests that share a program drive the existing
MockGLESthrough a resolver of their own, and that resolver falls back tokMockResolverGLESfor everything it does not name. It replaces the entry points whose results the deferred path reads: the compile status, the link status, the program and shader names, and the info logs. The mock as it stands leaves the value ofglGetShaderivandglGetProgramivuntouched, so neither a link that succeeds nor a link that fails on purpose can be expressed through it. TeachingMockGLESImplthose methods, the way #192196 addedIsProgram, would remove the resolver and belongs in a change of its own.Scope and limits
Correct me if this should be done by platform only (Windows) check instead, since right now the deferred path is enabled only when the driver reports itself as ANGLE and advertises the extension. That is a driver check and not a platform check: an Android device whose GLES driver is ANGLE takes the same path, and no Android device was available to measure it. The Android GLES drivers have known trouble with runtime effect programs (#186186). Every other driver keeps the previous behaviour, and widening the condition to one of them is one line in
HasParallelShaderCompileonce it is measured.glMaxShaderCompilerThreadsKHRis not called, so the number of threads inside the driver is left at its default. This change only bounds how many links start before the library checks them.The
*OpenGLESplayground variants run on Windows, on the mini PC, where the driver gives the ES context through WGL. With the emoji, text and glyph groups excluded, 1236 of the remaining 1319 tests pass and 67 skip, none fail, the two new tests among them, and the run still stops 16 tests short, onTypographerTest.LazyAtlasTracksColor. Those groups are excluded because a test whose text reaches a color glyph through DirectWrite aborts in Skia withNo PNG decoder registered(SkScalerContext_win_dw.cpp), which this change does not touch. Upstream runs the set on Linux underxvfband excludes it from the macOS unit test run as covered by the golden tests, so CI keeps that coverage.Pre-launch Checklist