kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

A way to statically load DebugProbesKt in production

Open dovchinnikov opened this issue 2 years ago • 3 comments

-javaagent is not a production-level solution for IJ, so IJ enables coroutine debugger dynamically at very early stage of application start up:

DebugProbes.enableCreationStackTraces = false
DebugProbes.install()

What do we have now?

  • kotlinx-coroutines-debug bundles repackaged byte-buddy (15,3 MB out of total 20,3 MB).
  • Considerable time is spent on loading the byte-buddy (anywhere from 300ms to 500ms in our runs).
  • It's used only to patch a single class kotlin.coroutines.jvm.internal.DebugProbesKt which is located in kotlin-stdlib.
  • It does not work on winarm64 at the moment (minor).

What should be instead?

A way to statically load the proper binary without dealing with byte-buddy.

Why?

  • Reduce the size of production jars.
  • Reduce the class loading time.

Possible solution

Replace stdlib's kotlin.coroutines.jvm.internal.DebugProbesKt with the following:

private val probesBridge get() = Class.forName("ProbesBridge Name TBD")
private val probeCoroutineCreatedMethod: Method? = runCatching {
  probesBridge.getDeclaredMethod("probeCoroutineCreated", Continuation::class.java)
}.getOrNull()
private val probeCoroutineResumedMethod: Method? = runCatching {
  probesBridge.getDeclaredMethod("probeCoroutineResumed", Continuation::class.java)
}.getOrNull()
private val probeCoroutineSuspended: Method? = runCatching {
  probesBridge.getDeclaredMethod("probeCoroutineSuspended", Continuation::class.java)
}.getOrNull()

internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
  if (probeCoroutineCreatedMethod != null) {
    return probeCoroutineCreatedMethod.invoke(null, completion) as Continuation<T>
  }
  return completion
}

internal fun probeCoroutineResumed(frame: Continuation<*>) {
  probeCoroutineResumedMethod?.invoke(null, frame)
}

internal fun probeCoroutineSuspended(frame: Continuation<*>) {
  probeCoroutineSuspended?.invoke(null, frame)
}

This approach is already used here. JIT should not have any trouble inlining one of == null branches when ProbesBridge is present or absent.

  1. ProbesBridge should set AgentInstallationType in its <clinit>.
  2. ProbesBridge should start the cleaner thread in its <clinit> (questionable, but enables no-code setup by putting the debug jar into classpath).
  3. (Optionally) Provide a separate minimal production-ready kotlinx-coroutines-debug-core JAR with probes. kotlinx.coroutines.debug.internal.DebugProbesImpl resides in kotlinx-coroutines-core-jvm, I think that it should be a part of that JAR instead.

Other solutions

  1. Patch kotlin-stdlib jar statically, replace kotlin.coroutines.jvm.internal.DebugProbesKt during build time, publish a separate kotlin-stdlib artefact.
  2. Patch kotlin-stdlib jar dynamically. 2.1. Put the jar before kotlin-stdlib in classpath, similar to #3356. 2.2. Change IJ own class loader to load DebugProbesKt.bin or the proposed class when kotlin.coroutines.jvm.internal.DebugProbesKt is requested (somewhat equivalent to javaagent solution, also a hack).
  3. DI probes into stdlib. 3.1. proposed solution, or 3.2. separate interface,ServiceLoader similar to Dispatchers.Main, no op implementation by default.

dovchinnikov avatar Feb 21 '23 14:02 dovchinnikov

-javaagent is not a production-level solution for IJ

From our internal notes:

  • Passing -javaagent everywhere in each launch script is quite a lot of technical burden
  • It potentially (?) affects classloading times as it processes all classes in IJ

qwwdfsad avatar Feb 21 '23 14:02 qwwdfsad

https://youtrack.jetbrains.com/issue/KT-62096/Consider-replacing-JVM-agent-specific-kotlin.coroutines.jvm.internal.DebugProbesKt-with-SPI-based-mechanism

qwwdfsad avatar Sep 20 '23 18:09 qwwdfsad