diff --git a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt index 23bb536a0ca2..d7e2752dd3bc 100644 --- a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt +++ b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt @@ -4,7 +4,10 @@ @file:Suppress("RAW_RUN_BLOCKING") package com.intellij.platform.ide.bootstrap +import com.intellij.diagnostic.COROUTINE_DUMP_HEADER import com.intellij.diagnostic.LoadingState +import com.intellij.diagnostic.dumpCoroutines +import com.intellij.diagnostic.enableCoroutineDump import com.intellij.ide.* import com.intellij.ide.bootstrap.InitAppContext import com.intellij.ide.gdpr.EndUserAgreement @@ -47,6 +50,7 @@ import com.intellij.util.PlatformUtils import com.intellij.util.io.URLUtil import com.intellij.util.io.createDirectories import com.intellij.util.lang.ZipFilePool +import com.jetbrains.JBR import kotlinx.coroutines.* import org.jetbrains.annotations.ApiStatus.Internal import org.jetbrains.annotations.VisibleForTesting @@ -178,13 +182,23 @@ internal suspend fun loadApp(app: ApplicationImpl, } } + val coroutineDebugJob = launch(CoroutineName("coroutine debug probes init")) { + enableCoroutineDump().onFailure { e -> + LOG.error("Cannot enable coroutine debug dump", e) + } + enableJstack() + } + asyncScope.launch { - launch(CoroutineName("checkThirdPartyPluginsAllowed")) { + // do not use launch here - don't overload CPU, let some room for JIT and other CPU-intensive tasks during start-up + coroutineDebugJob.join() + + span("checkThirdPartyPluginsAllowed") { checkThirdPartyPluginsAllowed() } // doesn't block app start-up - launch(CoroutineName("post app init tasks")) { + span("post app init tasks") { runPostAppInitTasks() } @@ -197,6 +211,17 @@ internal suspend fun loadApp(app: ApplicationImpl, } } +private suspend fun enableJstack() { + span("coroutine jstack configuration") { + JBR.getJstack()?.includeInfoFrom { + """ +$COROUTINE_DUMP_HEADER +${dumpCoroutines(stripDump = false)} +""" + } + } +} + private suspend fun preInitApp(app: ApplicationImpl, asyncScope: CoroutineScope, initLafJob: Job, diff --git a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/IdeStartupWizard.kt b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/IdeStartupWizard.kt index de9847ca2eeb..f1ef74fe22b2 100644 --- a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/IdeStartupWizard.kt +++ b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/IdeStartupWizard.kt @@ -26,9 +26,9 @@ private val LOG: Logger get() = logger() val isIdeStartupWizardEnabled: Boolean - get() = !ApplicationManagerEx.isInIntegrationTest() - && System.getProperty ("intellij.startup.wizard", "true").toBoolean() - && IdeStartupExperiment.isExperimentEnabled() + get() = !ApplicationManagerEx.isInIntegrationTest() && + System.getProperty ("intellij.startup.wizard", "true").toBoolean() && + IdeStartupExperiment.isExperimentEnabled() @ExperimentalCoroutinesApi internal suspend fun runStartupWizard(isInitialStart: Job, app: Application) { @@ -174,9 +174,9 @@ private object IdeStartupExperiment { @Suppress("DEPRECATION") private fun getGroupKind(group: Int) = when { - PlatformUtils.isIdeaUltimate() || PlatformUtils.isPyCharmPro() -> when { - group in 0..7 -> GroupKind.Experimental - group == 8 || group == 9 -> GroupKind.Control + PlatformUtils.isIdeaUltimate() || PlatformUtils.isPyCharmPro() -> when (group) { + in 0..7 -> GroupKind.Experimental + 8, 9 -> GroupKind.Control else -> GroupKind.Undefined } else -> when (group) { @@ -186,12 +186,13 @@ private object IdeStartupExperiment { } } - private fun String.asBucket() = MathUtil.nonNegativeAbs(this.hashCode()) % 256 + private fun asBucket(s: String) = MathUtil.nonNegativeAbs(s.hashCode()) % 256 + private fun getBucket(): Int { val deviceId = LOG.runAndLogException { DeviceIdManager.getOrGenerateId(object : DeviceIdManager.DeviceIdToken {}, "FUS") } ?: return 0 - return deviceId.asBucket() + return asBucket(deviceId) } val experimentGroup by lazy { @@ -204,7 +205,7 @@ private object IdeStartupExperiment { experimentGroup } - val experimentGroupKind by lazy { + val experimentGroupKind: GroupKind by lazy { getGroupKind(experimentGroup) } diff --git a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/config.kt b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/config.kt new file mode 100644 index 000000000000..0af0633af0bf --- /dev/null +++ b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/config.kt @@ -0,0 +1,160 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.ide.bootstrap + +import com.intellij.accessibility.enableScreenReaderSupportIfNecessary +import com.intellij.ide.gdpr.EndUserAgreement +import com.intellij.idea.AppMode +import com.intellij.openapi.application.ConfigImportHelper +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.application.impl.RawSwingDispatcher +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.util.IconLoader +import com.intellij.openapi.util.registry.EarlyAccessRegistryManager +import com.intellij.platform.diagnostic.telemetry.impl.span +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import java.nio.file.Path +import java.util.concurrent.CancellationException + +internal suspend fun importConfigIfNeeded(isHeadless: Boolean, + configImportNeededDeferred: Deferred, + lockSystemDirsJob: Job, + logDeferred: Deferred, + args: List, + targetDirectoryToImportConfig: Path?, + appStarterDeferred: Deferred, + euaDocumentDeferred: Deferred, + initLafJob: Job): Job? { + if (isHeadless) { + importConfigHeadless(configImportNeededDeferred = configImportNeededDeferred, + lockSystemDirsJob = lockSystemDirsJob, + logDeferred = logDeferred, + args = args, + targetDirectoryToImportConfig = targetDirectoryToImportConfig, + appStarterDeferred = appStarterDeferred, + euaDocumentDeferred = euaDocumentDeferred) + return null + } + + if (AppMode.isRemoteDevHost() || !configImportNeededDeferred.await()) { + return null + } + + initLafJob.join() + val log = logDeferred.await() + importConfig( + args = args, + targetDirectoryToImportConfig = targetDirectoryToImportConfig ?: PathManager.getConfigDir(), + log = log, + appStarter = appStarterDeferred.await(), + euaDocumentDeferred = euaDocumentDeferred, + ) + + if (ConfigImportHelper.isNewUser()) { + enableNewUi(logDeferred) + + if (isIdeStartupWizardEnabled) { + log.info("Will enter initial app wizard flow.") + val result = CompletableDeferred() + isInitialStart = result + return result + } + else { + return null + } + } + else { + return null + } +} + +private suspend fun importConfig(args: List, + targetDirectoryToImportConfig: Path, + log: Logger, + appStarter: AppStarter, + euaDocumentDeferred: Deferred, + headlessAutoImport: Boolean = false) { + if (headlessAutoImport) { + // headless AppStarters are not notified about config import + val veryFirstStartOnThisComputer = euaDocumentDeferred.await() != null + withContext(RawSwingDispatcher) { + try { + ConfigImportHelper.importConfigsTo(veryFirstStartOnThisComputer, targetDirectoryToImportConfig, args, log, true) + log.info("Automatic config import completed") + } + catch (e: UnsupportedOperationException) { + log.info("Automatic config import is not possible", e) + } + } + EarlyAccessRegistryManager.invalidate() + IconLoader.clearCache() + return + } + + span("screen reader checking") { + runCatching { + enableScreenReaderSupportIfNecessary() + }.getOrLogException(log) + } + + span("config importing") { + appStarter.beforeImportConfigs() + + val veryFirstStartOnThisComputer = euaDocumentDeferred.await() != null + withContext(RawSwingDispatcher) { + ConfigImportHelper.importConfigsTo(veryFirstStartOnThisComputer, targetDirectoryToImportConfig, args, log) + } + appStarter.importFinished(targetDirectoryToImportConfig) + EarlyAccessRegistryManager.invalidate() + IconLoader.clearCache() + } +} + +private suspend fun enableNewUi(logDeferred: Deferred) { + if (System.getProperty("ide.experimental.ui") == null) { + try { + EarlyAccessRegistryManager.setAndFlush(mapOf("ide.experimental.ui" to "true")) + } + catch (e: CancellationException) { + throw e + } + catch (e: Throwable) { + logDeferred.await().error(e) + } + } +} + +private suspend fun importConfigHeadless(configImportNeededDeferred: Deferred, + lockSystemDirsJob: Job, + logDeferred: Deferred, + args: List, + targetDirectoryToImportConfig: Path?, + appStarterDeferred: Deferred, + euaDocumentDeferred: Deferred) { + if (!configImportNeededDeferred.await()) { + return + } + + // make sure we lock the dir before writing + lockSystemDirsJob.join() + if (!ConfigImportHelper.isHeadlessAutomaticConfigImportAllowed()) { + enableNewUi(logDeferred) + } + else { + val log = logDeferred.await() + importConfig( + args = args, + targetDirectoryToImportConfig = targetDirectoryToImportConfig ?: PathManager.getConfigDir(), + log = log, + appStarter = appStarterDeferred.await(), + euaDocumentDeferred = euaDocumentDeferred, + headlessAutoImport = true + ) + if (ConfigImportHelper.isNewUser()) { + enableNewUi(logDeferred) + } + } +} diff --git a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/main.kt b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/main.kt index 9899b85970d2..de854a7e2c39 100644 --- a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/main.kt +++ b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/main.kt @@ -5,11 +5,9 @@ package com.intellij.platform.ide.bootstrap import com.intellij.BundleBase -import com.intellij.accessibility.enableScreenReaderSupportIfNecessary import com.intellij.diagnostic.* import com.intellij.ide.* import com.intellij.ide.bootstrap.* -import com.intellij.ide.gdpr.EndUserAgreement import com.intellij.ide.instrument.WriteIntentLockInstrumenter import com.intellij.ide.plugins.PluginManagerCore import com.intellij.idea.* @@ -22,10 +20,8 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.IconLoader import com.intellij.openapi.util.ShutDownTracker import com.intellij.openapi.util.SystemInfoRt -import com.intellij.openapi.util.registry.EarlyAccessRegistryManager import com.intellij.platform.diagnostic.telemetry.impl.OpenTelemetryConfigurator import com.intellij.platform.diagnostic.telemetry.impl.TelemetryManagerImpl import com.intellij.platform.diagnostic.telemetry.impl.span @@ -41,7 +37,6 @@ import com.intellij.util.lang.ZipFilePool import com.jetbrains.JBR import io.opentelemetry.sdk.OpenTelemetrySdkBuilder import kotlinx.coroutines.* -import kotlinx.coroutines.CancellationException import org.jetbrains.annotations.ApiStatus.Internal import java.awt.Toolkit import java.lang.invoke.MethodHandles @@ -58,7 +53,6 @@ import java.util.function.BiConsumer import java.util.function.BiFunction import java.util.logging.ConsoleHandler import java.util.logging.Level -import kotlin.concurrent.Volatile import kotlin.system.exitProcess internal const val IDE_STARTED: String = "------------------------------------------------------ IDE STARTED ------------------------------------------------------" @@ -89,6 +83,10 @@ private val commandProcessor: AtomicReference<(List) -> Deferred? = null private set +@Volatile +@JvmField +internal var isInitialStart: CompletableDeferred? = null + // the main thread's dispatcher is sequential - use it with care @OptIn(ExperimentalCoroutinesApi::class) fun CoroutineScope.startApplication(args: List, @@ -133,6 +131,7 @@ fun CoroutineScope.startApplication(args: List, val initBaseLafJob = launch { initUi(initAwtToolkitJob = initAwtToolkitJob, isHeadless = isHeadless, asyncScope = this@startApplication) } + if (!isHeadless) { val initUiScale = launch { if (SystemInfoRt.isMac) { @@ -147,7 +146,9 @@ fun CoroutineScope.startApplication(args: List, } } - scheduleShowSplashIfNeeded(lockSystemDirsJob = lockSystemDirsJob, initUiScale = initUiScale, appInfoDeferred = appInfoDeferred, + scheduleShowSplashIfNeeded(lockSystemDirsJob = lockSystemDirsJob, + initUiScale = initUiScale, + appInfoDeferred = appInfoDeferred, args = args) scheduleUpdateFrameClassAndWindowIconAndPreloadSystemFonts(initAwtToolkitJob = initAwtToolkitJob, initUiScale = initUiScale, @@ -201,67 +202,24 @@ fun CoroutineScope.startApplication(args: List, } } - scheduleLoadSystemLibsAndLogInfoAndInitMacApp(logDeferred, appInfoDeferred, initLafJob, args, mainScope) + scheduleLoadSystemLibsAndLogInfoAndInitMacApp(logDeferred = logDeferred, + appInfoDeferred = appInfoDeferred, + initUiDeferred = initLafJob, + args = args, + mainScope = mainScope) val euaDocumentDeferred = async { loadEuaDocument(appInfoDeferred) } val configImportDeferred: Deferred = async { - if (isHeadless) { - if (!configImportNeededDeferred.await()) { - return@async null - } - // make sure we lock the dir before writing - lockSystemDirsJob.join() - if (!ConfigImportHelper.isHeadlessAutomaticConfigImportAllowed()) { - enableNewUi(logDeferred) - } - else { - val log = logDeferred.await() - importConfig( - args = args, - targetDirectoryToImportConfig = targetDirectoryToImportConfig ?: PathManager.getConfigDir(), - log = log, - appStarter = appStarterDeferred.await(), - euaDocumentDeferred = euaDocumentDeferred, - headlessAutoImport = true - ) - if (ConfigImportHelper.isNewUser()) { - enableNewUi(logDeferred) - } - } - return@async null - } - - if (AppMode.isRemoteDevHost() || !configImportNeededDeferred.await()) { - return@async null - } - - initLafJob.join() - val log = logDeferred.await() - importConfig( - args = args, - targetDirectoryToImportConfig = targetDirectoryToImportConfig ?: PathManager.getConfigDir(), - log = log, - appStarter = appStarterDeferred.await(), - euaDocumentDeferred = euaDocumentDeferred, - ) - - if (ConfigImportHelper.isNewUser()) { - enableNewUi(logDeferred) - - if (isIdeStartupWizardEnabled) { - log.info("Will enter initial app wizard flow.") - val result = CompletableDeferred() - isInitialStart = result - result - } - else { - null - } - } - else { - null - } + importConfigIfNeeded(isHeadless = isHeadless, + configImportNeededDeferred = configImportNeededDeferred, + lockSystemDirsJob = lockSystemDirsJob, + logDeferred = logDeferred, + args = args, + targetDirectoryToImportConfig = targetDirectoryToImportConfig, + appStarterDeferred = appStarterDeferred, + euaDocumentDeferred = euaDocumentDeferred, + initLafJob =initLafJob) } val pluginSetDeferred = async { @@ -295,7 +253,7 @@ fun CoroutineScope.startApplication(args: List, Class.forName(OpenTelemetrySdkBuilder::class.java.name, true, classLoader) } - val appLoaded = launch { + val appLoaded = async { val initEventQueueJob = scheduleInitIdeEventQueue(initAwtToolkit = initAwtToolkitJob, isHeadless = isHeadless) checkSystemDirJob.join() @@ -312,36 +270,18 @@ fun CoroutineScope.startApplication(args: List, ApplicationImpl(CoroutineScope(mainScope.coroutineContext.job).namedChildScope("Application"), isInternal) } - val starter = loadApp(app = app, - initAwtToolkitAndEventQueueJob = initEventQueueJob, - pluginSetDeferred = pluginSetDeferred, - appInfoDeferred = appInfoDeferred, - euaDocumentDeferred = euaDocumentDeferred, - asyncScope = this@startApplication, - initLafJob = initLafJob, - logDeferred = logDeferred, - appRegisteredJob = appRegisteredJob, - args = args.filterNot { CommandLineArgs.isKnownArgument(it) }) - // out of appLoaded scope - this@startApplication.launch { - val isInitialStart = configImportDeferred.await() - // appLoaded not only provides starter, but also loads app, that's why it is here - IdeStartupWizardCollector.logExperimentState() - if (isInitialStart != null) { - LoadingState.compareAndSetCurrentState(LoadingState.COMPONENTS_LOADED, LoadingState.APP_READY) - val log = logDeferred.await() - runCatching { - span("startup wizard run") { - runStartupWizard(isInitialStart = isInitialStart, app = ApplicationManager.getApplication()) - } - }.getOrLogException(log) - } - executeApplicationStarter(starter = starter, args = args) - } + loadApp(app = app, + initAwtToolkitAndEventQueueJob = initEventQueueJob, + pluginSetDeferred = pluginSetDeferred, + appInfoDeferred = appInfoDeferred, + euaDocumentDeferred = euaDocumentDeferred, + asyncScope = this@startApplication, + initLafJob = initLafJob, + logDeferred = logDeferred, + appRegisteredJob = appRegisteredJob, + args = args.filterNot { CommandLineArgs.isKnownArgument(it) }) } - scheduleEnableCoroutineDumpAndJstack() - launch { // required for appStarter.prepareStart appInfoDeferred.join() @@ -362,52 +302,31 @@ fun CoroutineScope.startApplication(args: List, appStarter.start(InitAppContext(appRegistered = appRegisteredJob, appLoaded = appLoaded)) } } -} - -private suspend fun enableNewUi(logDeferred: Deferred) { - if (System.getProperty("ide.experimental.ui") == null) { - try { - EarlyAccessRegistryManager.setAndFlush(mapOf("ide.experimental.ui" to "true")) - } - catch (e: CancellationException) { - throw e - } - catch (e: Throwable) { - logDeferred.await().error(e) - } - } -} - -@Volatile -@JvmField -internal var isInitialStart: CompletableDeferred? = null - -private fun CoroutineScope.scheduleEnableCoroutineDumpAndJstack() { - if (!System.getProperty("idea.enable.coroutine.dump", "true").toBoolean()) { - return - } + // out of appLoaded scope launch { - span("coroutine debug probes init") { - try { - enableCoroutineDump() - } - catch (ignore: NoClassDefFoundError) { - // if for some reason, the class loader has ByteBuddy in the classpath - // (it is an error, and should be fixed - our dev mode and production behaves correctly) - } - catch (e: Exception) { - e.printStackTrace() + // starter is used later, but we need to wait for appLoaded completion + val starter = appLoaded.await() + + val isInitialStart = configImportDeferred.await() + // appLoaded not only provides starter, but also loads app, that's why it is here + launch { + if (ConfigImportHelper.isFirstSession()) { + IdeStartupWizardCollector.logExperimentState() } } - span("coroutine jstack configuration") { - JBR.getJstack()?.includeInfoFrom { - """ -$COROUTINE_DUMP_HEADER -${dumpCoroutines(stripDump = false)} -""" - } + + if (isInitialStart != null) { + LoadingState.compareAndSetCurrentState(LoadingState.COMPONENTS_LOADED, LoadingState.APP_READY) + val log = logDeferred.await() + runCatching { + span("startup wizard run") { + runStartupWizard(isInitialStart = isInitialStart, app = ApplicationManager.getApplication()) + } + }.getOrLogException(log) } + + executeApplicationStarter(starter = starter, args = args) } } @@ -474,8 +393,9 @@ private fun CoroutineScope.scheduleLoadSystemLibsAndLogInfoAndInitMacApp(logDefe } } -fun processWindowsLauncherCommandLine(currentDirectory: String, args: Array): Int = - EXTERNAL_LISTENER.apply(currentDirectory, args) +fun processWindowsLauncherCommandLine(currentDirectory: String, args: Array): Int { + return EXTERNAL_LISTENER.apply(currentDirectory, args) +} @get:Internal val isImplicitReadOnEDTDisabled: Boolean @@ -504,48 +424,6 @@ private suspend fun runPreAppClass(args: List, classBeforeAppProperty: S } } -private suspend fun importConfig(args: List, - targetDirectoryToImportConfig: Path, - log: Logger, - appStarter: AppStarter, - euaDocumentDeferred: Deferred, - headlessAutoImport: Boolean = false) { - if (headlessAutoImport) { - // headless AppStarters are not notified about config import - val veryFirstStartOnThisComputer = euaDocumentDeferred.await() != null - withContext(RawSwingDispatcher) { - try { - ConfigImportHelper.importConfigsTo(veryFirstStartOnThisComputer, targetDirectoryToImportConfig, args, log, true) - log.info("Automatic config import completed") - } - catch (e: UnsupportedOperationException) { - log.info("Automatic config import is not possible", e) - } - } - EarlyAccessRegistryManager.invalidate() - IconLoader.clearCache() - return - } - - span("screen reader checking") { - runCatching { - enableScreenReaderSupportIfNecessary() - }.getOrLogException(log) - } - - span("config importing") { - appStarter.beforeImportConfigs() - - val veryFirstStartOnThisComputer = euaDocumentDeferred.await() != null - withContext(RawSwingDispatcher) { - ConfigImportHelper.importConfigsTo(veryFirstStartOnThisComputer, targetDirectoryToImportConfig, args, log) - } - appStarter.importFinished(targetDirectoryToImportConfig) - EarlyAccessRegistryManager.invalidate() - IconLoader.clearCache() - } -} - private fun CoroutineScope.configureJavaUtilLogging(): Job { return launch(CoroutineName("console logger configuration")) { val rootLogger = java.util.logging.Logger.getLogger("") diff --git a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt index 5d1bbc8cd100..c15dfb6994d7 100644 --- a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt +++ b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt @@ -29,8 +29,8 @@ fun isCoroutineDumpHeader(line: String): Boolean { return line == COROUTINE_DUMP_HEADER || line == COROUTINE_DUMP_HEADER_STRIPPED } -fun enableCoroutineDump() { - runCatching { +fun enableCoroutineDump(): Result { + return runCatching { DebugProbes.enableCreationStackTraces = false DebugProbes.install() }