├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── .run └── Run IDE with Plugin.run.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── google │ │ └── coroutinestacks │ │ ├── CoroutineStacksBundle.kt │ │ ├── CoroutineStacksToolWindowFactory.kt │ │ ├── CoroutineTraceForestBuilder.kt │ │ └── ui │ │ ├── CoroutineFramesList.kt │ │ ├── CoroutineStacksPanel.kt │ │ ├── DraggableContainerWithEdges.kt │ │ ├── ForestLayout.kt │ │ ├── ZoomableJBScrollPane.kt │ │ └── buttons.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── messages │ └── CoroutineStacksBundle.properties ├── test └── kotlin │ └── com │ └── google │ └── coroutinestacks │ └── test │ ├── CoroutineStacksFromDumpTest.kt │ └── utils │ ├── coroutineDumpParser.kt │ └── mocks.kt └── testData ├── README.md ├── dumps ├── noChildren.txt ├── twoChildren.txt └── twoChildrenChat.txt ├── outs ├── noChildren.txt ├── twoChildren.txt └── twoChildrenChat.txt ├── parse_dumps.py └── requirements.txt /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Build 9 | 10 | on: 11 | push: 12 | branches: [ "main" ] 13 | pull_request: 14 | branches: [ "main" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 11 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '11' 30 | distribution: 'temurin' 31 | - name: Build with Gradle 32 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 33 | with: 34 | arguments: build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CoroutineStacks -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 33 | 35 | true 36 | true 37 | false 38 | 39 | 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the 73 | Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our community guidelines 22 | 23 | This project follows 24 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coroutine Stacks 2 | ![example workflow](https://github.com/nikita-nazarov/coroutine-stacks/actions/workflows/gradle.yml/badge.svg) 3 | 4 | This project was developed during Google Summer of Code 2023 and is dedicated to creating an Intellij Plugin that will enhance the coroutine debugging experience by creating a view with the graph representation of coroutines and their stack traces, similar to how it is done in the [Parallel Stacks](https://www.jetbrains.com/help/rider/Debugging_Multithreaded_Applications.html#parallel-stacks) feature of the JetBrains Rider IDE. 5 | 6 | ![coroutine-stacks-demo](https://github.com/google/coroutine-stacks/assets/25721619/7b8caf0c-ad82-476c-91b8-3cac105155cf) 7 | 8 | 9 | ## How to install the plugin 10 | The plugin is published in [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/23117-coroutine-stacks/), so you can install it from the `Plugins` menu in the IDE. 11 | 12 | ## How to use the plugin 13 | Once you start the debugger click on the `Coroutine Stacks` label in the bottom right corner of the IDE. If you use the new UI you should click on the icon with four circles froming a square. After that you will see a panel with coroutine stack traces. On the top of it you will find a couple of useful buttons: 14 | 1. Add library frames filter 15 | 2. Capture a coroutine dump 16 | 3. Add coroutine creation stack traces to the panel 17 | 4. Select the dispatcher 18 | 5. Zoom the panel in and out 19 | 20 | Check out the [Quick Start Guide](https://plugins.jetbrains.com/plugin/23117-coroutine-stacks/documentation/quick-start-guide) for the detailed description of the plugin features. 21 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask 2 | 3 | plugins { 4 | id("java") 5 | id("org.jetbrains.kotlin.jvm") version "1.8.21" 6 | id("org.jetbrains.intellij.platform") version "2.1.0" 7 | } 8 | 9 | group = "com.google" 10 | version = "1.0.3" 11 | 12 | repositories { 13 | mavenCentral() 14 | 15 | intellijPlatform { 16 | defaultRepositories() 17 | } 18 | } 19 | 20 | tasks { 21 | // Set the JVM compatibility versions 22 | withType { 23 | sourceCompatibility = "17" 24 | targetCompatibility = "17" 25 | } 26 | withType { 27 | kotlinOptions.jvmTarget = "17" 28 | } 29 | 30 | patchPluginXml { 31 | sinceBuild.set("222.3346") 32 | untilBuild.set("253.*") 33 | } 34 | 35 | signPlugin { 36 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 37 | privateKey.set(System.getenv("PRIVATE_KEY")) 38 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 39 | } 40 | 41 | publishPlugin { 42 | token = System.getenv("PUBLISH_TOKEN") 43 | } 44 | } 45 | 46 | tasks.named("test") { 47 | useJUnitPlatform() 48 | jvmArgumentProviders += CommandLineArgumentProvider { 49 | listOf("-Didea.kotlin.plugin.use.k2=true") 50 | } 51 | } 52 | 53 | tasks.named("runIde") { 54 | jvmArgumentProviders += CommandLineArgumentProvider { 55 | listOf("-Didea.kotlin.plugin.use.k2=true") 56 | } 57 | } 58 | 59 | dependencies { 60 | intellijPlatform { 61 | intellijIdeaCommunity("2024.2.3") 62 | bundledPlugin("org.jetbrains.kotlin") 63 | bundledPlugin("com.intellij.java") 64 | 65 | pluginVerifier() 66 | zipSigner() 67 | instrumentationTools() 68 | } 69 | implementation(kotlin("stdlib-jdk8")) 70 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 71 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.0.3") 72 | } 73 | 74 | kotlin { 75 | jvmToolchain(17) 76 | } 77 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.stdlib.default.dependency=false 2 | # TODO temporary workaround for Kotlin 1.8.20+ (https://jb.gg/intellij-platform-kotlin-oom) 3 | kotlin.incremental.useClasspathSnapshot=false 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/coroutine-stacks/980b6fbce917eef64d03ee45a8c2f7581e1ebd03/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "CoroutineStacks" -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/CoroutineStacksBundle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks 18 | 19 | import com.intellij.DynamicBundle 20 | import org.jetbrains.annotations.Nls 21 | import org.jetbrains.annotations.NonNls 22 | import org.jetbrains.annotations.PropertyKey 23 | 24 | @NonNls 25 | private const val BUNDLE = "messages.CoroutineStacksBundle" 26 | 27 | object CoroutineStacksBundle : DynamicBundle(BUNDLE) { 28 | @Nls 29 | @JvmStatic 30 | fun message(@NonNls @PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String = getMessage(key, *params) 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/CoroutineStacksToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks 18 | 19 | import com.intellij.openapi.project.Project 20 | import com.intellij.openapi.wm.ToolWindow 21 | import com.intellij.openapi.wm.ToolWindowFactory 22 | import com.google.coroutinestacks.ui.CoroutineStacksPanel 23 | 24 | class CoroutineStacksToolWindowFactory : ToolWindowFactory { 25 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 26 | toolWindow.component.add(CoroutineStacksPanel(project)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/CoroutineTraceForestBuilder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks 18 | 19 | import com.google.coroutinestacks.ui.* 20 | import com.intellij.debugger.engine.JVMStackFrameInfoProvider 21 | import com.intellij.debugger.engine.SuspendContextImpl 22 | import com.intellij.ui.components.JBList 23 | import com.sun.jdi.Location 24 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData 25 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem 26 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.State 27 | import org.jetbrains.kotlin.idea.debugger.coroutine.util.CoroutineFrameBuilder 28 | import java.awt.Component 29 | import java.awt.Dimension 30 | import java.util.* 31 | 32 | data class Node( 33 | val stackFrameItem: CoroutineStackFrameItem? = null, 34 | var num: Int = 0, // Represents how many coroutines have this frame in their stack trace 35 | var runningCount: Int = 0, 36 | var suspendedCount: Int = 0, 37 | val children: MutableMap = mutableMapOf(), 38 | var coroutinesActive: String = "" 39 | ) 40 | 41 | data class CoroutineTrace( 42 | val stackFrameItems: MutableList, 43 | val runningCount: Int, 44 | val suspendedCount: Int, 45 | val coroutinesActiveLabel: String 46 | ) 47 | 48 | fun SuspendContextImpl.buildCoroutineStackForest( 49 | rootValue: Node, 50 | coroutineDataList: List, 51 | areLibraryFramesAllowed: Boolean, 52 | addCreationFrames: Boolean, 53 | zoomLevel: Float 54 | ): ZoomableJBScrollPane? { 55 | buildStackFrameGraph(rootValue, coroutineDataList, areLibraryFramesAllowed, addCreationFrames) 56 | val coroutineTraces = createCoroutineTraces(rootValue) 57 | return createCoroutineTraceForest(coroutineTraces, zoomLevel) 58 | } 59 | 60 | private fun SuspendContextImpl.createCoroutineTraceForest( 61 | traces: List, 62 | zoomLevel: Float 63 | ): ZoomableJBScrollPane? { 64 | if (traces.isEmpty()) { 65 | return null 66 | } 67 | val vertexData = mutableListOf?>() 68 | val componentData = mutableListOf() 69 | var previousListSelection: JBList<*>? = null 70 | var maxWidth = 0 71 | var traceNotNullCount = 0 72 | 73 | traces.forEach { trace -> 74 | if (trace == null) { 75 | vertexData.add(null) 76 | return@forEach 77 | } 78 | 79 | val vertex = CoroutineFramesList(this, trace) 80 | vertex.addListSelectionListener { e -> 81 | val currentList = e.source as? JBList<*> ?: return@addListSelectionListener 82 | if (previousListSelection != currentList) { 83 | previousListSelection?.clearSelection() 84 | } 85 | previousListSelection = currentList 86 | } 87 | vertexData.add(vertex) 88 | maxWidth += vertex.preferredSize.width 89 | traceNotNullCount += 1 90 | } 91 | 92 | if (traceNotNullCount == 0) { 93 | return null 94 | } 95 | val averagePreferredWidth = maxWidth / traceNotNullCount 96 | 97 | val firstVertex = vertexData.firstOrNull() ?: return null 98 | val averagePreferredCellHeight = firstVertex.preferredSize.height / firstVertex.model.size 99 | val fontSize = firstVertex.font.size2D 100 | 101 | vertexData.forEach { vertex -> 102 | if (vertex != null) { 103 | vertex.preferredSize = Dimension(averagePreferredWidth, vertex.preferredSize.height) 104 | vertex.fixedCellHeight = averagePreferredCellHeight 105 | componentData.add(vertex) 106 | return@forEach 107 | } 108 | componentData.add(Separator()) 109 | } 110 | 111 | val forest = DraggableContainerWithEdges() 112 | componentData.forEach { forest.add(it) } 113 | forest.layout = ForestLayout() 114 | 115 | return ZoomableJBScrollPane( 116 | forest, 117 | averagePreferredWidth, 118 | averagePreferredCellHeight, 119 | fontSize, 120 | zoomLevel 121 | ) 122 | } 123 | 124 | fun createCoroutineTraces(rootValue: Node): List { 125 | val stack = Stack>().apply { push(rootValue to 0) } 126 | val parentStack = Stack() 127 | var previousLevel: Int? = null 128 | val coroutineTraces = mutableListOf() 129 | 130 | while (stack.isNotEmpty()) { 131 | val (currentNode, currentLevel) = stack.pop() 132 | val parent = if (parentStack.isNotEmpty()) parentStack.pop() else null 133 | 134 | if (parent != null && parent.num != currentNode.num) { 135 | val currentTrace = CoroutineTrace( 136 | mutableListOf(currentNode.stackFrameItem), 137 | currentNode.runningCount, 138 | currentNode.suspendedCount, 139 | currentNode.coroutinesActive 140 | ) 141 | repeat((previousLevel ?: 0) - currentLevel + 1) { 142 | coroutineTraces.add(null) 143 | } 144 | coroutineTraces.add(currentTrace) 145 | previousLevel = currentLevel 146 | } else if (parent != null) { 147 | coroutineTraces.lastOrNull()?.stackFrameItems?.add(0, currentNode.stackFrameItem) 148 | } 149 | 150 | currentNode.children.values.reversed().forEach { child -> 151 | val level = if (currentNode.num != child.num) { 152 | currentLevel + 1 153 | } else { 154 | currentLevel 155 | } 156 | stack.push(child to level) 157 | parentStack.push(currentNode) 158 | } 159 | } 160 | 161 | return coroutineTraces 162 | } 163 | 164 | private fun SuspendContextImpl.buildStackFrameGraph( 165 | rootValue: Node, 166 | coroutineDataList: List, 167 | areLibraryFramesAllowed: Boolean, 168 | addCreationFrames: Boolean 169 | ) { 170 | val isFrameAllowed = { frame: CoroutineStackFrameItem -> 171 | areLibraryFramesAllowed || !frame.isLibraryFrame(this) 172 | } 173 | 174 | val buildCoroutineFrames = { data: CoroutineInfoData -> 175 | try { 176 | val frameList = CoroutineFrameBuilder.build(data, this) 177 | if (frameList == null) { 178 | emptyList() 179 | } else if (addCreationFrames) { 180 | frameList.frames + frameList.creationFrames 181 | } else { 182 | frameList.frames 183 | } 184 | } catch (_ : Exception) { 185 | emptyList() 186 | } 187 | } 188 | 189 | buildStackFrameGraph(rootValue, coroutineDataList, isFrameAllowed, buildCoroutineFrames) 190 | } 191 | 192 | fun buildStackFrameGraph( 193 | rootValue: Node, 194 | coroutineDataList: List, 195 | isFrameAllowed: (CoroutineStackFrameItem) -> Boolean, 196 | buildCoroutineFrames: (CoroutineInfoData) -> List 197 | ) { 198 | coroutineDataList.forEach { coroutineData -> 199 | var currentNode = rootValue 200 | val frames = buildCoroutineFrames(coroutineData) 201 | 202 | frames.reversed().forEach { stackFrame -> 203 | if (isFrameAllowed(stackFrame)) { 204 | val location = stackFrame.location 205 | val child = currentNode.children.getOrPut(location) { 206 | Node(stackFrame, children = mutableMapOf()) 207 | } 208 | 209 | child.num++ 210 | coroutineData.descriptor.apply { 211 | child.coroutinesActive += "${name}${id} ${state}\n" 212 | if (state == State.SUSPENDED) 213 | child.suspendedCount++ 214 | else if (state == State.RUNNING) 215 | child.runningCount++ 216 | } 217 | currentNode = child 218 | } 219 | } 220 | } 221 | } 222 | 223 | internal fun CoroutineStackFrameItem.isLibraryFrame(suspendContext: SuspendContextImpl): Boolean { 224 | val xStackFrame = createFrame(suspendContext.debugProcess) 225 | val jvmStackFrameInfoProvider = (xStackFrame as? JVMStackFrameInfoProvider) ?: return false 226 | return jvmStackFrameInfoProvider.isInLibraryContent 227 | } 228 | -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/CoroutineFramesList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import com.intellij.debugger.engine.SuspendContextImpl 20 | import com.intellij.icons.AllIcons 21 | import com.intellij.openapi.application.ApplicationManager 22 | import com.intellij.ui.JBColor 23 | import com.intellij.ui.RowIcon 24 | import com.intellij.ui.components.JBList 25 | import com.intellij.util.IconUtil 26 | import com.intellij.util.ui.JBUI 27 | import com.intellij.xdebugger.frame.XExecutionStack 28 | import com.intellij.xdebugger.frame.XStackFrame 29 | import com.google.coroutinestacks.CoroutineTrace 30 | import com.google.coroutinestacks.isLibraryFrame 31 | import org.jetbrains.kotlin.analysis.decompiler.stub.file.ClsClassFinder 32 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CreationCoroutineStackFrameItem 33 | import org.jetbrains.kotlin.idea.debugger.coroutine.view.SimpleColoredTextIconPresentationRenderer 34 | import java.awt.* 35 | import java.awt.event.MouseAdapter 36 | import java.awt.event.MouseEvent 37 | import javax.swing.* 38 | import javax.swing.border.Border 39 | import javax.swing.border.LineBorder 40 | 41 | sealed class ListItem(val text: String) 42 | class Header(text: String, val icon: Icon, var scale: Float = 1.0F) : ListItem(text) 43 | class Frame(location: String, val isCreationFrame: Boolean, val isLibraryFrame: Boolean) : ListItem(location) 44 | 45 | class CoroutineFramesList( 46 | suspendContext: SuspendContextImpl, 47 | trace: CoroutineTrace 48 | ) : JBList() { 49 | companion object { 50 | private val itemBorder = BorderFactory.createMatteBorder(0, 0, 1, 0, JBColor.GRAY) 51 | private val leftPaddingBorder: Border = JBUI.Borders.emptyLeft(3) 52 | private val compoundBorder = BorderFactory.createCompoundBorder(itemBorder, leftPaddingBorder) 53 | 54 | private val creationFrameColor = JBColor(0xeaf6ff, 0x4f556b) 55 | private val libraryFrameColor = JBColor(0xffffe4, 0x4f4b41) 56 | private val ordinaryBorderColor = JBColor.GRAY 57 | private val currentCoroutineBorderColor = JBColor.BLUE 58 | 59 | private val runningIcon = AllIcons.Debugger.ThreadRunning 60 | private val suspendedIcon = AllIcons.Debugger.ThreadFrozen 61 | private val allStatesIcon = RowIcon(runningIcon, suspendedIcon) 62 | 63 | private const val CORNER_RADIUS = 10 64 | private const val BORDER_THICKNESS = 1 65 | } 66 | 67 | init { 68 | setListData(buildList(suspendContext, trace)) 69 | 70 | val borderColor = trace.getBorderColor(suspendContext) 71 | border = object : LineBorder(borderColor, BORDER_THICKNESS) { 72 | override fun getBorderInsets(c: Component?): Insets { 73 | val insets = super.getBorderInsets(c) 74 | return JBUI.insets(insets.top, insets.left, insets.bottom, insets.right) 75 | } 76 | 77 | override fun paintBorder(c: Component?, g: Graphics?, x: Int, y: Int, width: Int, height: Int) { 78 | val g2d = g as? Graphics2D ?: return 79 | val arc = 2 * CORNER_RADIUS 80 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 81 | g2d.color = borderColor 82 | g2d.drawRoundRect(x, y, width - 1, height - 1, arc, arc) 83 | } 84 | } 85 | 86 | cellRenderer = object : DefaultListCellRenderer() { 87 | override fun getListCellRendererComponent( 88 | list: JList<*>, 89 | value: Any, 90 | index: Int, 91 | isSelected: Boolean, 92 | cellHasFocus: Boolean 93 | ): Component { 94 | val stackFrameRenderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) 95 | if (stackFrameRenderer !is JComponent || value !is ListItem) { 96 | return stackFrameRenderer 97 | } 98 | 99 | with(stackFrameRenderer) { 100 | text = value.text 101 | 102 | val listSize = list.model.size 103 | border = when { 104 | index < listSize - 1 -> compoundBorder 105 | index == listSize - 1 -> leftPaddingBorder 106 | else -> null 107 | } 108 | 109 | when (value) { 110 | is Header -> { 111 | icon = IconUtil.scale(value.icon, null, value.scale) 112 | toolTipText = trace.coroutinesActiveLabel 113 | font = font.deriveFont(Font.BOLD) 114 | } 115 | is Frame -> { 116 | toolTipText = value.text 117 | if (!isSelected) { 118 | if (value.isCreationFrame) { 119 | background = creationFrameColor 120 | } else if (value.isLibraryFrame) { 121 | background = libraryFrameColor 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | return stackFrameRenderer 129 | } 130 | } 131 | 132 | addMouseListener(object : MouseAdapter() { 133 | override fun mouseClicked(e: MouseEvent?) { 134 | val list = e?.source as? JBList<*> ?: return 135 | val index = list.locationToIndex(e.point).takeIf { it > 0 } ?: return 136 | val stackFrameItem = trace.stackFrameItems[index - 1] ?: return 137 | 138 | val frame = stackFrameItem.createFrame(suspendContext.debugProcess) 139 | val xExecutionStack = suspendContext.activeExecutionStack as? XExecutionStack 140 | if (xExecutionStack != null && frame != null) { 141 | suspendContext.setCurrentStackFrame(xExecutionStack, frame) 142 | } 143 | } 144 | }) 145 | } 146 | 147 | private fun CoroutineTrace.getBorderColor(suspendContext: SuspendContextImpl): Color { 148 | val lastStackFrame = stackFrameItems.firstOrNull()?.location 149 | val breakpointLocation = suspendContext.location 150 | return if (breakpointLocation == lastStackFrame) { 151 | currentCoroutineBorderColor 152 | } else { 153 | ordinaryBorderColor 154 | } 155 | } 156 | 157 | private fun buildList(suspendContext: SuspendContextImpl, trace: CoroutineTrace): Array { 158 | val data = mutableListOf() 159 | val header = with(trace) { 160 | when { 161 | runningCount != 0 && suspendedCount != 0 -> 162 | Header("$runningCount Running, $suspendedCount Suspended", allStatesIcon) 163 | runningCount != 0 -> 164 | Header("$runningCount Running", runningIcon) 165 | suspendedCount != 0 -> 166 | Header("$suspendedCount Suspended", suspendedIcon) 167 | else -> 168 | return emptyArray() 169 | } 170 | } 171 | data.add(header) 172 | 173 | val renderer = SimpleColoredTextIconPresentationRenderer() 174 | for (frame in trace.stackFrameItems) { 175 | if (frame == null) continue 176 | val renderedLocation = renderer.render(frame.location).simpleString() 177 | data.add( 178 | Frame( 179 | renderedLocation, 180 | frame is CreationCoroutineStackFrameItem, 181 | frame.isLibraryFrame(suspendContext) 182 | ) 183 | ) 184 | } 185 | 186 | return data.toTypedArray() 187 | } 188 | } 189 | 190 | // Copied from org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineSelectedNodeListener#setCurrentStackFrame 191 | private fun SuspendContextImpl.setCurrentStackFrame(executionStack: XExecutionStack, stackFrame: XStackFrame) { 192 | val fileToNavigate = stackFrame.sourcePosition?.file ?: return 193 | val session = debugProcess.session.xDebugSession ?: return 194 | if (!ClsClassFinder.isKotlinInternalCompiledFile(fileToNavigate)) { 195 | ApplicationManager.getApplication().invokeLater { 196 | session.setCurrentStackFrame(executionStack, stackFrame, false) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/CoroutineStacksPanel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import com.intellij.debugger.engine.DebugProcessListener 20 | import com.intellij.debugger.engine.JavaDebugProcess 21 | import com.intellij.debugger.engine.SuspendContext 22 | import com.intellij.debugger.engine.SuspendContextImpl 23 | import com.intellij.debugger.engine.events.SuspendContextCommandImpl 24 | import com.intellij.debugger.impl.DebuggerManagerListener 25 | import com.intellij.debugger.impl.DebuggerSession 26 | import com.intellij.debugger.impl.PrioritizedTask 27 | import com.intellij.icons.AllIcons 28 | import com.intellij.openapi.application.ApplicationManager 29 | import com.intellij.openapi.application.ModalityState 30 | import com.intellij.openapi.application.runInEdt 31 | import com.intellij.openapi.project.Project 32 | import com.intellij.openapi.ui.ComboBox 33 | import com.intellij.openapi.wm.ToolWindow 34 | import com.intellij.openapi.wm.ToolWindowManager 35 | import com.intellij.openapi.wm.ex.ToolWindowManagerListener 36 | import com.intellij.ui.AnimatedIcon 37 | import com.intellij.ui.JBColor.GRAY 38 | import com.intellij.ui.components.JBPanelWithEmptyText 39 | import com.intellij.xdebugger.XDebuggerManager 40 | import com.google.coroutinestacks.CoroutineStacksBundle.message 41 | import com.google.coroutinestacks.Node 42 | import com.google.coroutinestacks.buildCoroutineStackForest 43 | import com.intellij.openapi.ui.MessageType 44 | import com.intellij.xdebugger.impl.XDebuggerManagerImpl 45 | import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle 46 | import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineDumpAction 47 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoCache 48 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData 49 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.toCompleteCoroutineInfoData 50 | import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy 51 | import java.awt.Dimension 52 | import javax.swing.* 53 | 54 | class CoroutineStacksPanel(private val project: Project) : JBPanelWithEmptyText() { 55 | companion object { 56 | val dispatcherSelectionMenuSize = Dimension(200, 25) 57 | const val MAXIMUM_ZOOM_LEVEL = 1f 58 | const val MINIMUM_ZOOM_LEVEL = -0.5 59 | const val SCALE_FACTOR = 0.1f 60 | 61 | // Should be the same as in plugin.xml 62 | const val COROUTINE_STACKS_TOOL_WINDOW_ID = "Coroutine Stacks" 63 | } 64 | 65 | private val panelContent = Box.createVerticalBox() 66 | private val forest = Box.createVerticalBox() 67 | private val loadingCoroutineDataLabel = JLabel(message("loading.coroutine.data"), AnimatedIcon.Default(), SwingConstants.LEFT) 68 | private val buildingPanelLabel = JLabel(message("building.panel"), AnimatedIcon.Default(), SwingConstants.LEFT) 69 | private var coroutineStackForest: ZoomableJBScrollPane? = null 70 | 71 | private var zoomLevel = 0f 72 | private var isPanelAlreadyBuilt = false 73 | private var isToolWindowActive = false 74 | 75 | // Creation frames are not fetched by Intellij IDEA since 2024.2 76 | private var addCreationFrames = false 77 | 78 | var areLibraryFramesAllowed = true 79 | 80 | private val panelBuilderListener = object : DebugProcessListener { 81 | override fun paused(suspendContext: SuspendContext) { 82 | isPanelAlreadyBuilt = false 83 | if (suspendContext !is SuspendContextImpl) { 84 | emptyText.text = message("coroutine.stacks.could.not.be.built") 85 | return 86 | } 87 | scheduleBuildPanelCommand(suspendContext) 88 | } 89 | 90 | override fun resumed(suspendContext: SuspendContext?) { 91 | panelContent.removeAll() 92 | panelContent.add(forest) 93 | } 94 | } 95 | 96 | init { 97 | areLibraryFramesAllowed = true 98 | layout = BoxLayout(this, BoxLayout.Y_AXIS) 99 | emptyText.text = message("no.java.debug.process.is.running") 100 | 101 | project.messageBus.connect() 102 | .subscribe(ToolWindowManagerListener.TOPIC, object : ToolWindowManagerListener { 103 | override fun stateChanged( 104 | toolWindowManager: ToolWindowManager, 105 | changeType: ToolWindowManagerListener.ToolWindowManagerEventType 106 | ) { 107 | if (toolWindowManager.lastActiveToolWindowId != COROUTINE_STACKS_TOOL_WINDOW_ID) { 108 | return 109 | } 110 | 111 | when (changeType) { 112 | ToolWindowManagerListener.ToolWindowManagerEventType.HideToolWindow -> isToolWindowActive = false 113 | ToolWindowManagerListener.ToolWindowManagerEventType.ActivateToolWindow -> { 114 | isToolWindowActive = true 115 | val suspendContext = project.getSuspendContext() ?: return 116 | scheduleBuildPanelCommand(suspendContext) 117 | } 118 | else -> {} 119 | } 120 | } 121 | 122 | override fun toolWindowShown(toolWindow: ToolWindow) { 123 | if (toolWindow.id == COROUTINE_STACKS_TOOL_WINDOW_ID) { 124 | isToolWindowActive = true 125 | } 126 | } 127 | }) 128 | 129 | project.messageBus.connect() 130 | .subscribe(DebuggerManagerListener.TOPIC, object : DebuggerManagerListener { 131 | override fun sessionAttached(session: DebuggerSession?) { 132 | emptyText.text = message("should.be.stopped.on.a.breakpoint") 133 | } 134 | 135 | override fun sessionCreated(session: DebuggerSession) { 136 | session.process.addDebugProcessListener(panelBuilderListener) 137 | } 138 | 139 | override fun sessionRemoved(session: DebuggerSession) { 140 | emptyText.text = message("no.java.debug.process.is.running") 141 | emptyText.component.isVisible = true 142 | removeAll() 143 | panelContent.removeAll() 144 | panelContent.add(forest) 145 | } 146 | }) 147 | } 148 | 149 | private fun buildCoroutineGraph(suspendContextImpl: SuspendContextImpl) { 150 | forest.replaceContentsWithLabel(loadingCoroutineDataLabel) 151 | 152 | val coroutineInfoCache: CoroutineInfoCache 153 | try { 154 | coroutineInfoCache = CoroutineDebugProbesProxy(suspendContextImpl).dumpCoroutines() 155 | } catch (e: Exception) { 156 | emptyText.text = message("nothing.to.show") 157 | return 158 | } 159 | 160 | if (coroutineInfoCache.cache.isEmpty()) { 161 | emptyText.text = message("nothing.to.show") 162 | return 163 | } 164 | 165 | val dispatcherToCoroutineDataList = mutableMapOf>() 166 | for (data in coroutineInfoCache.cache) { 167 | data.descriptor.dispatcher?.let { 168 | dispatcherToCoroutineDataList.getOrPut(it) { mutableListOf() }.add(data) 169 | } 170 | } 171 | 172 | val firstDispatcher = dispatcherToCoroutineDataList.keys.firstOrNull() 173 | val context = GraphBuildingContext( 174 | suspendContextImpl, 175 | dispatcherToCoroutineDataList, 176 | firstDispatcher 177 | ) 178 | panelContent.add(CoroutineStacksPanelHeader(context)) 179 | panelContent.add(forest) 180 | add(panelContent) 181 | 182 | context.rebuildGraph() 183 | } 184 | 185 | fun updateCoroutineStackForest( 186 | coroutineDataList: List, 187 | suspendContextImpl: SuspendContextImpl 188 | ) { 189 | forest.replaceContentsWithLabel(buildingPanelLabel) 190 | suspendContextImpl.debugProcess.managerThread.schedule(object : SuspendContextCommandImpl(suspendContextImpl) { 191 | override fun contextAction(suspendContext: SuspendContextImpl) { 192 | val root = Node() 193 | coroutineStackForest = suspendContextImpl.buildCoroutineStackForest( 194 | root, 195 | coroutineDataList, 196 | areLibraryFramesAllowed, 197 | addCreationFrames, 198 | zoomLevel, 199 | ) 200 | if (coroutineStackForest == null) { 201 | forest.replaceContentsWithLabel(message("nothing.to.show")) 202 | return 203 | } 204 | 205 | runInEdt { 206 | forest.removeAll() 207 | coroutineStackForest?.verticalScrollBar?.apply { 208 | value = maximum 209 | } 210 | forest.add(coroutineStackForest) 211 | updateUI() 212 | } 213 | } 214 | 215 | override fun getPriority() = 216 | PrioritizedTask.Priority.NORMAL 217 | }) 218 | } 219 | 220 | private fun scheduleBuildPanelCommand(suspendContext: SuspendContextImpl) { 221 | if (!isToolWindowActive || isPanelAlreadyBuilt) { 222 | return 223 | } 224 | 225 | suspendContext.debugProcess.managerThread.schedule(object : SuspendContextCommandImpl(suspendContext) { 226 | override fun contextAction(suspendContext: SuspendContextImpl) { 227 | emptyText.component.isVisible = false 228 | buildCoroutineGraph(suspendContext) 229 | } 230 | 231 | override fun getPriority() = 232 | PrioritizedTask.Priority.LOW 233 | }) 234 | isPanelAlreadyBuilt = true 235 | } 236 | 237 | inner class GraphBuildingContext( 238 | val suspendContext: SuspendContextImpl, 239 | val dispatcherToCoroutineDataList: Map>, 240 | var selectedDispatcher: String? 241 | ) { 242 | fun rebuildGraph() { 243 | val coroutineDataList = dispatcherToCoroutineDataList[selectedDispatcher] 244 | if (!coroutineDataList.isNullOrEmpty()) { 245 | updateCoroutineStackForest(coroutineDataList, suspendContext) 246 | } 247 | } 248 | } 249 | 250 | inner class CoroutineStacksPanelHeader(context: GraphBuildingContext) : Box(BoxLayout.X_AXIS) { 251 | init { 252 | add(LibraryFrameToggle(context)) 253 | add(CaptureDumpButton(context)) 254 | // add(CreationFramesToggle(context)) 255 | add(createHorizontalGlue()) 256 | add(DispatcherDropdownMenu(context)) 257 | add(createHorizontalGlue()) 258 | add(ZoomInButton()) 259 | add(ZoomOutButton()) 260 | add(ZoomToOriginalSizeButton()) 261 | } 262 | } 263 | 264 | inner class CaptureDumpButton( 265 | private val context: GraphBuildingContext 266 | ) : PanelButton(AllIcons.Actions.Dump, message("get.coroutine.dump")) { 267 | @Suppress("DEPRECATION") 268 | override fun action() { 269 | val suspendContext = context.suspendContext 270 | val process = suspendContext.debugProcess 271 | val session = process.session 272 | process.managerThread.schedule(object : SuspendContextCommandImpl(suspendContext) { 273 | override fun contextAction(suspendContext: SuspendContextImpl) { 274 | val coroutines = context.dispatcherToCoroutineDataList 275 | .values 276 | .flatten() 277 | .map { it.toCompleteCoroutineInfoData() } 278 | if (coroutines.isEmpty()) { 279 | return 280 | } 281 | 282 | ApplicationManager.getApplication().invokeLater({ 283 | val ui = session.xDebugSession?.ui ?: return@invokeLater 284 | CoroutineDumpAction().addCoroutineDump(project, coroutines, ui, session.searchScope) 285 | }, ModalityState.NON_MODAL) 286 | } 287 | }) 288 | } 289 | } 290 | 291 | inner class ZoomToOriginalSizeButton : PanelButton(message("zoom.to.original.size.button.hint")) { 292 | init { 293 | text = message("zoom.to.original.size.button.label") 294 | } 295 | 296 | override fun action() { 297 | coroutineStackForest?.scale(-zoomLevel) 298 | zoomLevel = 0f 299 | } 300 | } 301 | 302 | inner class ZoomInButton : PanelButton(AllIcons.General.ZoomIn, message("zoom.in.button.hint")) { 303 | override fun action() { 304 | if (zoomLevel > MAXIMUM_ZOOM_LEVEL) { 305 | return 306 | } 307 | zoomLevel += SCALE_FACTOR 308 | coroutineStackForest?.scale(SCALE_FACTOR) 309 | } 310 | } 311 | 312 | inner class ZoomOutButton : PanelButton(AllIcons.General.ZoomOut, message("zoom.out.button.hint")) { 313 | override fun action() { 314 | if (zoomLevel < MINIMUM_ZOOM_LEVEL) { 315 | return 316 | } 317 | zoomLevel -= SCALE_FACTOR 318 | coroutineStackForest?.scale(-SCALE_FACTOR) 319 | } 320 | } 321 | 322 | inner class DispatcherDropdownMenu( 323 | context: GraphBuildingContext 324 | ) : ComboBox(context.dispatcherToCoroutineDataList.keys.toTypedArray()) { 325 | init { 326 | addActionListener { 327 | context.selectedDispatcher = selectedItem as? String 328 | context.rebuildGraph() 329 | } 330 | apply { 331 | preferredSize = dispatcherSelectionMenuSize 332 | maximumSize = dispatcherSelectionMenuSize 333 | minimumSize = dispatcherSelectionMenuSize 334 | } 335 | } 336 | } 337 | 338 | inner class LibraryFrameToggle( 339 | private val context: GraphBuildingContext 340 | ) : PanelToggleableButton( 341 | AllIcons.General.Filter, 342 | message("show.library.frames"), 343 | message("hide.library.frames"), 344 | areLibraryFramesAllowed 345 | ) { 346 | override var condition by ::areLibraryFramesAllowed 347 | 348 | override fun action() = context.rebuildGraph() 349 | } 350 | 351 | inner class CreationFramesToggle( 352 | private val context: GraphBuildingContext 353 | ) : PanelToggleableButton( 354 | AllIcons.Debugger.Frame, 355 | message("add.creation.frames"), 356 | message("remove.creation.frames"), 357 | !addCreationFrames 358 | ) { 359 | override var condition by ::addCreationFrames 360 | 361 | override fun action() = context.rebuildGraph() 362 | } 363 | } 364 | 365 | private fun Box.replaceContentsWithLabel(content: String) { 366 | val label = JLabel(content) 367 | replaceContentsWithLabel(label) 368 | } 369 | 370 | private fun Box.replaceContentsWithLabel(label: JLabel) { 371 | runInEdt { 372 | label.apply { 373 | alignmentX = JBPanelWithEmptyText.CENTER_ALIGNMENT 374 | alignmentY = JBPanelWithEmptyText.CENTER_ALIGNMENT 375 | foreground = GRAY 376 | } 377 | removeAll() 378 | add(Box.createVerticalGlue()) 379 | add(label) 380 | add(Box.createVerticalGlue()) 381 | updateUI() 382 | } 383 | } 384 | 385 | private fun Project.getSuspendContext(): SuspendContextImpl? { 386 | val currentSession = XDebuggerManager.getInstance(this).currentSession ?: return null 387 | val currentProcess = (currentSession.debugProcess as? JavaDebugProcess)?.debuggerSession?.process ?: return null 388 | return currentProcess.suspendManager.pausedContext 389 | } 390 | -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/DraggableContainerWithEdges.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import com.intellij.ui.JBColor 20 | import java.awt.* 21 | import java.awt.event.* 22 | import java.awt.geom.Path2D 23 | import javax.swing.JViewport 24 | 25 | class DraggableContainerWithEdges : Container(), MouseMotionListener, MouseListener { 26 | companion object { 27 | const val BEZIER_CURVE_CONTROL_POINT_OFFSET = 20 28 | const val EDGE_WIDTH = 1.0F 29 | } 30 | 31 | private var holdPointOnView: Point? = null 32 | 33 | init { 34 | addMouseMotionListener(this) 35 | addMouseListener(this) 36 | } 37 | 38 | override fun paint(g: Graphics) { 39 | super.paint(g) 40 | val g2d = g as? Graphics2D ?: return 41 | 42 | dfs(object : ComponentVisitor { 43 | override fun visitComponent(parentIndex: Int, index: Int) { 44 | if (parentIndex < 0) return 45 | val comp = getComponent(index) 46 | val parentComp = getComponent(parentIndex) 47 | val parentTopCenter = Point(parentComp.x + parentComp.preferredSize.width / 2, parentComp.y) 48 | val compBottomCenter = Point(comp.x + comp.preferredSize.width / 2, comp.y + comp.preferredSize.height) 49 | val bezierCurve = calculateBezierCurve(parentTopCenter, compBottomCenter) 50 | 51 | g2d.stroke = BasicStroke(EDGE_WIDTH) 52 | g2d.color = JBColor.BLUE 53 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 54 | g2d.draw(bezierCurve) 55 | } 56 | }) 57 | } 58 | 59 | private fun calculateBezierCurve(start: Point, end: Point): Path2D { 60 | val path = Path2D.Double() 61 | path.moveTo(start.x.toDouble(), start.y.toDouble()) 62 | path.curveTo( 63 | start.x.toDouble(), start.y.toDouble() - BEZIER_CURVE_CONTROL_POINT_OFFSET, 64 | end.x.toDouble(), end.y.toDouble() + BEZIER_CURVE_CONTROL_POINT_OFFSET, 65 | end.x.toDouble(), end.y.toDouble() 66 | ) 67 | return path 68 | } 69 | 70 | override fun mouseDragged(e: MouseEvent) { 71 | val viewport = parent as? JViewport ?: return 72 | val holdPointOnView = holdPointOnView ?: return 73 | val dragEventPoint = e.point 74 | val viewPos = viewport.viewPosition 75 | val maxViewPosX = width - viewport.width 76 | val maxViewPosY = height - viewport.height 77 | if (maxViewPosX > 0) { 78 | viewPos.x -= dragEventPoint.x - holdPointOnView.x 79 | if (viewPos.x < 0) { 80 | viewPos.x = 0 81 | holdPointOnView.x = dragEventPoint.x 82 | } 83 | if (viewPos.x > maxViewPosX) { 84 | viewPos.x = maxViewPosX 85 | holdPointOnView.x = dragEventPoint.x 86 | } 87 | } 88 | 89 | if (maxViewPosY > 0) { 90 | viewPos.y -= dragEventPoint.y - holdPointOnView.y 91 | if (viewPos.y < 0) { 92 | viewPos.y = 0 93 | holdPointOnView.y = dragEventPoint.y 94 | } 95 | if (viewPos.y > maxViewPosY) { 96 | viewPos.y = maxViewPosY 97 | holdPointOnView.y = dragEventPoint.y 98 | } 99 | } 100 | 101 | viewport.viewPosition = viewPos 102 | } 103 | 104 | override fun mousePressed(e: MouseEvent?) { 105 | e ?: return 106 | cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR) 107 | holdPointOnView = e.point 108 | } 109 | 110 | override fun mouseReleased(e: MouseEvent?) { 111 | setCursor(null) 112 | } 113 | 114 | override fun mouseMoved(e: MouseEvent?) {} 115 | override fun mouseClicked(e: MouseEvent?) {} 116 | override fun mouseEntered(e: MouseEvent?) {} 117 | override fun mouseExited(e: MouseEvent?) {} 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/ForestLayout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import java.awt.Component 20 | import java.awt.Container 21 | import java.awt.Dimension 22 | import java.util.* 23 | import javax.swing.ScrollPaneLayout 24 | 25 | class ForestLayout(private val xPadding: Int = 50, private val yPadding: Int = 50) : ScrollPaneLayout() { 26 | override fun addLayoutComponent(name: String?, comp: Component?) { 27 | } 28 | 29 | override fun removeLayoutComponent(comp: Component?) { 30 | } 31 | 32 | override fun preferredLayoutSize(parent: Container): Dimension { 33 | var maxY = 0 34 | var width = xPadding 35 | var currentHeight = yPadding 36 | parent.dfs(object : ComponentVisitor { 37 | override fun visitComponent(parentIndex: Int, index: Int) { 38 | val compSize = parent.getComponentSize(index) 39 | val nextComponent = if (index + 1 < parent.componentCount) parent.getComponent(index + 1) else null 40 | if (nextComponent == null || nextComponent is Separator) { 41 | width += compSize.width + xPadding 42 | } 43 | 44 | currentHeight += compSize.height + yPadding 45 | if (maxY < currentHeight) { 46 | maxY = currentHeight 47 | } 48 | } 49 | 50 | override fun leaveComponent(parentIndex: Int, index: Int) { 51 | currentHeight -= yPadding + parent.getComponentSize(index).height 52 | } 53 | }) 54 | 55 | val insets = parent.insets 56 | return Dimension(width + insets.left + insets.right, maxY + insets.top + insets.bottom) 57 | } 58 | 59 | override fun minimumLayoutSize(parent: Container): Dimension = parent.preferredSize 60 | 61 | override fun layoutContainer(parent: Container) { 62 | val size = parent.componentCount 63 | if (size == 0) { 64 | return 65 | } 66 | 67 | val widthToDrawSubtree = Array(size) { -xPadding } 68 | val ys = Array(size) { 0 } 69 | val xs = Array(size) { 0 } 70 | val childrenIndices = Array(size) { mutableListOf() } 71 | val parentSize = parent.size 72 | var currentHeight = parentSize.height - yPadding 73 | parent.dfs(object : ComponentVisitor { 74 | override fun visitComponent(parentIndex: Int, index: Int) { 75 | if (parentIndex != -1) { 76 | childrenIndices[parentIndex].add(index) 77 | } 78 | 79 | currentHeight -= parent.getComponentSize(index).height 80 | ys[index] = currentHeight 81 | currentHeight -= yPadding 82 | } 83 | 84 | override fun leaveComponent(parentIndex: Int, index: Int) { 85 | val compSize = parent.getComponentSize(index) 86 | currentHeight += yPadding + compSize.height 87 | if (widthToDrawSubtree[index] <= 0) { 88 | widthToDrawSubtree[index] = compSize.width + xPadding 89 | } 90 | 91 | if (parentIndex < 0) { 92 | return 93 | } 94 | widthToDrawSubtree[parentIndex] += widthToDrawSubtree[index] + xPadding 95 | } 96 | }) 97 | 98 | var mostRightX = 0 99 | parent.dfs(object : ComponentVisitor { 100 | override fun leaveComponent(parentIndex: Int, index: Int) { 101 | val numChildren = childrenIndices[index].size 102 | if (numChildren == 0) { 103 | xs[index] = mostRightX + xPadding 104 | mostRightX += xPadding + parent.getComponentSize(index).width 105 | } else if (numChildren % 2 == 0) { 106 | for (child in childrenIndices[index]) { 107 | xs[index] += xs[child] 108 | } 109 | xs[index] /= numChildren 110 | } else { 111 | xs[index] = xs[childrenIndices[index][numChildren / 2]] 112 | } 113 | } 114 | }) 115 | 116 | for (i in 0 until size) { 117 | val comp = parent.getComponent(i) 118 | if (!comp.isVisible || comp is Separator) { 119 | continue 120 | } 121 | 122 | val compSize = comp.preferredSize 123 | comp.setBounds(xs[i], ys[i], compSize.width, compSize.height) 124 | } 125 | } 126 | 127 | private fun Container.getComponentSize(index: Int): Dimension = 128 | getComponent(index).preferredSize 129 | } 130 | 131 | class Separator : Component() 132 | 133 | // A visitor to provide dfs component processing 134 | internal interface ComponentVisitor { 135 | fun visitComponent(parentIndex: Int, index: Int) { 136 | } 137 | 138 | fun leaveComponent(parentIndex: Int, index: Int) { 139 | } 140 | } 141 | 142 | internal fun Container.dfs(visitor: ComponentVisitor) { 143 | if (componentCount == 0) { 144 | return 145 | } 146 | 147 | val stack = Stack() 148 | val parents = Stack() 149 | stack.add(0) 150 | parents.add(-1) 151 | while (stack.isNotEmpty()) { 152 | var currentIndex = stack.pop() 153 | var currentParent = parents.peek() 154 | 155 | fun leaveComponent() { 156 | visitor.leaveComponent(currentParent, currentIndex) 157 | currentIndex = currentParent 158 | if (currentParent != -1) { 159 | parents.pop() 160 | currentParent = parents.peek() 161 | } 162 | } 163 | 164 | visitor.visitComponent(currentParent, currentIndex) 165 | var i = currentIndex + 1 166 | while (i < componentCount) { 167 | if (getComponent(i) is Separator) { 168 | leaveComponent() 169 | i += 1 170 | } else { 171 | stack.push(i) 172 | parents.push(currentIndex) 173 | break 174 | } 175 | } 176 | 177 | if (stack.isEmpty() && currentIndex != -1) { 178 | while (currentIndex != -1) { 179 | leaveComponent() 180 | } 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/ZoomableJBScrollPane.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import com.intellij.ui.components.JBScrollPane 20 | import java.awt.Component 21 | import java.awt.Container 22 | import java.awt.Dimension 23 | 24 | class ZoomableJBScrollPane( 25 | view: Component, 26 | private val averagePreferredWidth: Int, 27 | private val averagePreferredCellHeight: Int, 28 | private val preferredFontSize: Float, 29 | initialZoomLevel: Float 30 | ) : JBScrollPane(view) { 31 | 32 | init { 33 | scale(initialZoomLevel) 34 | verticalScrollBar.value = verticalScrollBar.maximum 35 | } 36 | 37 | fun scale(scaleFactor: Float) { 38 | val view = viewport.view as? Container ?: return 39 | 40 | view.components.forEach { component -> 41 | if (component is CoroutineFramesList) { 42 | component.zoom(scaleFactor) 43 | } 44 | } 45 | } 46 | 47 | private fun CoroutineFramesList.zoom(scaleFactor: Float) { 48 | if (model.size == 0) { 49 | return 50 | } 51 | 52 | val header = model.getElementAt(0) as? Header 53 | if (header != null) { 54 | header.scale += scaleFactor 55 | } 56 | 57 | font = font.deriveFont(font.size2D + preferredFontSize * scaleFactor) 58 | fixedCellHeight += (averagePreferredCellHeight * scaleFactor).toInt() 59 | preferredSize = Dimension( 60 | preferredSize.width + (averagePreferredWidth * scaleFactor).toInt(), 61 | fixedCellHeight * model.size 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/kotlin/com/google/coroutinestacks/ui/buttons.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.ui 18 | 19 | import java.awt.event.* 20 | import javax.swing.Icon 21 | import javax.swing.JButton 22 | 23 | abstract class PanelButton(tooltip: String) : JButton() { 24 | constructor(icon: Icon, tooltip: String) : this(tooltip) { 25 | this.icon = icon 26 | } 27 | 28 | init { 29 | toolTipText = tooltip 30 | transparent = true 31 | addMouseListener(object : MouseAdapter() { 32 | override fun mousePressed(e: MouseEvent?) { 33 | super.mousePressed(e) 34 | transparent = false 35 | } 36 | 37 | override fun mouseReleased(e: MouseEvent?) { 38 | super.mouseReleased(e) 39 | transparent = true 40 | } 41 | }) 42 | 43 | addActionListener { 44 | action() 45 | } 46 | } 47 | 48 | abstract fun action() 49 | 50 | final override fun addMouseListener(l: MouseListener?) = 51 | super.addMouseListener(l) 52 | 53 | final override fun addActionListener(l: ActionListener?) = 54 | super.addActionListener(l) 55 | } 56 | 57 | abstract class PanelToggleableButton( 58 | icon: Icon, 59 | private val falseConditionText: String, 60 | private val trueConditionText: String, 61 | isTransparent: Boolean = true 62 | ) : JButton(icon) { 63 | abstract var condition: Boolean 64 | 65 | init { 66 | transparent = isTransparent 67 | setToolTip() 68 | addActionListener { 69 | condition = !condition 70 | transparent = !transparent 71 | setToolTip() 72 | action() 73 | } 74 | } 75 | 76 | abstract fun action() 77 | 78 | private fun setToolTip() { 79 | toolTipText = if (condition) { 80 | trueConditionText 81 | } else { 82 | falseConditionText 83 | } 84 | } 85 | 86 | final override fun addActionListener(l: ActionListener?) = 87 | super.addActionListener(l) 88 | } 89 | 90 | internal var JButton.transparent: Boolean 91 | get() = !isOpaque 92 | set(state) { 93 | isOpaque = !state 94 | isContentAreaFilled = !state 95 | isBorderPainted = !state 96 | } 97 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | com.google.CoroutineStacks 21 | 22 | 24 | Coroutine Stacks 25 | 26 | 27 | Nikita Nazarov 28 | 29 | 32 | 33 |
36 | Quick Start Guide
37 | Issue tracker
38 | ]]> 39 |
40 | 41 | 42 | com.intellij.modules.androidstudio 43 | 44 | 45 | 47 | com.intellij.modules.platform 48 | com.intellij.java 49 | org.jetbrains.kotlin 50 | 51 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/messages/CoroutineStacksBundle.properties: -------------------------------------------------------------------------------- 1 | no.java.debug.process.is.running=No Java debug process is currently running 2 | should.be.stopped.on.a.breakpoint=The debug process should be stopped on a breakpoint 3 | coroutine.stacks.could.not.be.built=Coroutine Stacks could not be built 4 | nothing.to.show=No stack traces are available for display 5 | hide.library.frames=Hide frames from library 6 | show.library.frames=Show all frames 7 | zoom.in.button.hint=Zoom In 8 | zoom.out.button.hint=Zoom Out 9 | get.coroutine.dump=Get Coroutine Dump 10 | zoom.to.original.size.button.label=1:1 11 | zoom.to.original.size.button.hint=Zoom to Original Size 12 | remove.creation.frames=Remove creation stack frames 13 | add.creation.frames=Add creation stack frames 14 | loading.coroutine.data=Loading coroutine data... 15 | building.panel=Building panel... 16 | -------------------------------------------------------------------------------- /src/test/kotlin/com/google/coroutinestacks/test/CoroutineStacksFromDumpTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.test 18 | 19 | import com.google.coroutinestacks.test.utils.buildForest 20 | import org.junit.jupiter.api.Assertions 21 | import org.junit.jupiter.api.Test 22 | import com.google.coroutinestacks.test.utils.parseCoroutineDump 23 | import java.io.File 24 | 25 | const val DUMP_DIR = "src/testData/dumps/" 26 | const val OUT_DIR = "src/testData/outs/" 27 | 28 | class CoroutineStacksFromDumpTest { 29 | private fun runTest(fileName: String) { 30 | val info = parseCoroutineDump(DUMP_DIR + fileName) 31 | val actualForest = buildForest(info) 32 | val expectedForest = File(OUT_DIR + fileName).readText() 33 | Assertions.assertEquals(actualForest, expectedForest) 34 | } 35 | 36 | @Test 37 | fun testNoChildren() = runTest("noChildren.txt") 38 | 39 | @Test 40 | fun testTwoChildren() = runTest("twoChildren.txt") 41 | 42 | @Test 43 | fun testTwoChildrenChat() = runTest("twoChildrenChat.txt") 44 | } 45 | -------------------------------------------------------------------------------- /src/test/kotlin/com/google/coroutinestacks/test/utils/coroutineDumpParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.test.utils 18 | 19 | import com.google.coroutinestacks.CoroutineTrace 20 | import com.google.coroutinestacks.Node 21 | import com.google.coroutinestacks.buildStackFrameGraph 22 | import com.google.coroutinestacks.createCoroutineTraces 23 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData 24 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.DefaultCoroutineStackFrameItem 25 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.State 26 | import java.io.IOException 27 | import java.nio.file.Files 28 | import java.nio.file.Paths 29 | import java.util.* 30 | 31 | fun buildForest(info: List): String { 32 | val root = Node() 33 | buildStackFrameGraph( 34 | root, 35 | info, 36 | isFrameAllowed = { true }, 37 | buildCoroutineFrames = { it.continuationStackFrames } 38 | ) 39 | val traces = createCoroutineTraces(root) 40 | return buildForestFromTraces(traces) 41 | } 42 | 43 | fun parseCoroutineDump(fileName: String): List { 44 | val result = mutableListOf() 45 | try { 46 | val lines = Files.readAllLines(Paths.get(fileName)) 47 | var currentInfo: MockCoroutineInfoData? = null 48 | for (line in lines) { 49 | val trimmedLine = line.trim() 50 | if (trimmedLine.startsWith("\"")) { 51 | currentInfo?.let { result.add(it) } 52 | val state = when (line.substringAfter("state: ")) { 53 | "SUSPENDED" -> State.SUSPENDED 54 | "RUNNING" -> State.RUNNING 55 | else -> null 56 | } ?: continue 57 | currentInfo = MockCoroutineInfoData(state) 58 | } else if (trimmedLine.isNotEmpty()) { 59 | currentInfo?.continuationStackFrames?.add( 60 | DefaultCoroutineStackFrameItem(MockLocation(trimmedLine), emptyList()) 61 | ) 62 | } 63 | } 64 | currentInfo?.let { result.add(it) } 65 | } catch (_: IOException) { 66 | } 67 | 68 | return result 69 | } 70 | 71 | private fun buildForestFromTraces(traces: List): String = buildString { 72 | val currentIndentation = LinkedList() 73 | for (trace in traces) { 74 | if (trace == null) { 75 | if (currentIndentation.isNotEmpty()) { 76 | currentIndentation.pop() 77 | } 78 | continue 79 | } 80 | 81 | val indentation = currentIndentation.joinToString("") 82 | append(indentation) 83 | 84 | val totalCoroutines = trace.runningCount + trace.suspendedCount 85 | val header = "$totalCoroutines " + if (totalCoroutines > 1) "Coroutines" else "Coroutine" 86 | append(header) 87 | append(trace.coroutinesActiveLabel.replace("\n", ",")) 88 | for (frame in trace.stackFrameItems.reversed()) { 89 | val label = (frame?.location as? MockLocation)?.label 90 | if (label != null) { 91 | append("\n$indentation\t") 92 | append(label) 93 | } 94 | } 95 | append("\n") 96 | currentIndentation.push('\t') 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/kotlin/com/google/coroutinestacks/test/utils/mocks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.coroutinestacks.test.utils 18 | 19 | import com.sun.jdi.Location 20 | import org.jetbrains.kotlin.idea.debugger.coroutine.data.* 21 | 22 | class MockCoroutineInfoData(state: State) : CoroutineInfoData( 23 | CoroutineDescriptor("", "", state, null, "") 24 | ) { 25 | override val activeThread = null 26 | override val creationStackFrames: List = emptyList() 27 | override val continuationStackFrames: MutableList = mutableListOf() 28 | override val jobHierarchy = emptyList() 29 | } 30 | 31 | class MockLocation(val label: String) : Location { 32 | override fun hashCode(): Int { 33 | return label.hashCode() 34 | } 35 | 36 | override fun equals(other: Any?): Boolean { 37 | if (this === other) return true 38 | if (other !is MockLocation) return false 39 | return label == other.label 40 | } 41 | 42 | override fun virtualMachine() = throw UnsupportedOperationException() 43 | override fun compareTo(other: Location) = throw UnsupportedOperationException() 44 | override fun declaringType() = throw UnsupportedOperationException() 45 | override fun method() = throw UnsupportedOperationException() 46 | override fun codeIndex() = throw UnsupportedOperationException() 47 | override fun sourceName() = throw UnsupportedOperationException() 48 | override fun sourceName(stratum: String?) = throw UnsupportedOperationException() 49 | override fun sourcePath() = throw UnsupportedOperationException() 50 | override fun sourcePath(stratum: String?) = throw UnsupportedOperationException() 51 | override fun lineNumber() = throw UnsupportedOperationException() 52 | override fun lineNumber(stratum: String?) = throw UnsupportedOperationException() 53 | } 54 | -------------------------------------------------------------------------------- /src/testData/README.md: -------------------------------------------------------------------------------- 1 | # How to add a test case 2 | 1. Capture a coroutine dump of an application of interest and paste it in `dumps/` 3 | 2. Run a script to build a coroutine forest: `python parse_dumps.py`. Don't forget to install requirements with `pip install requirements.txt` 4 | 3. Add a corresponding test case to `CoroutineStacksFromDumpTest` 5 | -------------------------------------------------------------------------------- /src/testData/dumps/noChildren.txt: -------------------------------------------------------------------------------- 1 | "coroutine", state: RUNNING 2 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invokeSuspend(ChatApplicationTest.kt:75) 3 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invoke(ChatApplicationTest.kt:-1) 4 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invoke(ChatApplicationTest.kt:-1) 5 | at io.ktor.client.plugins.websocket.BuildersKt.webSocket(builders.kt:101) 6 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1.invokeSuspend(ChatApplicationTest.kt:72) 7 | at io.ktor.server.testing.TestApplicationKt$testApplication$builder$1$1.invokeSuspend(TestApplication.kt:335) 8 | 9 | 10 | "nonce-generator", state: SUSPENDED 11 | at io.ktor.util.NonceKt$nonceGeneratorJob$1.invokeSuspend(Nonce.kt:76) 12 | 13 | 14 | "coroutine", state: SUSPENDED 15 | at io.ktor.server.engine.BaseApplicationEngine$3.invokeSuspend(BaseApplicationEngine.kt:75) 16 | 17 | 18 | "coroutine", state: SUSPENDED 19 | at io.ktor.server.engine.EngineContextCancellationHelperKt$launchOnCancellation$1.invokeSuspend(EngineContextCancellationHelper.kt:37) 20 | 21 | 22 | "coroutine", state: SUSPENDED 23 | 24 | 25 | "coroutine", state: SUSPENDED 26 | at io.ktor.utils.io.ByteBufferChannel.readSuspendImpl(ByteBufferChannel.kt:2230) 27 | at io.ktor.utils.io.ByteBufferChannel.copyDirect$ktor_io(ByteBufferChannel.kt:1265) 28 | at io.ktor.utils.io.ByteReadChannelKt.copyAndClose(ByteReadChannel.kt:255) 29 | at io.ktor.server.testing.TestApplicationResponse$responseChannel$job$1.invokeSuspend(TestApplicationResponse.kt:87) 30 | at io.ktor.utils.io.CoroutinesKt$launchChannel$job$1.invokeSuspend(Coroutines.kt:134) 31 | 32 | 33 | "ws-writer", state: SUSPENDED 34 | at io.ktor.websocket.WebSocketWriter.writeLoop(WebSocketWriter.kt:46) 35 | at io.ktor.websocket.WebSocketWriter$writeLoopJob$1.invokeSuspend(WebSocketWriter.kt:40) 36 | 37 | 38 | "ws-reader", state: SUSPENDED 39 | at io.ktor.utils.io.ByteBufferChannel.readSuspendImpl(ByteBufferChannel.kt:2230) 40 | at io.ktor.utils.io.ByteBufferChannel.readAvailableSuspend(ByteBufferChannel.kt:731) 41 | at io.ktor.websocket.WebSocketReader.readLoop(WebSocketReader.kt:68) 42 | at io.ktor.websocket.WebSocketReader$readerJob$1.invokeSuspend(WebSocketReader.kt:40) 43 | 44 | 45 | "raw-ws", state: SUSPENDED 46 | at io.ktor.websocket.RawWebSocketJvm$1.invokeSuspend(RawWebSocketJvm.kt:67) 47 | 48 | 49 | "raw-ws-handler", state: SUSPENDED 50 | at io.ktor.samples.chat.backend.ChatApplication$main$4$1.invokeSuspend(ChatApplication.kt:185) 51 | at io.ktor.server.websocket.RoutingKt.handleServerSession(Routing.kt:253) 52 | at io.ktor.server.websocket.RoutingKt.proceedWebSocket(Routing.kt:238) 53 | at io.ktor.server.websocket.RoutingKt$webSocket$2.invokeSuspend(Routing.kt:202) 54 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1$1.invokeSuspend(Routing.kt:106) 55 | at io.ktor.server.websocket.WebSocketUpgrade$upgrade$2.invokeSuspend(WebSocketUpgrade.kt:98) 56 | 57 | 58 | "ws-writer", state: SUSPENDED 59 | at io.ktor.websocket.WebSocketWriter.writeLoop(WebSocketWriter.kt:46) 60 | at io.ktor.websocket.WebSocketWriter$writeLoopJob$1.invokeSuspend(WebSocketWriter.kt:40) 61 | 62 | 63 | "ws-reader", state: SUSPENDED 64 | at io.ktor.utils.io.ByteBufferChannel.readSuspendImpl(ByteBufferChannel.kt:2230) 65 | at io.ktor.utils.io.ByteBufferChannel.readAvailableSuspend(ByteBufferChannel.kt:731) 66 | at io.ktor.websocket.WebSocketReader.readLoop(WebSocketReader.kt:68) 67 | at io.ktor.websocket.WebSocketReader$readerJob$1.invokeSuspend(WebSocketReader.kt:40) 68 | 69 | 70 | "coroutine", state: SUSPENDED 71 | at io.ktor.server.testing.client.TestHttpClientEngineBridge$runWebSocketRequest$call$2.invokeSuspend(TestHttpClientEngineBridgeJvm.kt:40) 72 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$5$1.invokeSuspend(TestApplicationEngineJvm.kt:88) 73 | 74 | 75 | "ws-pinger", state: SUSPENDED 76 | at io.ktor.websocket.PingPongKt$pinger$1$1.invokeSuspend(PingPong.kt:66) 77 | at kotlinx.coroutines.TimeoutKt.withTimeoutOrNull(Timeout.kt:100) 78 | at io.ktor.websocket.PingPongKt$pinger$1.invokeSuspend(PingPong.kt:64) 79 | 80 | 81 | "ws-ponger", state: SUSPENDED 82 | at io.ktor.websocket.PingPongKt$ponger$1.invokeSuspend(PingPong.kt:119) 83 | 84 | 85 | "ws-incoming-processor", state: SUSPENDED 86 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runIncomingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:345) 87 | 88 | 89 | "ws-outgoing-processor", state: SUSPENDED 90 | at io.ktor.websocket.DefaultWebSocketSessionImpl.outgoingProcessorLoop(DefaultWebSocketSession.kt:245) 91 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runOutgoingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:229) 92 | 93 | 94 | "ws-ponger", state: SUSPENDED 95 | at io.ktor.websocket.PingPongKt$ponger$1.invokeSuspend(PingPong.kt:119) 96 | 97 | 98 | "ws-incoming-processor", state: SUSPENDED 99 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runIncomingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:345) 100 | 101 | 102 | "ws-outgoing-processor", state: SUSPENDED 103 | at io.ktor.websocket.DefaultWebSocketSessionImpl.outgoingProcessorLoop(DefaultWebSocketSession.kt:245) 104 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runOutgoingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:229) 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/testData/dumps/twoChildren.txt: -------------------------------------------------------------------------------- 1 | "coroutine", state: SUSPENDED 2 | at MainKt$main$1.invokeSuspend(Main.kt:9) 3 | 4 | 5 | "coroutine", state: SUSPENDED 6 | at MainKt$thread1$2.invokeSuspend(Main.kt:23) 7 | at MainKt$main$1$job1$1.invokeSuspend(Main.kt:7) 8 | 9 | 10 | "coroutine", state: SUSPENDED 11 | at MainKt$childThread$2.invokeSuspend(Main.kt:33) 12 | at MainKt$thread1$2$childJob$1.invokeSuspend(Main.kt:19) 13 | 14 | 15 | "coroutine", state: SUSPENDED 16 | at MainKt$childThread$2.invokeSuspend(Main.kt:31) 17 | at MainKt$thread1$2$childJob$1.invokeSuspend(Main.kt:19) 18 | 19 | 20 | "coroutine", state: SUSPENDED 21 | at MainKt$childThread$2.invokeSuspend(Main.kt:33) 22 | at MainKt$thread1$2$childJob$1.invokeSuspend(Main.kt:19) 23 | 24 | 25 | "coroutine", state: RUNNING 26 | at MainKt.grandchildThread(Main.kt:39) 27 | at MainKt$childThread$2$grandchildJob$1.invokeSuspend(Main.kt:30) 28 | -------------------------------------------------------------------------------- /src/testData/dumps/twoChildrenChat.txt: -------------------------------------------------------------------------------- 1 | "coroutine", state: RUNNING 2 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1.invokeSuspend(ChatApplicationTest.kt:108) 3 | at io.ktor.server.testing.TestApplicationKt$testApplication$builder$1$1.invokeSuspend(TestApplication.kt:335) 4 | 5 | 6 | "nonce-generator", state: SUSPENDED 7 | at io.ktor.util.NonceKt$nonceGeneratorJob$1.invokeSuspend(Nonce.kt:76) 8 | 9 | 10 | "coroutine", state: SUSPENDED 11 | at io.ktor.server.engine.BaseApplicationEngine$3.invokeSuspend(BaseApplicationEngine.kt:75) 12 | 13 | 14 | "coroutine", state: SUSPENDED 15 | at io.ktor.server.engine.EngineContextCancellationHelperKt$launchOnCancellation$1.invokeSuspend(EngineContextCancellationHelper.kt:37) 16 | 17 | 18 | "coroutine", state: RUNNING 19 | at java.lang.ClassLoader$NativeLibrary.load0(ClassLoader.java:-2) 20 | at java.lang.ClassLoader$NativeLibrary.load(ClassLoader.java:2445) 21 | at java.lang.ClassLoader$NativeLibrary.loadLibrary(ClassLoader.java:2501) 22 | at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:2700) 23 | at java.lang.ClassLoader.loadLibrary(ClassLoader.java:2630) 24 | at java.lang.Runtime.load0(Runtime.java:768) 25 | at java.lang.System.load(System.java:1837) 26 | at org.fusesource.jansi.internal.JansiLoader.loadNativeLibrary(JansiLoader.java:238) 27 | at org.fusesource.jansi.internal.JansiLoader.extractAndLoadLibraryFile(JansiLoader.java:206) 28 | at org.fusesource.jansi.internal.JansiLoader.loadJansiNativeLibrary(JansiLoader.java:312) 29 | at org.fusesource.jansi.internal.JansiLoader.initialize(JansiLoader.java:62) 30 | at org.fusesource.jansi.internal.CLibrary.(CLibrary.java:36) 31 | at org.fusesource.jansi.AnsiConsole.ansiStream(AnsiConsole.java:255) 32 | at org.fusesource.jansi.AnsiConsole.initStreams(AnsiConsole.java:559) 33 | at org.fusesource.jansi.AnsiConsole.systemInstall(AnsiConsole.java:513) 34 | at io.ktor.server.plugins.callloging.CallLoggingConfig.colored(CallLoggingConfig.kt:112) 35 | at io.ktor.server.plugins.callloging.CallLoggingConfig.defaultFormat(CallLoggingConfig.kt:103) 36 | at io.ktor.server.plugins.callloging.CallLoggingConfig.access$defaultFormat(CallLoggingConfig.kt:19) 37 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 38 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 39 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.invoke$logSuccess(CallLogging.kt:54) 40 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.access$invoke$logSuccess(CallLogging.kt:32) 41 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 42 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 43 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invokeSuspend(CallLogging.kt:74) 44 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 45 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 46 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invokeSuspend(MDCHook.kt:29) 47 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 48 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 49 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 50 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 51 | at io.ktor.util.pipeline.DebugPipelineContext.proceedWith(DebugPipelineContext.kt:42) 52 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invokeSuspend(DefaultTransform.kt:29) 53 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 54 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 55 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 56 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 57 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 58 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 59 | at io.ktor.server.websocket.RoutingKt.respondWebSocketRaw(Routing.kt:293) 60 | at io.ktor.server.websocket.RoutingKt.access$respondWebSocketRaw(Routing.kt:1) 61 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invokeSuspend(Routing.kt:105) 62 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 63 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 64 | at io.ktor.server.routing.Route$buildPipeline$1$1.invokeSuspend(Route.kt:116) 65 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 66 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 67 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 68 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 69 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 70 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 71 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 72 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 73 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 74 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 75 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 76 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 77 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 78 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 79 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 80 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 81 | at io.ktor.server.routing.Routing.executeResult(Routing.kt:190) 82 | at io.ktor.server.routing.Routing.interceptor(Routing.kt:64) 83 | at io.ktor.server.routing.Routing$Plugin$install$1.invokeSuspend(Routing.kt:140) 84 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 85 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 86 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 87 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 88 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invokeSuspend(BaseApplicationEngine.kt:124) 89 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 90 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 91 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 92 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 93 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 94 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 95 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 96 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 97 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 98 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 99 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 100 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 101 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 102 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 103 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 104 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 105 | at io.ktor.server.testing.TestApplicationEngine$3.invokeSuspend(TestApplicationEngine.kt:308) 106 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 107 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 108 | at io.ktor.server.testing.TestApplicationEngine$2.invokeSuspend(TestApplicationEngine.kt:90) 109 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 110 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 111 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 112 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 113 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 114 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 115 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 116 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 117 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 118 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 119 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 120 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 121 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 122 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 123 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 124 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 125 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4.invokeSuspend(TestApplicationEngineJvm.kt:100) 126 | 127 | 128 | "coroutine", state: RUNNING 129 | at org.fusesource.jansi.AnsiConsole.isInstalled(AnsiConsole.java:529) 130 | at io.ktor.server.plugins.callloging.CallLoggingConfig.colored(CallLoggingConfig.kt:111) 131 | at io.ktor.server.plugins.callloging.CallLoggingConfig.defaultFormat(CallLoggingConfig.kt:103) 132 | at io.ktor.server.plugins.callloging.CallLoggingConfig.access$defaultFormat(CallLoggingConfig.kt:19) 133 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 134 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 135 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.invoke$logSuccess(CallLogging.kt:54) 136 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.access$invoke$logSuccess(CallLogging.kt:32) 137 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 138 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 139 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invokeSuspend(CallLogging.kt:74) 140 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 141 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 142 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invokeSuspend(MDCHook.kt:29) 143 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 144 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 145 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 146 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 147 | at io.ktor.util.pipeline.DebugPipelineContext.proceedWith(DebugPipelineContext.kt:42) 148 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invokeSuspend(DefaultTransform.kt:29) 149 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 150 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 151 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 152 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 153 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 154 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 155 | at io.ktor.server.websocket.RoutingKt.respondWebSocketRaw(Routing.kt:293) 156 | at io.ktor.server.websocket.RoutingKt.access$respondWebSocketRaw(Routing.kt:1) 157 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invokeSuspend(Routing.kt:105) 158 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 159 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 160 | at io.ktor.server.routing.Route$buildPipeline$1$1.invokeSuspend(Route.kt:116) 161 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 162 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 163 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 164 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 165 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 166 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 167 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 168 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 169 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 170 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 171 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 172 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 173 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 174 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 175 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 176 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 177 | at io.ktor.server.routing.Routing.executeResult(Routing.kt:190) 178 | at io.ktor.server.routing.Routing.interceptor(Routing.kt:64) 179 | at io.ktor.server.routing.Routing$Plugin$install$1.invokeSuspend(Routing.kt:140) 180 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 181 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 182 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 183 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 184 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invokeSuspend(BaseApplicationEngine.kt:124) 185 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 186 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 187 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 188 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 189 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 190 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 191 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 192 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 193 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 194 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 195 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 196 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 197 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 198 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 199 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 200 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 201 | at io.ktor.server.testing.TestApplicationEngine$3.invokeSuspend(TestApplicationEngine.kt:308) 202 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 203 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 204 | at io.ktor.server.testing.TestApplicationEngine$2.invokeSuspend(TestApplicationEngine.kt:90) 205 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 206 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 207 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 208 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 209 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 210 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 211 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 212 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 213 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 214 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 215 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 216 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 217 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 218 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 219 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 220 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 221 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4.invokeSuspend(TestApplicationEngineJvm.kt:100) 222 | 223 | 224 | -------------------------------------------------------------------------------- /src/testData/outs/noChildren.txt: -------------------------------------------------------------------------------- 1 | 1 Coroutine RUNNING, 2 | at io.ktor.server.testing.TestApplicationKt$testApplication$builder$1$1.invokeSuspend(TestApplication.kt:335) 3 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1.invokeSuspend(ChatApplicationTest.kt:72) 4 | at io.ktor.client.plugins.websocket.BuildersKt.webSocket(builders.kt:101) 5 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invoke(ChatApplicationTest.kt:-1) 6 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invoke(ChatApplicationTest.kt:-1) 7 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1$1.invokeSuspend(ChatApplicationTest.kt:75) 8 | 1 Coroutine SUSPENDED, 9 | at io.ktor.util.NonceKt$nonceGeneratorJob$1.invokeSuspend(Nonce.kt:76) 10 | 1 Coroutine SUSPENDED, 11 | at io.ktor.server.engine.BaseApplicationEngine$3.invokeSuspend(BaseApplicationEngine.kt:75) 12 | 1 Coroutine SUSPENDED, 13 | at io.ktor.server.engine.EngineContextCancellationHelperKt$launchOnCancellation$1.invokeSuspend(EngineContextCancellationHelper.kt:37) 14 | 1 Coroutine SUSPENDED, 15 | at io.ktor.utils.io.CoroutinesKt$launchChannel$job$1.invokeSuspend(Coroutines.kt:134) 16 | at io.ktor.server.testing.TestApplicationResponse$responseChannel$job$1.invokeSuspend(TestApplicationResponse.kt:87) 17 | at io.ktor.utils.io.ByteReadChannelKt.copyAndClose(ByteReadChannel.kt:255) 18 | at io.ktor.utils.io.ByteBufferChannel.copyDirect$ktor_io(ByteBufferChannel.kt:1265) 19 | at io.ktor.utils.io.ByteBufferChannel.readSuspendImpl(ByteBufferChannel.kt:2230) 20 | 2 Coroutines SUSPENDED, SUSPENDED, 21 | at io.ktor.websocket.WebSocketWriter$writeLoopJob$1.invokeSuspend(WebSocketWriter.kt:40) 22 | at io.ktor.websocket.WebSocketWriter.writeLoop(WebSocketWriter.kt:46) 23 | 2 Coroutines SUSPENDED, SUSPENDED, 24 | at io.ktor.websocket.WebSocketReader$readerJob$1.invokeSuspend(WebSocketReader.kt:40) 25 | at io.ktor.websocket.WebSocketReader.readLoop(WebSocketReader.kt:68) 26 | at io.ktor.utils.io.ByteBufferChannel.readAvailableSuspend(ByteBufferChannel.kt:731) 27 | at io.ktor.utils.io.ByteBufferChannel.readSuspendImpl(ByteBufferChannel.kt:2230) 28 | 1 Coroutine SUSPENDED, 29 | at io.ktor.websocket.RawWebSocketJvm$1.invokeSuspend(RawWebSocketJvm.kt:67) 30 | 1 Coroutine SUSPENDED, 31 | at io.ktor.server.websocket.WebSocketUpgrade$upgrade$2.invokeSuspend(WebSocketUpgrade.kt:98) 32 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1$1.invokeSuspend(Routing.kt:106) 33 | at io.ktor.server.websocket.RoutingKt$webSocket$2.invokeSuspend(Routing.kt:202) 34 | at io.ktor.server.websocket.RoutingKt.proceedWebSocket(Routing.kt:238) 35 | at io.ktor.server.websocket.RoutingKt.handleServerSession(Routing.kt:253) 36 | at io.ktor.samples.chat.backend.ChatApplication$main$4$1.invokeSuspend(ChatApplication.kt:185) 37 | 1 Coroutine SUSPENDED, 38 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$5$1.invokeSuspend(TestApplicationEngineJvm.kt:88) 39 | at io.ktor.server.testing.client.TestHttpClientEngineBridge$runWebSocketRequest$call$2.invokeSuspend(TestHttpClientEngineBridgeJvm.kt:40) 40 | 1 Coroutine SUSPENDED, 41 | at io.ktor.websocket.PingPongKt$pinger$1.invokeSuspend(PingPong.kt:64) 42 | at kotlinx.coroutines.TimeoutKt.withTimeoutOrNull(Timeout.kt:100) 43 | at io.ktor.websocket.PingPongKt$pinger$1$1.invokeSuspend(PingPong.kt:66) 44 | 2 Coroutines SUSPENDED, SUSPENDED, 45 | at io.ktor.websocket.PingPongKt$ponger$1.invokeSuspend(PingPong.kt:119) 46 | 2 Coroutines SUSPENDED, SUSPENDED, 47 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runIncomingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:345) 48 | 2 Coroutines SUSPENDED, SUSPENDED, 49 | at io.ktor.websocket.DefaultWebSocketSessionImpl$runOutgoingProcessor$1.invokeSuspend(DefaultWebSocketSession.kt:229) 50 | at io.ktor.websocket.DefaultWebSocketSessionImpl.outgoingProcessorLoop(DefaultWebSocketSession.kt:245) 51 | -------------------------------------------------------------------------------- /src/testData/outs/twoChildren.txt: -------------------------------------------------------------------------------- 1 | 1 Coroutine SUSPENDED, 2 | at MainKt$main$1.invokeSuspend(Main.kt:9) 3 | 1 Coroutine SUSPENDED, 4 | at MainKt$main$1$job1$1.invokeSuspend(Main.kt:7) 5 | at MainKt$thread1$2.invokeSuspend(Main.kt:23) 6 | 3 Coroutines SUSPENDED, SUSPENDED, SUSPENDED, 7 | at MainKt$thread1$2$childJob$1.invokeSuspend(Main.kt:19) 8 | 2 Coroutines SUSPENDED, SUSPENDED, 9 | at MainKt$childThread$2.invokeSuspend(Main.kt:33) 10 | 1 Coroutine SUSPENDED, 11 | at MainKt$childThread$2.invokeSuspend(Main.kt:31) 12 | 1 Coroutine RUNNING, 13 | at MainKt$childThread$2$grandchildJob$1.invokeSuspend(Main.kt:30) 14 | at MainKt.grandchildThread(Main.kt:39) 15 | -------------------------------------------------------------------------------- /src/testData/outs/twoChildrenChat.txt: -------------------------------------------------------------------------------- 1 | 1 Coroutine RUNNING, 2 | at io.ktor.server.testing.TestApplicationKt$testApplication$builder$1$1.invokeSuspend(TestApplication.kt:335) 3 | at io.ktor.samples.chat.backend.ChatApplicationTest$testDualConversation$1.invokeSuspend(ChatApplicationTest.kt:108) 4 | 1 Coroutine SUSPENDED, 5 | at io.ktor.util.NonceKt$nonceGeneratorJob$1.invokeSuspend(Nonce.kt:76) 6 | 1 Coroutine SUSPENDED, 7 | at io.ktor.server.engine.BaseApplicationEngine$3.invokeSuspend(BaseApplicationEngine.kt:75) 8 | 1 Coroutine SUSPENDED, 9 | at io.ktor.server.engine.EngineContextCancellationHelperKt$launchOnCancellation$1.invokeSuspend(EngineContextCancellationHelper.kt:37) 10 | 2 Coroutines RUNNING, RUNNING, 11 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4.invokeSuspend(TestApplicationEngineJvm.kt:100) 12 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 13 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 14 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 15 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 16 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 17 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 18 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 19 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 20 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 21 | at io.ktor.server.testing.TestApplicationEngineJvmKt$handleWebSocketConversationNonBlocking$4$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 22 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 23 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 24 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 25 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 26 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 27 | at io.ktor.server.testing.TestApplicationEngine$2.invoke(TestApplicationEngine.kt:-1) 28 | at io.ktor.server.testing.TestApplicationEngine$2.invokeSuspend(TestApplicationEngine.kt:90) 29 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 30 | at io.ktor.server.testing.TestApplicationEngine$3.invoke(TestApplicationEngine.kt:-1) 31 | at io.ktor.server.testing.TestApplicationEngine$3.invokeSuspend(TestApplicationEngine.kt:308) 32 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 33 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 34 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 35 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 36 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 37 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 38 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 39 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 40 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt:-1) 41 | at io.ktor.server.testing.TestApplicationEngine$3$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 42 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 43 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 44 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 45 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 46 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 47 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt:-1) 48 | at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invokeSuspend(BaseApplicationEngine.kt:124) 49 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 50 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 51 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 52 | at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt:-1) 53 | at io.ktor.server.routing.Routing$Plugin$install$1.invokeSuspend(Routing.kt:140) 54 | at io.ktor.server.routing.Routing.interceptor(Routing.kt:64) 55 | at io.ktor.server.routing.Routing.executeResult(Routing.kt:190) 56 | at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:20) 57 | at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1) 58 | at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169) 59 | at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89) 60 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 61 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invoke(ContextUtils.kt:-1) 62 | at io.ktor.util.debug.ContextUtilsKt$initContextInDebugMode$2.invokeSuspend(ContextUtils.kt:20) 63 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 64 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt:-1) 65 | at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invokeSuspend(Pipeline.kt:478) 66 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 67 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 68 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 69 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 70 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 71 | at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt:-1) 72 | at io.ktor.server.routing.Route$buildPipeline$1$1.invokeSuspend(Route.kt:116) 73 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 74 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invoke(Routing.kt:-1) 75 | at io.ktor.server.websocket.RoutingKt$webSocketRaw$2$1$1$1.invokeSuspend(Routing.kt:105) 76 | at io.ktor.server.websocket.RoutingKt.access$respondWebSocketRaw(Routing.kt:1) 77 | at io.ktor.server.websocket.RoutingKt.respondWebSocketRaw(Routing.kt:293) 78 | at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77) 79 | at io.ktor.util.pipeline.DebugPipelineContext.execute$ktor_utils(DebugPipelineContext.kt:63) 80 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 81 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 82 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 83 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invoke(DefaultTransform.kt:-1) 84 | at io.ktor.server.engine.DefaultTransformKt$installDefaultTransformations$1.invokeSuspend(DefaultTransform.kt:29) 85 | at io.ktor.util.pipeline.DebugPipelineContext.proceedWith(DebugPipelineContext.kt:42) 86 | at io.ktor.util.pipeline.DebugPipelineContext.proceed(DebugPipelineContext.kt:57) 87 | at io.ktor.util.pipeline.DebugPipelineContext.proceedLoop(DebugPipelineContext.kt:80) 88 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 89 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invoke(MDCHook.kt:-1) 90 | at io.ktor.server.plugins.callloging.ResponseSent$install$1.invokeSuspend(MDCHook.kt:29) 91 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 92 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invoke(CallLogging.kt:-1) 93 | at io.ktor.server.plugins.callloging.CallLoggingKt$logCompletedCalls$1.invokeSuspend(CallLogging.kt:74) 94 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 95 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2$3.invoke(CallLogging.kt:65) 96 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.access$invoke$logSuccess(CallLogging.kt:32) 97 | at io.ktor.server.plugins.callloging.CallLoggingKt$CallLogging$2.invoke$logSuccess(CallLogging.kt:54) 98 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 99 | at io.ktor.server.plugins.callloging.CallLoggingConfig$formatCall$1.invoke(CallLoggingConfig.kt:24) 100 | at io.ktor.server.plugins.callloging.CallLoggingConfig.access$defaultFormat(CallLoggingConfig.kt:19) 101 | at io.ktor.server.plugins.callloging.CallLoggingConfig.defaultFormat(CallLoggingConfig.kt:103) 102 | 1 Coroutine RUNNING, 103 | at io.ktor.server.plugins.callloging.CallLoggingConfig.colored(CallLoggingConfig.kt:112) 104 | at org.fusesource.jansi.AnsiConsole.systemInstall(AnsiConsole.java:513) 105 | at org.fusesource.jansi.AnsiConsole.initStreams(AnsiConsole.java:559) 106 | at org.fusesource.jansi.AnsiConsole.ansiStream(AnsiConsole.java:255) 107 | at org.fusesource.jansi.internal.CLibrary.(CLibrary.java:36) 108 | at org.fusesource.jansi.internal.JansiLoader.initialize(JansiLoader.java:62) 109 | at org.fusesource.jansi.internal.JansiLoader.loadJansiNativeLibrary(JansiLoader.java:312) 110 | at org.fusesource.jansi.internal.JansiLoader.extractAndLoadLibraryFile(JansiLoader.java:206) 111 | at org.fusesource.jansi.internal.JansiLoader.loadNativeLibrary(JansiLoader.java:238) 112 | at java.lang.System.load(System.java:1837) 113 | at java.lang.Runtime.load0(Runtime.java:768) 114 | at java.lang.ClassLoader.loadLibrary(ClassLoader.java:2630) 115 | at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:2700) 116 | at java.lang.ClassLoader$NativeLibrary.loadLibrary(ClassLoader.java:2501) 117 | at java.lang.ClassLoader$NativeLibrary.load(ClassLoader.java:2445) 118 | at java.lang.ClassLoader$NativeLibrary.load0(ClassLoader.java:-2) 119 | 1 Coroutine RUNNING, 120 | at io.ktor.server.plugins.callloging.CallLoggingConfig.colored(CallLoggingConfig.kt:111) 121 | at org.fusesource.jansi.AnsiConsole.isInstalled(AnsiConsole.java:529) 122 | -------------------------------------------------------------------------------- /src/testData/parse_dumps.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import pygtrie as trie 17 | from dataclasses import dataclass 18 | 19 | DUMP_DIR = "dumps/" 20 | OUT_DIR = "outs/" 21 | 22 | @dataclass 23 | class Coroutine: 24 | state: str 25 | stack_trace: list 26 | 27 | def parse_dump(file_name: str) -> list[Coroutine]: 28 | coroutines = [] 29 | with open(file_name, "r") as f: 30 | current_coroutine = None 31 | for line in f.readlines(): 32 | line = line.strip() 33 | if not line: 34 | continue 35 | elif line.startswith("\""): 36 | if current_coroutine: 37 | coroutines.append(current_coroutine) 38 | current_coroutine = Coroutine(line.split("state: ")[1], []) 39 | elif current_coroutine: 40 | current_coroutine.stack_trace.append(line) 41 | 42 | if current_coroutine: 43 | coroutines.append(current_coroutine) 44 | return coroutines 45 | 46 | def build_tree(coroutines: list[Coroutine]) -> str: 47 | t = trie.StringTrie() 48 | for i, coroutine in enumerate(coroutines): 49 | frames = [] 50 | for frame in reversed(coroutine.stack_trace): 51 | frames.append(f"{frame}\n") 52 | t.setdefault("".join(frames), []).append(i) 53 | 54 | stack: list[list[int]] = [] 55 | indents: list[str] = [] 56 | result: list[str] = [] 57 | 58 | def traverse_callback(path_conv, path, children, labels=[]): 59 | nonlocal stack 60 | nonlocal indents 61 | nonlocal result 62 | 63 | # Forsing the subtree traverse 64 | children = list(children) 65 | if not labels: 66 | return 67 | 68 | label_in_top = False 69 | while stack and not label_in_top: 70 | for label in labels: 71 | if label in stack[-1]: 72 | label_in_top = True 73 | break 74 | if not label_in_top: 75 | if indents: 76 | indents.pop() 77 | stack.pop() 78 | 79 | 80 | if stack and labels != stack[-1]: 81 | indents.append("\t") 82 | 83 | indentation = "".join(indents) 84 | if not stack or labels != stack[-1]: 85 | result.append(f"{indentation}{len(labels)} Coroutine") 86 | if len(labels) > 1: 87 | result.append("s") 88 | 89 | for label in labels: 90 | result.append(f" {coroutines[label].state},") 91 | result.append("\n") 92 | 93 | stack.append(labels) 94 | 95 | last_frame = path[-1].splitlines()[-1] 96 | result.append(f"{indentation}\t{last_frame}\n") 97 | 98 | t.traverse(traverse_callback) 99 | 100 | return "".join(result) 101 | 102 | def write_output(file_name: str, out: str): 103 | with open(file_name, "w") as f: 104 | f.write(out) 105 | 106 | def main(): 107 | for file_name in os.listdir(DUMP_DIR): 108 | coroutines = parse_dump(os.path.join(DUMP_DIR, file_name)) 109 | out = build_tree(coroutines) 110 | write_output(os.path.join(OUT_DIR, file_name), out) 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /src/testData/requirements.txt: -------------------------------------------------------------------------------- 1 | pygtrie==2.5.0 2 | --------------------------------------------------------------------------------