├── .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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------