diff --git a/.idea/modules.xml b/.idea/modules.xml index a10911b37422..07a9d726a8fe 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -743,6 +743,7 @@ + diff --git a/platform/core-nio-fs/src/com/intellij/platform/core/nio/fs/MultiRoutingFileSystem.java b/platform/core-nio-fs/src/com/intellij/platform/core/nio/fs/MultiRoutingFileSystem.java index 2cad4f4e5eea..35f2c0eaf273 100644 --- a/platform/core-nio-fs/src/com/intellij/platform/core/nio/fs/MultiRoutingFileSystem.java +++ b/platform/core-nio-fs/src/com/intellij/platform/core/nio/fs/MultiRoutingFileSystem.java @@ -147,7 +147,12 @@ public final class MultiRoutingFileSystem extends DelegatingFileSystem myPathSpy = (ignored, ignored2) -> {}; + /** * Adds a new backend filesystem that handles requests to specific roots. *

@@ -100,6 +104,27 @@ public final class MultiRoutingFileSystemProvider } } + public static void setPathSpy( + @NotNull FileSystemProvider provider, + @NotNull BiConsumer<@NotNull String, @NotNull Boolean> pathSpy + ) { + if (provider.getClass().getName().equals(MultiRoutingFileSystemProvider.class.getName())) { + try { + provider.getClass().getMethod("setPathSpy", BiConsumer.class).invoke(provider, pathSpy); + } + catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + else { + throw new IllegalArgumentException(String.format("%s is not an instance of %s", provider, MultiRoutingFileSystemProvider.class)); + } + } + + public void setPathSpy(@NotNull BiConsumer pathSpy) { + myPathSpy = pathSpy; + } + public MultiRoutingFileSystemProvider(FileSystemProvider localFSProvider) { myLocalProvider = localFSProvider; myFileSystem = new MultiRoutingFileSystem(this, myLocalProvider.getFileSystem(URI.create("file:///"))); @@ -176,6 +201,7 @@ public final class MultiRoutingFileSystemProvider FileSystem backend1 = myFileSystem.getBackend(path1String); FileSystemProvider provider1 = backend1.provider(); if (path2 == null) { + myPathSpy.accept(path1String, provider1 != this); return provider1; } @@ -188,14 +214,17 @@ public final class MultiRoutingFileSystemProvider FileSystemProvider provider2 = backend2.provider(); if (provider1.equals(provider2)) { + myPathSpy.accept(path1String, provider1 != this); return provider1; } if (canHandleRouting(provider1, backend2.getPath(path2String))) { + myPathSpy.accept(path1String, provider1 != this); return provider1; } if (canHandleRouting(provider2, backend1.getPath(path1String))) { + myPathSpy.accept(path2String, provider2 != this); return provider2; } diff --git a/platform/eel-impl/BUILD.bazel b/platform/eel-impl/BUILD.bazel index ec0af69bd3d8..d8f7fd91dc71 100644 --- a/platform/eel-impl/BUILD.bazel +++ b/platform/eel-impl/BUILD.bazel @@ -30,4 +30,25 @@ jvm_library( ], runtime_deps = [":eel-impl_resources"] ) -### auto-generated section `build intellij.platform.eel.impl` end \ No newline at end of file +### auto-generated section `build intellij.platform.eel.impl` end + +### auto-generated section `build intellij.platform.eel.impl.benchmarks` start +load("@rules_java//java:defs.bzl", "java_library") +load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_test") + +jvm_library( + name = "eel-impl-benchmarks_test_lib", + visibility = ["//visibility:public"], + srcs = glob(["benchmarks/**/*.kt", "benchmarks/**/*.java"], allow_empty = True), + deps = [ + "@lib//:jmh-core", + "@lib//:jmh-generator-annprocess", + "//platform/eel-impl", + ] +) + +jvm_test( + name = "eel-impl-benchmarks_test", + runtime_deps = [":eel-impl-benchmarks_test_lib"] +) +### auto-generated section `build intellij.platform.eel.impl.benchmarks` end \ No newline at end of file diff --git a/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/BenchmarkLauncher.java b/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/BenchmarkLauncher.java new file mode 100644 index 000000000000..bf43c97d71db --- /dev/null +++ b/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/BenchmarkLauncher.java @@ -0,0 +1,12 @@ +// 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.eel.impl.fs; + +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +public final class BenchmarkLauncher { + public static void main(String[] args) throws RunnerException { + new Runner(new OptionsBuilder().build()).run(); + } +} diff --git a/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/EelEarlyAccessFilterBenchmark.java b/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/EelEarlyAccessFilterBenchmark.java new file mode 100644 index 000000000000..093d8c12daf9 --- /dev/null +++ b/platform/eel-impl/benchmarks/com/intellij/platform/eel/impl/fs/EelEarlyAccessFilterBenchmark.java @@ -0,0 +1,52 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.eel.impl.fs; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.math.BigInteger; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Threads(4) +@Warmup(iterations = 0) // This code affects users since the beginning of the application. Users don't warm up our products. +public class EelEarlyAccessFilterBenchmark { + @State(Scope.Benchmark) + public static class BenchmarkState { + final EelEarlyAccessFilter filter = new EelEarlyAccessFilter(); + final String[] pathFixtures; + + public BenchmarkState() { + pathFixtures = new String[10]; + Random random = new Random(31337); + for (int i = 0; i < pathFixtures.length; i++) { + byte[] bytes = new byte[random.nextInt(1023) + 1]; + pathFixtures[i] = new BigInteger(bytes).toString(64); + } + } + } + + @Benchmark + public void checkOnePathOneTime(BenchmarkState state, Blackhole blackhole) { + blackhole.consume(state.filter.check(state.pathFixtures[0])); + } + + @Benchmark + public void checkOnePathTenTimes(BenchmarkState state, Blackhole blackhole) { + for (int ignored = 0; ignored < 10; ignored++) { + blackhole.consume(state.filter.check(state.pathFixtures[0])); + } + } + + @Benchmark + public void checkTenPathsOneTime(BenchmarkState state, Blackhole blackhole) { + for (int ignored = 0; ignored < 10; ignored++) { + for (int i = 0; i < 10; i++) { + blackhole.consume(state.filter.check(state.pathFixtures[i])); + } + } + } +} diff --git a/platform/eel-impl/intellij.platform.eel.impl.benchmarks.iml b/platform/eel-impl/intellij.platform.eel.impl.benchmarks.iml new file mode 100644 index 000000000000..0c026263faa2 --- /dev/null +++ b/platform/eel-impl/intellij.platform.eel.impl.benchmarks.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/eel-impl/resources/intellij.platform.eel.impl.xml b/platform/eel-impl/resources/intellij.platform.eel.impl.xml index 1dd0ddefa1e6..91d5973ae8c4 100644 --- a/platform/eel-impl/resources/intellij.platform.eel.impl.xml +++ b/platform/eel-impl/resources/intellij.platform.eel.impl.xml @@ -6,5 +6,7 @@ serviceImplementation="com.intellij.platform.eel.impl.local.LocalWindowsEelApiImpl"/> + + \ No newline at end of file diff --git a/platform/eel-impl/src/com/intellij/platform/eel/impl/fs/EelEarlyAccessApplicationActivity.kt b/platform/eel-impl/src/com/intellij/platform/eel/impl/fs/EelEarlyAccessApplicationActivity.kt new file mode 100644 index 000000000000..d5b83dcf6677 --- /dev/null +++ b/platform/eel-impl/src/com/intellij/platform/eel/impl/fs/EelEarlyAccessApplicationActivity.kt @@ -0,0 +1,60 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.eel.impl.fs + +import com.intellij.ide.ApplicationActivity +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.logger +import com.intellij.platform.core.nio.fs.MultiRoutingFileSystem +import com.intellij.platform.core.nio.fs.MultiRoutingFileSystemProvider +import org.jetbrains.annotations.VisibleForTesting +import java.lang.invoke.MethodHandles +import java.nio.file.FileSystems +import java.util.function.BiConsumer + +internal class EelEarlyAccessApplicationActivity : ApplicationActivity { + override suspend fun execute() { + val fs = FileSystems.getDefault() + if (fs.javaClass.name != MultiRoutingFileSystem::class.java.name) return + + val logger = logger() + val filter = EelEarlyAccessFilter() + + MultiRoutingFileSystemProvider.setPathSpy(fs.provider(), BiConsumer { strPath, isDefaultProvider -> + if (isDefaultProvider && strPath.startsWith("\\\\wsl")) { + if (filter.check(strPath)) { + // Q: What happened? + // A: Some code tried to access \\wsl.localhost before the initialization of the IJent file system. + // Therefore, the code will access the original filesystem of WSL brought by Microsoft, + // and the code won't access our polished file system with various workarounds, optimizations, etc. + // Also, the code that triggered this error is an obstacle for implementing DevContainers over Eel. + // + // Q: How to fix it? + // A: Call `com.intellij.platform.eel.provider.EelInitialization.runEelInitialization` in advance. + // Sometimes it's easy, sometimes you have to rework the UI/UX, sorry for that. + // + // Q: Why not initialize the IJent filesystem lazily, right here, at this moment? + // A: Although this initialization usually takes a second, sometimes it can be significantly slower. + // It depends not only on the I/O speed of the machine, but also on personal configurations of the user. + // Being called from EDT, this lazy initialization would lead to yet another freeze. + logger.error( + "Remote file system accessed before Eel initialization. The description is in the source code.", + Attachment("path", strPath.toString()), + ) + } + } + }) + } +} + +@VisibleForTesting +class EelEarlyAccessFilter { + private val poorMansBloomFilter = Array(1 shl 16) { true } + private val varHandle = MethodHandles.arrayElementVarHandle(poorMansBloomFilter::class.java) + + fun check(strPath: String): Boolean { + val hash = strPath.hashCode().let { + (it and 0xFFFF) xor (it ushr 16 and 0xFFFF) + } + return varHandle.getAndSet(poorMansBloomFilter, hash, false) as Boolean + } +} \ No newline at end of file diff --git a/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt b/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt index 13b6b7eb3163..ee47d5ed5749 100644 --- a/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt +++ b/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ApplicationLoader.kt @@ -281,6 +281,7 @@ private val asyncAppListenerAllowListForNonCorePlugin = java.util.Set.of( "com.intellij.internal.statistic.updater.StatisticsJobsScheduler", "com.intellij.internal.statistic.updater.StatisticsStateCollectorsScheduler", "org.jetbrains.kotlin.idea.base.plugin.K2UnsupportedPluginsNotificationActivity", + "com.intellij.platform.eel.impl.fs.EelEarlyAccessApplicationActivity", ) private fun CoroutineScope.executeAsyncAppInitListeners() {