diff --git a/docs/notebooks/1-ContextPropagation.ipynb b/docs/notebooks/1-ContextPropagation.ipynb new file mode 100644 index 000000000000..bd82c380d49f --- /dev/null +++ b/docs/notebooks/1-ContextPropagation.ipynb @@ -0,0 +1,1168 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Context Propagation\n", + "\n" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:12:13.884615Z", + "start_time": "2025-08-05T11:12:13.834415Z" + } + }, + "cell_type": "code", + "source": [ + "%use intellij-platform\n", + "import com.intellij.util.application\n", + "import com.intellij.util.ui.EDT\n", + "import javax.swing.JOptionPane\n", + "import com.intellij.openapi.application.ModalityState\n", + "import com.intellij.openapi.progress.ProgressManager\n", + "import com.intellij.platform.ide.progress.ModalTaskOwner\n", + "import com.intellij.platform.ide.progress.runWithModalProgressBlocking\n", + "import com.intellij.openapi.progress.EmptyProgressIndicator\n", + "import com.intellij.openapi.progress.ProcessCanceledException\n", + "import java.util.concurrent.Callable\n", + "import kotlinx.coroutines.Dispatchers\n", + "import kotlinx.coroutines.launch\n", + "import kotlinx.coroutines.runBlocking\n", + "import kotlinx.coroutines.withContext\n", + "import kotlinx.coroutines.yield\n", + "import kotlin.coroutines.AbstractCoroutineContextElement\n", + "import kotlin.coroutines.CoroutineContext\n", + "import com.intellij.concurrency.currentThreadContext\n", + "import kotlin.coroutines.EmptyCoroutineContext\n", + "import com.intellij.concurrency.IntelliJContextElement\n", + "import com.intellij.openapi.progress.blockingContextScope\n", + "import com.intellij.openapi.progress.currentThreadCoroutineScope\n", + "import com.intellij.openapi.progress.withCurrentThreadCoroutineScopeBlocking\n", + "import kotlinx.coroutines.future.asCompletableFuture\n", + "import kotlinx.coroutines.CoroutineStart\n", + "import com.intellij.concurrency.installThreadContext\n", + "import kotlinx.coroutines.coroutineScope\n", + "\n", + "\n", + "\n", + "\n" + ], + "outputs": [], + "execution_count": 24 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "In this notebook, we will present one of the core concepts in the IntelliJ Platform -- Context Propagation." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Motivation\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "As programming languages and frameworks get more high-level, they tend to operate with increasingly more abstract entities. It also concerns computations and control flow of programs.\n", + "Java and Kotlin abstract machine code and OS-level threads into convenient syntax and objects associated with threads.\n", + "\n", + "One of the convenient abstractions that programming languages provide is lexical scoping:\n" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:27.377221Z", + "start_time": "2025-08-05T11:10:27.328328Z" + } + }, + "cell_type": "code", + "source": [ + "fun sample(param: Int) {\n", + " val x = param + 1\n", + " if (2 + 2 == 4) {\n", + " val y = x + 2\n", + " }\n", + " // `y` is not accessible here\n", + " // but `param` is accessible\n", + "}" + ], + "outputs": [], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "IntelliJ Platform is a multithreaded application, and it deals with _computations_ rather than with single-threaded functions.\n", + "The usual control flow (`if` / `while` / `for`) is extended with threads and asynchronous computations." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:29.118717Z", + "start_time": "2025-08-05T11:10:29.011861Z" + } + }, + "cell_type": "code", + "source": [ + "application.executeOnPooledThread {\n", + " application.assertIsNonDispatchThread() // we are on a background thread\n", + " val intensiveComputation = 1 + 1\n", + " application.invokeLater {\n", + " application.assertIsDispatchThread() // we are on EDT\n", + " JOptionPane.showMessageDialog(null, \"Result of intensive computation: $intensiveComputation\", \"Sample title\", JOptionPane.INFORMATION_MESSAGE)\n", + " }\n", + "};" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "java.util.concurrent.FutureTask@42875519[Completed normally]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Sometimes in the Platform, we can notice a pattern: some data becomes associated with a computation, just like a variable is associated with its lexical scope." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Example: Modality State\n", + "\n", + "Modality state is an object that indicates that a computation belongs to some modal window. The computation usually runs on background, and sometimes it may want to access the UI thread. In this case, we must pass modality state to the UI computation so that the platform will not postpone this execution." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:37.436609Z", + "start_time": "2025-08-05T11:10:34.913013Z" + } + }, + "cell_type": "code", + "source": [ + "val outerModalityState = ModalityState.defaultModalityState()\n", + "// the default choice for modality is `nonModal`: every computation assumes that it is launched in absence of modal dialogs by default\n", + "assert(outerModalityState == ModalityState.nonModal())\n", + "\n", + "// launching java-style modal progress\n", + "ProgressManager.getInstance().runProcessWithProgressSynchronously(\n", + " {\n", + " // modality is different here, as we entered a modal computation\n", + " val innerModalityState = ModalityState.defaultModalityState()\n", + " assert(innerModalityState != ModalityState.nonModal())\n", + "\n", + " Thread.sleep(1000) // emulation of long background work\n", + " application.invokeAndWait({\n", + " val modalityStateInDispatchThread = ModalityState.defaultModalityState()\n", + " assert(innerModalityState == modalityStateInDispatchThread)\n", + " }, innerModalityState)\n", + " Thread.sleep(1000) // another long background work\n", + " }, \"Sample modal dialog\", true, null)" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "true" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Exercise:** what would happen if we passed `ModalityState.nonModal()` to `invokeAndWait` and why?\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Example: (Legacy) Cancellation\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Most of the computations in IntelliJ Platform need to be cancellable -- otherwise, the application becomes unresponsive. The **legacy** cancellation model in IntelliJ Platform is built around `ProgressIndicator`, which also can belong to several computations" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:40.736245Z", + "start_time": "2025-08-05T11:10:39.506975Z" + } + }, + "cell_type": "code", + "source": [ + "val indicator = EmptyProgressIndicator()\n", + "\n", + "application.executeOnPooledThread(\n", + " {\n", + " Thread.sleep(1000)\n", + " indicator.cancel()\n", + " })\n", + "\n", + "application.executeOnPooledThread(Callable {\n", + " return@Callable application.executeOnPooledThread(Callable {\n", + " Thread.sleep(500)\n", + " ProgressManager.getInstance().runProcess(\n", + " {\n", + " try {\n", + " while (true) {\n", + " ProgressManager.checkCanceled()\n", + " }\n", + " }\n", + " catch (e: ProcessCanceledException) {\n", + " }\n", + " }, indicator)\n", + " return@Callable \"canceled successfully\"\n", + " }).get()\n", + "}).get()" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "canceled successfully" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "In both these examples we see an instance of a parameter that is attached to a computation, and this parameter should transcend the boundaries of a single thread. The idea behind Context Propagation is to provide a unified interface for such parameters." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Kotlin Coroutines" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "In Kotlin Coroutines, a similar problem was resolved with the help of `CoroutineContext` -- a heterogeneous list that is attached to computations and which is inherited after coroutine transitions." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:43.572473Z", + "start_time": "2025-08-05T11:10:43.427110Z" + } + }, + "cell_type": "code", + "source": [ + "class MyCoroutineContextElement(val id: Int) : AbstractCoroutineContextElement(MyCoroutineContextElement) {\n", + " companion object Key : CoroutineContext.Key\n", + "\n", + " override fun toString(): String {\n", + " return \"MyCoroutineContextElement($id)\"\n", + " }\n", + "}\n", + "\n", + "runBlocking {\n", + " DISPLAY(\"Element in runBlocking: ${coroutineContext[MyCoroutineContextElement]}\")\n", + " withContext(MyCoroutineContextElement(1)) {\n", + " DISPLAY(\"Element in withContext: ${coroutineContext[MyCoroutineContextElement]}\")\n", + " launch(Dispatchers.Default) {\n", + " DISPLAY(\"Element in launch: ${coroutineContext[MyCoroutineContextElement]}\")\n", + " yield()\n", + " DISPLAY(\"Element after yield: ${coroutineContext[MyCoroutineContextElement]}\")\n", + " withContext(MyCoroutineContextElement(2)) {\n", + " DISPLAY(\"Element in another withContext: ${coroutineContext[MyCoroutineContextElement]}\")\n", + " }\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Element in runBlocking: null" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element in withContext: MyCoroutineContextElement(1)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element in launch: MyCoroutineContextElement(1)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element after yield: MyCoroutineContextElement(1)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element in another withContext: MyCoroutineContextElement(2)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "StandaloneCoroutine{Completed}@736978e9" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Another instances of this pattern are `Reader` monad in Haskell and dynamic binding in Lisp-like languages." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Platform API" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Context Propagation of IntelliJ Platform is a heterogeneous list of implicit parameters attached to a computation, alongside the rules of transferring of this list. Let's look at some examples.\n", + "\n", + "Context Propagation is designed to be closely compatible with Kotlin coroutines. The list of parameters is implemented as `CoroutineContext`, and it can be retrieved with the function `currentThreadContext`" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:10:50.132593Z", + "start_time": "2025-08-05T11:10:50.097070Z" + } + }, + "cell_type": "code", + "source": "DISPLAY(currentThreadContext())", + "outputs": [ + { + "data": { + "text/plain": [ + "BlockingCoroutine{Active}@2750db32" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The intended way to interact with context propagation is from suspending code. The coroutine context can be accessed with `currentThreadContext()` even in non-suspend functions.\n" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:09.030535Z", + "start_time": "2025-08-05T11:11:08.919986Z" + } + }, + "cell_type": "code", + "source": [ + "fun nonSuspendingFunction(action: () -> Unit) {\n", + " action()\n", + "}\n", + "\n", + "runBlocking {\n", + " withContext(MyCoroutineContextElement(42)) {\n", + " val elementFromCoroutines = coroutineContext[MyCoroutineContextElement]\n", + " DISPLAY(\"Element from coroutines: ${elementFromCoroutines}\")\n", + " nonSuspendingFunction {\n", + " val elementFromThreadContext = currentThreadContext()[MyCoroutineContextElement] // no suspension here!\n", + " DISPLAY(\"Element from thread context: $elementFromThreadContext\")\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Element from coroutines: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element from thread context: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 14 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Internally, context propagation is implemented as a thread-local variable containing `CoroutineContext`. Note, that coroutines machinery takes precedence over this thread local:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:11.853862Z", + "start_time": "2025-08-05T11:11:11.698216Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " withContext(MyCoroutineContextElement(42)) {\n", + " DISPLAY(\"Element after withContext: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " launch(start = CoroutineStart.UNDISPATCHED, context = MyCoroutineContextElement(43)) { // note the undispatched start!\n", + " DISPLAY(\"Element after undispatched launch: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Element after withContext: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element after undispatched launch: MyCoroutineContextElement(43)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "StandaloneCoroutine{Completed}@131f2473" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 15 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "If needed, thread context can be overridden from blocking code with `installThreadContext`. Note, that `installThreadContext` will replace the thread-local value by default, instead of appending to it (like it is done in `withContext`). The reason is that `installThreadContext` is mostly used by the Platform, and it often needs complete replacement of the context." + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "installThreadContext(MyCoroutineContextElement(42)) {\n", + " DISPLAY(\"Explicitly installed thread context: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + "}" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Cross-thread propagation\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "By default, coroutine context is _not_ transferred to another thread. This is because old concurrency model of IntelliJ Platform was following Java, where asynchronous computations were not really tracked by anyone. We expect that average coroutine context elements are not prepared to be propagated to \"fire-and-forget\" computations:" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "runBlocking {\n", + " withContext(MyCoroutineContextElement(42)) {\n", + " DISPLAY(\"Element context inside withContext: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " application.invokeLater {\n", + " DISPLAY(\"Element context inside invokeLater: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " }\n", + " application.invokeLater {\n", + " DISPLAY(\"Element context inside executeOnPooledThread: ${currentThreadContext()[MyTransferrableElement]}\")\n", + " }\n", + " }\n", + "}\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "IntelliJ Platform provides a way for some elements to permit their transfer via computations such as `invokeLater`. The coroutine context element needs to extend `IntelliJContextElement` interface." + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "class MyTransferrableElement(val id: Int) : IntelliJContextElement, AbstractCoroutineContextElement(MyTransferrableElement) {\n", + " companion object Key : CoroutineContext.Key\n", + "\n", + " // The element permits its transfer here\n", + " override fun produceChildElement(parentContext: CoroutineContext, isStructured: Boolean): IntelliJContextElement? {\n", + " return this\n", + " }\n", + "\n", + " override fun toString(): String {\n", + " return \"MyTransferrableElement($id)\"\n", + " }\n", + "}\n", + "\n", + "installThreadContext(MyTransferrableElement(42)) {\n", + " DISPLAY(\"Element context inside installThreadContext: ${currentThreadContext()[MyTransferrableElement]}\")\n", + " application.invokeLater {\n", + " DISPLAY(\"Element context inside invokeLater: ${currentThreadContext()[MyTransferrableElement]}\")\n", + " }\n", + " application.executeOnPooledThread {\n", + " DISPLAY(\"Element context inside executeOnPooledThread: ${currentThreadContext()[MyTransferrableElement]}\")\n", + " }\n", + "}" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Exercise:** try to create a context element that gets propagated only if `MyTransferrableElement` is present at the moment of asynchronous scheduling (i.e., the invocation of `invokeLater`)" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "In particular, it means that asynchronous computations (such that a lambda in `invokeLater`) carry their own scope with them.\n", + "Sometimes there is a need to execute computations synchronously -- the most popular example is synchronous dispatch of events by the EDT event queue. In this case, the Platform _resets_ the context before event dispatch -- otherwise, there would be an error.\n", + "The resetting of the context can be achieved with `resetThreadContext` function." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:35.666435Z", + "start_time": "2025-08-05T11:11:35.555998Z" + } + }, + "cell_type": "code", + "source": [ + "installThreadContext(MyCoroutineContextElement(42)) {\n", + " DISPLAY(\"Element context inside installThreadContext: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " resetThreadContext {\n", + " DISPLAY(\"Element context inside resetThreadContext: ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Element context inside installThreadContext: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element context inside resetThreadContext: null" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 18 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Use case: Cancellation Propagation" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "One important application of context propagation is cancellation propagation. This is a subset of Context Propagation, which is used for maintaining the lifetime descriptor of computations and to get a limited version of structured concurrency.\n", + "It is explored better in the notebook about Cancellation Model of IntelliJ Platform." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Use case: `blockingContextScope`" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Some plugins in IntelliJ Platform were written against the legacy concurrency model. Modern implementation of the Platform is interested in controlling the _scope_ of computations running in old plugins. For example, we can have a chain of asynchronous computations:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:38.399039Z", + "start_time": "2025-08-05T11:11:38.309543Z" + } + }, + "cell_type": "code", + "source": [ + "application.invokeLater {\n", + " application.executeOnPooledThread {\n", + " application.invokeLater {\n", + " application.executeOnPooledThread {\n", + " application.invokeAndWait {\n", + " DISPLAY(\"I am deep in asynchronous stack!\")\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "I am deep in asynchronous stack!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 19 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "To track these computations, the platform introduced a special function which is called `blockingContextScope`. This function is a combination of `coroutineScope` and `blockingContext`, and it allows to track the legacy computations and await their termination:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:42.210548Z", + "start_time": "2025-08-05T11:11:41.142065Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " DISPLAY(\"Launching `blockingContextScope`\")\n", + " blockingContextScope {\n", + " application.executeOnPooledThread {\n", + " DISPLAY(\"First `executeOnPooledThread` is running\")\n", + " Thread.sleep(1000)\n", + " DISPLAY(\"First `executeOnPooledThread` is finished\")\n", + " }\n", + " DISPLAY(\"First `executeOnPooledThread` is scheduled\")\n", + " }\n", + " DISPLAY(\"`blockingContextScope` is finished`\")\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Launching `blockingContextScope`" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "First `executeOnPooledThread` is scheduled" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "First `executeOnPooledThread` is running" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "First `executeOnPooledThread` is finished" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "`blockingContextScope` is finished`" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 20 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "`blockingContextScope` internally works via transferable elements." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Use case: `Observation.awaitConfiguration`\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Some applications that are using IntelliJ Platform are interested in using a headless instance of an IDE to import and index a project, so that they later can query something about the codebase.\n", + "IntelliJ Platform provides a headless runner that is called _warmup_, which opens a project and waits for all non-trivial processes are finished.\n", + "\n", + "Internally, warmup enumerates a set of important entry points (i.e., computations that run on project open) and then waits for their completion. Some of these computations can execute something asynchronusly -- in this case, warmup also tracks these asynchrnous computations as well. This way warmup is aware about the tree of computations, and it considers the process of project configuration done when all computations in this tree terminate. This turns out to be a surprisingly stable way to await the configuration process of the IDE -- and it requires minimal external support. Warmup was one of the original motivations to implement Context Propagation.\n", + "\n", + "The entry points are usually marked with `com.intellij.platform.backend.observation.TrackingUtil.trackActivity`, and it allows tracking the whole configuration process." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Advanced Topics" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Structured Propagation\n", + "\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Sometimes we know that the child computation will not outlive the parent one. The simplest example is `Application.invokeAndWait`. If this is the case, then context propagation is _structured_." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:47.516494Z", + "start_time": "2025-08-05T11:11:46.366741Z" + } + }, + "cell_type": "code", + "source": [ + "DISPLAY(\"Before invokeAndWait\")\n", + "application.invokeAndWait {\n", + " Thread.sleep(500)\n", + " DISPLAY(\"Inside invokeAndWait\")\n", + " Thread.sleep(500)\n", + "}\n", + "DISPLAY(\"After invokeAndWait\")" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Before invokeAndWait" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Inside invokeAndWait" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "After invokeAndWait" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 21 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This case is similar to synchronous execution, and here _all_ elements are propagated. Additionally, the inheritors of `IntelliJContextElement` can react to structured propagation via `isStructured` flag." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:11:50.303015Z", + "start_time": "2025-08-05T11:11:50.182692Z" + } + }, + "cell_type": "code", + "source": [ + "class MyTransferrableStructuredElement(val id: Int) : IntelliJContextElement, AbstractCoroutineContextElement(MyTransferrableStructuredElement) {\n", + " companion object Key : CoroutineContext.Key\n", + "\n", + " // The element permits its transfer here\n", + " override fun produceChildElement(parentContext: CoroutineContext, isStructured: Boolean): IntelliJContextElement? {\n", + " if (isStructured) {\n", + " return this\n", + " }\n", + " else {\n", + " return null\n", + " }\n", + " }\n", + "\n", + " override fun toString(): String {\n", + " return \"MyTransferrableStructuredElement($id)\"\n", + " }\n", + "}\n", + "\n", + "installThreadContext(MyTransferrableStructuredElement(42) + MyCoroutineContextElement(43)) {\n", + " DISPLAY(\"Element context inside installThreadContext: Transferrable -- ${currentThreadContext()[MyTransferrableStructuredElement]}, Regular -- ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " application.invokeLater {\n", + " DISPLAY(\"Element context inside invokeLater: Transferrable -- ${currentThreadContext()[MyTransferrableStructuredElement]}, Regular -- ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " }\n", + " application.invokeAndWait {\n", + " DISPLAY(\"Element context inside invokeAndWait: Transferrable -- ${currentThreadContext()[MyTransferrableStructuredElement]}, Regular -- ${currentThreadContext()[MyCoroutineContextElement]}\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Element context inside installThreadContext: Transferrable -- MyTransferrableStructuredElement(42), Regular -- MyCoroutineContextElement(43)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element context inside invokeLater: Transferrable -- null, Regular -- null" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Element context inside invokeAndWait: Transferrable -- MyTransferrableStructuredElement(42), Regular -- MyCoroutineContextElement(43)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 22 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Context Checkpoints\n" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "In Kotlin Coroutines, there is a way to launch asynchronous computations that are bound to some `CoroutineScope`:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:12:18.797541Z", + "start_time": "2025-08-05T11:12:18.737360Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " coroutineScope {\n", + " launch { DISPLAY(\"I am in a coroutine scope\") }\n", + " launch { DISPLAY(\"I too am in a coroutine scope\") }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "I am in a coroutine scope" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "I too am in a coroutine scope" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "StandaloneCoroutine{Completed}@62d462aa" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 25 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "It is difficult to use coroutine scope in non-suspending code, as it is unclear where the computations are bounded. However, the platform offers one solution here.\n", + "It is possible to create a coroutine scope from the currently installed thread context with `withCurrentThreadCoroutineScopeBlocking`, and then access it with `currentThreadCoroutineContext`:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:12:21.583769Z", + "start_time": "2025-08-05T11:12:21.449306Z" + } + }, + "cell_type": "code", + "source": [ + "installThreadContext(MyCoroutineContextElement(42)) {\n", + " val (result, trackingJob) = withCurrentThreadCoroutineScopeBlocking {\n", + " DISPLAY(\"Current context element: ${currentThreadContext()[MyCoroutineContextElement]}\") // prints 42\n", + " installThreadContext(currentThreadContext() + MyCoroutineContextElement(43), true) {\n", + " DISPLAY(\"Context element after replacement: ${currentThreadContext()[MyCoroutineContextElement]}\") // prints 43\n", + " currentThreadCoroutineScope().launch {\n", + " DISPLAY(\"Context inside `launch`: ${currentThreadContext()[MyCoroutineContextElement]}\") // prints 42!\n", + " }\n", + " }\n", + " }\n", + " trackingJob.asCompletableFuture().join()\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Current context element: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Context element after replacement: MyCoroutineContextElement(43)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Context inside `launch`: MyCoroutineContextElement(42)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 26 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This is useful when you want to pass the ability to launch coroutines on some scope which is under your control" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "2.2.20-dev-4982", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "sessionRunMode": "IDE_PROCESS" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}