diff --git a/CHANGELOG.md b/CHANGELOG.md index 188490a4ef..ead583c27d 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 freezes when opening projects, and EDT freezes when the theme is changed and opening embedded DevTools (#8477) ## 87.1.0 diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index 2a0b5b2a4a..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,8 +58,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 +76,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 +255,59 @@ 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. - return; - } + 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 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 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); + 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(); + final DtdUtils dtdUtils = new DtdUtils(); + dtdUtils.readyDtdService(project) + .thenAccept(dtdService -> { if (dtdService == null) { - log().error("Unable to send theme changed event because DTD service is null"); + log().warn("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); - } - }, 1, TimeUnit.SECONDS); - } + 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 -> { + log().debug("DTD not ready; skipping themeChanged event", e); + return null; + }); + }, 1, TimeUnit.SECONDS); } private void checkSdkVersionNotification(@NotNull Project project) { 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; + }); } } 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() { 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