From 11b0fd28b83974de7e9d08a46ede8df943f1ed48 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 11:57:28 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=90=9B=20Fix=20EDT=20freezes=20with?= =?UTF-8?q?=20the=20FlutterInitializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/io/flutter/FlutterInitializer.java | 98 ++++++++++--------- .../io/flutter/FlutterInitializerTest.java | 57 +++++++++++ 2 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 testSrc/unit/io/flutter/FlutterInitializerTest.java diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index 2a0b5b2a4a..fbc7d62b1c 100644 --- a/src/io/flutter/FlutterInitializer.java +++ b/src/io/flutter/FlutterInitializer.java @@ -60,7 +60,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -78,6 +78,10 @@ public class FlutterInitializer extends FlutterProjectActivity { private @NotNull AtomicLong lastScheduledThemeChangeTime = new AtomicLong(); + // Shared scheduler to avoid creating/closing executors on EDT + @NotNull + private final ScheduledExecutorService scheduler = AppExecutorUtil.getAppScheduledExecutorService(); + @Override public void executeProjectStartup(@NotNull Project project) { log().info("Executing Flutter plugin startup for project: " + project.getName()); @@ -253,57 +257,55 @@ private void sendThemeChangedEvent(@NotNull Project project) { lastScheduledThemeChangeTime.set(requestTime); // Schedule event to be sent in a second if nothing more recent has come in. - try (var executor = Executors.newSingleThreadScheduledExecutor()) { - executor.schedule(() -> { - if (lastScheduledThemeChangeTime.get() != requestTime) { - // A more recent request has been set, so drop this request. + scheduler.schedule(() -> { + if (lastScheduledThemeChangeTime.get() != requestTime) { + // A more recent request has been set, so drop this request. + return; + } + + final JsonObject params = new JsonObject(); + params.addProperty("eventKind", "themeChanged"); + params.addProperty("streamId", "Editor"); + + final JsonObject themeData = new JsonObject(); + final DevToolsUtils utils = new DevToolsUtils(); + themeData.addProperty("isDarkMode", Boolean.FALSE.equals(utils.getIsBackgroundBright())); + themeData.addProperty("backgroundColor", utils.getColorHexCode()); + themeData.addProperty("fontSize", utils.getFontSize().intValue()); + + final JsonObject eventData = new JsonObject(); + eventData.add("theme", themeData); + params.add("eventData", eventData); + + try { + final DtdUtils dtdUtils = new DtdUtils(); + final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); + if (dtdService == null) { + log().error("Unable to send theme changed event because DTD service is null"); return; } - final JsonObject params = new JsonObject(); - params.addProperty("eventKind", "themeChanged"); - params.addProperty("streamId", "Editor"); - - final JsonObject themeData = new JsonObject(); - final DevToolsUtils utils = new DevToolsUtils(); - themeData.addProperty("isDarkMode", Boolean.FALSE.equals(utils.getIsBackgroundBright())); - themeData.addProperty("backgroundColor", utils.getColorHexCode()); - themeData.addProperty("fontSize", utils.getFontSize().intValue()); - - final JsonObject eventData = new JsonObject(); - eventData.add("theme", themeData); - params.add("eventData", eventData); - - try { - final DtdUtils dtdUtils = new DtdUtils(); - final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); - if (dtdService == null) { - log().error("Unable to send theme changed event because DTD service is null"); - return; - } - - dtdService.sendRequest("postEvent", params, false, object -> { - JsonObject result = object.getAsJsonObject("result"); - if (result == null) { - log().error("Theme changed event returned null result"); - return; - } - JsonPrimitive type = result.getAsJsonPrimitive("type"); - if (type == null) { - log().error("Theme changed event result type is null"); - return; - } - if (!"Success".equals(type.getAsString())) { - log().error("Theme changed event result: " + type.getAsString()); - } + dtdService.sendRequest("postEvent", params, false, object -> { + JsonObject result = object.getAsJsonObject("result"); + if (result == null) { + log().error("Theme changed event returned null result"); + return; } - ); - } - catch (WebSocketException | InterruptedException | ExecutionException e) { - log().error("Unable to send theme changed event", e); - } - }, 1, TimeUnit.SECONDS); - } + JsonPrimitive type = result.getAsJsonPrimitive("type"); + if (type == null) { + log().error("Theme changed event result type is null"); + return; + } + if (!"Success".equals(type.getAsString())) { + log().error("Theme changed event result: " + type.getAsString()); + } + } + ); + } + catch (WebSocketException | InterruptedException | ExecutionException e) { + log().error("Unable to send theme changed event", e); + } + }, 1, TimeUnit.SECONDS); } private void checkSdkVersionNotification(@NotNull Project project) { diff --git a/testSrc/unit/io/flutter/FlutterInitializerTest.java b/testSrc/unit/io/flutter/FlutterInitializerTest.java new file mode 100644 index 0000000000..7bd3c0adae --- /dev/null +++ b/testSrc/unit/io/flutter/FlutterInitializerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter; + +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link FlutterInitializer}. + */ +public class FlutterInitializerTest { + + @Test + public void testInitializerCanBeCreated() { + // Test that we can create FlutterInitializer without issues + // This validates that the shared scheduler field is properly initialized + FlutterInitializer initializer = new FlutterInitializer(); + assertNotNull("FlutterInitializer should be created successfully", initializer); + } + + @Test + public void testSchedulerFieldExists() throws Exception { + // Test that the scheduler field exists and is properly initialized + FlutterInitializer initializer = new FlutterInitializer(); + + Field schedulerField = FlutterInitializer.class.getDeclaredField("scheduler"); + schedulerField.setAccessible(true); + + Object scheduler = schedulerField.get(initializer); + assertNotNull("Scheduler field should be initialized", scheduler); + assertTrue("Scheduler should be a ScheduledExecutorService", + scheduler instanceof ScheduledExecutorService); + } + + @Test + public void testDebounceFieldExists() throws Exception { + // Test that the debounce field exists and is properly initialized + FlutterInitializer initializer = new FlutterInitializer(); + + Field debounceField = FlutterInitializer.class.getDeclaredField("lastScheduledThemeChangeTime"); + debounceField.setAccessible(true); + + Object debounceTimer = debounceField.get(initializer); + assertNotNull("Debounce timer field should be initialized", debounceTimer); + assertTrue("Debounce timer should be an AtomicLong", + debounceTimer instanceof AtomicLong); + } +} \ No newline at end of file From 9556f1a0285aeb973134ba766f233d3c1227e44b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 11:58:42 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=90=9B=20Fix=20EDT=20freezes=20with?= =?UTF-8?q?=20the=20EmbeddedJxBrowser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flutter/jxbrowser/EmbeddedJxBrowser.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java b/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java index 1470ef74f4..7528067789 100644 --- a/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java +++ b/src/io/flutter/jxbrowser/EmbeddedJxBrowser.java @@ -129,6 +129,8 @@ public class EmbeddedJxBrowser extends EmbeddedBrowser { "Waiting for JxBrowser installation timed out. Restart your IDE to try again."; private static final String INSTALLATION_WAIT_FAILED = "The JxBrowser installation failed unexpectedly. Restart your IDE to try again."; private static final int INSTALLATION_WAIT_LIMIT_SECONDS = 30; + + @NotNull private final AtomicReference engineRef = new AtomicReference<>(null); private final Project project; @@ -173,19 +175,22 @@ private EmbeddedJxBrowser(@NotNull Project project) { } @Override - public @Nullable EmbeddedTab openEmbeddedTab(ContentManager contentManager) { + public @Nullable EmbeddedTab openEmbeddedTab(@NotNull ContentManager contentManager) { manageJxBrowserDownload(contentManager); - if (engineRef.get() == null) { - engineRef.compareAndSet(null, EmbeddedBrowserEngine.getInstance().getEngine()); - } + // Do NOT initialize the JxBrowser Engine on the EDT. + // Historically, we tried to 'fallback' and construct the Engine here when it's null: + // engineRef.compareAndSet(null, EmbeddedBrowserEngine.getInstance().getEngine()); + // That path synchronously triggers Engine.newInstance(...) which blocks (CountDownLatch.await) + // and can freeze the Event Dispatch Thread (see #8394). + // + // Proceed only when the engine has been initialized by the async installation callback. final Engine engine = engineRef.get(); if (engine == null) { - showMessageWithUrlLink(jxBrowserErrorMessage(), contentManager); + // Show an "installation in progress" UX instead of attempting synchronous engine creation. + handleJxBrowserInstallationInProgress(contentManager); return null; } - else { - return new EmbeddedJxBrowserTab(engine); - } + return new EmbeddedJxBrowserTab(engine); } private @NotNull String jxBrowserErrorMessage() { From 5390e6d50c5e67fed61de27bbd3169441401fc06 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 12:01:38 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoring=20`DtdUtil?= =?UTF-8?q?s`=20to=20be=20independent=20across=20projects=20and=20not=20sl?= =?UTF-8?q?eeping=20threads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/io/flutter/dart/DtdUtils.java | 54 +++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/io/flutter/dart/DtdUtils.java b/src/io/flutter/dart/DtdUtils.java index d719b58198..e0ad727b07 100644 --- a/src/io/flutter/dart/DtdUtils.java +++ b/src/io/flutter/dart/DtdUtils.java @@ -6,34 +6,48 @@ package io.flutter.dart; import com.intellij.openapi.project.Project; +import com.intellij.util.concurrency.AppExecutorUtil; import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; import org.jetbrains.annotations.NotNull; -import java.util.concurrent.CompletableFuture; +import java.util.Map; +import java.util.concurrent.*; public class DtdUtils { + private static final Map> WAITERS = new ConcurrentHashMap<>(); + public @NotNull CompletableFuture readyDtdService(@NotNull Project project) { - final DartToolingDaemonService dtdService = DartToolingDaemonService.getInstance(project); - CompletableFuture readyService = new CompletableFuture<>(); - int attemptsRemaining = 10; - final int TIME_IN_BETWEEN = 2; - while (attemptsRemaining > 0) { - attemptsRemaining--; + return WAITERS.computeIfAbsent(project, p -> { + final DartToolingDaemonService dtdService = DartToolingDaemonService.getInstance(project); + CompletableFuture readyService = new CompletableFuture<>(); + if (dtdService.getWebSocketReady()) { readyService.complete(dtdService); - break; - } - try { - Thread.sleep(TIME_IN_BETWEEN * 1000); + return readyService; } - catch (InterruptedException e) { - readyService.completeExceptionally(e); - break; - } - } - if (!readyService.isDone()) { - readyService.completeExceptionally(new Exception("Timed out waiting for DTD websocket to start")); - } - return readyService; + + final ScheduledExecutorService scheduler = AppExecutorUtil.getAppScheduledExecutorService(); + + final ScheduledFuture poll = scheduler.scheduleWithFixedDelay(() -> { + if (readyService.isDone()) return; + if (dtdService.getWebSocketReady()) { + readyService.complete(dtdService); + } + }, 0, 500, TimeUnit.MILLISECONDS); + + final ScheduledFuture timeout = scheduler.schedule(() -> { + readyService.completeExceptionally(new Exception("Timed out waiting for DTD websocket to start")); + }, 20, TimeUnit.SECONDS); + + readyService.whenComplete((s, t) -> { + poll.cancel(false); + timeout.cancel(false); + if (t != null) { + WAITERS.remove(p); + } + }); + + return readyService; + }); } } From 7e9d5ce52b8b5bd991006d5322c7c1f412c9445a Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 12:02:41 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20blocking=20cause?= =?UTF-8?q?d=20by=20`dtdUtils.readyDtdService(project).get()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/io/flutter/FlutterInitializer.java | 61 ++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index fbc7d62b1c..d6692b24e6 100644 --- a/src/io/flutter/FlutterInitializer.java +++ b/src/io/flutter/FlutterInitializer.java @@ -277,34 +277,39 @@ private void sendThemeChangedEvent(@NotNull Project project) { eventData.add("theme", themeData); params.add("eventData", eventData); - try { - final DtdUtils dtdUtils = new DtdUtils(); - final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); - if (dtdService == null) { - log().error("Unable to send theme changed event because DTD service is null"); - return; - } - - dtdService.sendRequest("postEvent", params, false, object -> { - JsonObject result = object.getAsJsonObject("result"); - if (result == null) { - log().error("Theme changed event returned null result"); - return; - } - JsonPrimitive type = result.getAsJsonPrimitive("type"); - if (type == null) { - log().error("Theme changed event result type is null"); - return; - } - if (!"Success".equals(type.getAsString())) { - log().error("Theme changed event result: " + type.getAsString()); - } - } - ); - } - catch (WebSocketException | InterruptedException | ExecutionException e) { - log().error("Unable to send theme changed event", e); - } + final DtdUtils dtdUtils = new DtdUtils(); + dtdUtils.readyDtdService(project) + .thenAccept(dtdService -> { + if (dtdService == null) { + log().warn("Unable to send theme changed event because DTD service is null"); + return; + } + try { + dtdService.sendRequest("postEvent", params, false, object -> { + JsonObject result = object.getAsJsonObject("result"); + if (result == null) { + log().error("Theme changed event returned null result"); + return; + } + JsonPrimitive type = result.getAsJsonPrimitive("type"); + if (type == null) { + log().error("Theme changed event result type is null"); + return; + } + if (!"Success".equals(type.getAsString())) { + log().error("Theme changed event result: " + type.getAsString()); + } + }); + } + catch (WebSocketException e) { + log().error("Unable to send theme changed event", e); + } + }) + .exceptionally(e -> { + // DTD 未就绪或超时:降级为 debug,避免在启动期污染日志/打断流程 + log().debug("DTD not ready; skipping themeChanged event", e); + return null; + }); }, 1, TimeUnit.SECONDS); } From 788fc5785a36a5c9b48c677d46e4685dd93ff1b5 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 12:04:08 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=93=9D=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188490a4ef..1fe83f1900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Made dev release daily instead of weekly - Set the device selector component to opaque during its creation to avoid an unexpected background color (#8471) - Refactored `DeviceSelectorAction` and add rich icons to different platform devices (#8475) +- Fix DTD & EDT freezes when the theme changed and opening embedded DevTools (#8477) ## 87.1.0 From 63d48af9e7d72a55b2b35437afaa86726561afe9 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 13:47:21 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=94=A5=20--?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/io/flutter/FlutterInitializer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index d6692b24e6..da2d0ac1df 100644 --- a/src/io/flutter/FlutterInitializer.java +++ b/src/io/flutter/FlutterInitializer.java @@ -29,7 +29,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.messages.MessageBusConnection; -import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; import de.roderick.weberknecht.WebSocketException; import io.flutter.android.IntelliJAndroidSdk; import io.flutter.bazel.WorkspaceCache; @@ -59,7 +58,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -306,7 +304,6 @@ private void sendThemeChangedEvent(@NotNull Project project) { } }) .exceptionally(e -> { - // DTD 未就绪或超时:降级为 debug,避免在启动期污染日志/打断流程 log().debug("DTD not ready; skipping themeChanged event", e); return null; }); From 5081731be628d7b1d5c83c87470bcb859226c12b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 20 Aug 2025 14:22:06 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9D=20++?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe83f1900..ead583c27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Made dev release daily instead of weekly - Set the device selector component to opaque during its creation to avoid an unexpected background color (#8471) - Refactored `DeviceSelectorAction` and add rich icons to different platform devices (#8475) -- Fix DTD & EDT freezes when the theme changed and opening embedded DevTools (#8477) +- Fix DTD freezes when opening projects, and EDT freezes when the theme is changed and opening embedded DevTools (#8477) ## 87.1.0