Repository navigation
Description
Steps to reproduce
This is reproducible in the engine's native embedder tests, without an app or plugin.
- Check out flutter/flutter at 3829a94 and sync engine dependencies.
- Add the regression test included below to engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc. Leave the production implementation unchanged.
- From engine/src, configure and build:
python flutter/tools/gn --unoptimized --no-lto
ninja -C out/host_debug_unopt embedder_unittests
out/host_debug_unopt/embedder_unittests.exe --gtest_filter=EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThreadExpected results
After synchronous engine shutdown, the resource context has been cleared on the I/O thread that made it current.
Actual results
The test-only baseline fails at both post-shutdown assertions:
context.resource_released: Actual false, Expected true
context.resource_current: Actual true, Expected false
1 FAILED TEST
EXIT=1
Applying the Skia release hook makes the same test pass. Validation:
Windows x64, host_debug_unopt (--unoptimized --no-lto), source 3829a944235cd8e25142d72302dc89db4cd16934:
- New regression with unchanged production code: failed both expected release assertions (exit 1).
- With the two Skia production changes: the same regression passed (exit 0).
- Full
embedder_unittests: 180 passed, 8 skipped, 1 pre-existing disabled test; no failures (exit 0). - Shutdown regression repeated 100 times: all passed (exit 0).
- Changed-line clang-format and
git diff --check: passed.
The eight skips are five AOT-only checks in this JIT build and three golden checks restricted to Linux. No test was disabled or skip condition changed by this patch. Other platforms remain for upstream CI.
Cause and proposed fix
Shell teardown already calls PlatformViewEmbedder::ReleaseResourceContext on the I/O thread after releasing the last IO manager. PlatformViewEmbedder forwards this to the surface. EmbedderSurfaceGLSkia does not override the default no-op, so its resource context remains current. EGL can defer destruction while the context remains current on a thread.
Implement the existing release hook in EmbedderSurfaceGLSkia and clear the current context through the existing embedder callback. GLES Impeller already implements analogous cleanup; this report does not claim a new Impeller fix.
The EGL reference specifies that context destruction is deferred until the context is no longer current to any thread: https://registry.khronos.org/EGL/sdk/docs/man/html/eglDestroyContext.xhtml
Related prior investigation: #138893 (comment) identified the same EGL thread-binding mechanism on December 12, 2023. The reporter closed the issue on July 18, 2024, and it was locked on August 1, 2024; no fixing PR is linked there. This focused issue provides a standalone native reproduction of the missing Skia resource-context release at 3829a94.
Code sample
diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
index c3e254eedc2..10aa0a555e8 100644
--- a/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
+++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc
@@ -6,7 +6,10 @@
#define FML_USED_ON_EMBEDDER
#include <atomic>
+#include <mutex>
#include <string>
+#include <thread>
+#include <utility>
#include <vector>
#include "GLES3/gl3.h"
@@ -52,6 +55,64 @@ TEST_F(EmbedderTest, CanCreateOpenGLRenderingEngine) {
ASSERT_TRUE(engine.is_valid());
}
+namespace {
+
+class ResourceReleaseTestContext final : public EmbedderTestContextGL {
+ public:
+ explicit ResourceReleaseTestContext(std::string assets_path)
+ : EmbedderTestContextGL(std::move(assets_path)) {
+ auto& config = GetRendererConfig().open_gl;
+ make_resource_ = config.make_resource_current;
+ clear_current_ = config.clear_current;
+ config.make_resource_current = [](void* data) {
+ auto* self = static_cast<ResourceReleaseTestContext*>(
+ static_cast<EmbedderTestContext*>(data));
+ const bool current = self->make_resource_(data);
+ if (current) {
+ std::scoped_lock lock(self->mutex_);
+ self->resource_thread_ = std::this_thread::get_id();
+ self->resource_current = true;
+ }
+ return current;
+ };
+ config.clear_current = [](void* data) {
+ auto* self = static_cast<ResourceReleaseTestContext*>(
+ static_cast<EmbedderTestContext*>(data));
+ const bool cleared = self->clear_current_(data);
+ std::scoped_lock lock(self->mutex_);
+ if (cleared && self->resource_thread_ == std::this_thread::get_id()) {
+ self->resource_current = false;
+ self->resource_released = true;
+ }
+ return cleared;
+ };
+ }
+ std::atomic<bool> resource_current = false;
+ std::atomic<bool> resource_released = false;
+
+ private:
+ BoolCallback make_resource_;
+ BoolCallback clear_current_;
+ std::mutex mutex_;
+ std::thread::id resource_thread_;
+};
+
+} // namespace
+
+TEST_F(EmbedderTest, ShutdownReleasesOpenGLResourceContextOnItsThread) {
+ ResourceReleaseTestContext context(GetFixturesDirectory());
+ EmbedderConfigBuilder builder(context);
+ builder.AddCommandLineArgument("--enable-impeller=false");
+ builder.SetSurface(DlISize(1, 1));
+ auto engine = builder.LaunchEngine();
+ ASSERT_TRUE(engine.is_valid());
+ ASSERT_TRUE(context.resource_current);
+ ASSERT_FALSE(context.resource_released);
+ engine.reset();
+ EXPECT_TRUE(context.resource_released);
+ EXPECT_FALSE(context.resource_current);
+}
+
//------------------------------------------------------------------------------
/// If an incorrectly configured compositor is set on the engine, the engine
/// must fail to launch instead of failing to render a frame at a later point inLogs
Full output from the regression before and after the fix follows. Both runs emit the same NativeSymbolResolver (SymInitialize 87) diagnostic; it does not prevent either test run. EXIT records the process exit code.
Before the fix
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
Note: Google Test filter = EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from EmbedderTest
[ RUN ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
Failed to init NativeSymbolResolver (SymInitialize 87)
../../flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc(112): error: Value of: context.resource_released
Actual: false
Expected: true
../../flutter/shell/platform/embedder/tests/embedder_gl_unittests.cc(113): error: Value of: context.resource_current
Actual: true
Expected: false
[ FAILED ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread (334 ms)
[----------] 1 test from EmbedderTest (334 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (334 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
1 FAILED TEST
EXIT=1
After the fix
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
Note: Google Test filter = EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from EmbedderTest
[ RUN ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread
Failed to init NativeSymbolResolver (SymInitialize 87)
[ OK ] EmbedderTest.ShutdownReleasesOpenGLResourceContextOnItsThread (348 ms)
[----------] 1 test from EmbedderTest (348 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (349 ms total)
[ PASSED ] 1 test.
EXIT=0
Environment
- Windows 11 Pro, x64, build 26200; physical desktop.
- Installed graphics adapters: NVIDIA GeForce RTX 4090 (driver 32.0.15.9186) and AMD Radeon Graphics (32.0.21043.5001).
- Engine source: 3829a94.
- Build: host_debug_unopt, Skia explicitly selected by the regression test.
- Compiler: Flutter-vended Fuchsia clang 24.0.0git, revision 07d67299a15ce03b053736e2d31a668ee0576987.
- No framework CLI command is needed for this native-engine reproduction.
Flutter Doctor output
This reproduction runs the native engine test executable directly, without the Flutter framework CLI or an application project. The engine revision, build configuration, compiler and host details are listed above; flutter doctor from a separate installed SDK would describe a different build.
Earlier downstream measurements (separate engine revision)
On Flutter 4cf2416, repeated Windows Skia window/engine creation and destruction reached 1,121,796,096 private committed bytes after 200 cycles with the stock engine. With the source repair, 1,000 cycles settled at 243,449,856 bytes (267,878,400 after warm-up), measured while the parent process was still running.
These are downstream application measurements from the pinned revision, not memory benchmarks of current upstream. The native test above is the standalone reproduction for this report.
Activity
mbcorona commentedon Sep 30, 2026
Confirmed on master. EmbedderSurfaceGLImpeller overrides ReleaseResourceContext but EmbedderSurfaceGLSkia does not, so it inherits the base no op and the Skia resource context stays current after shutdown. Per the EGL spec destruction is deferred while a context remains current, which is the leak. Matches your native repro.
Routing to team-engine.
[engine] Release the Skia OpenGL resource context on shutdown (flutte…