├── .github ├── csvlens.data ├── demo-live.gif ├── demo.gif ├── demo.tape ├── make_demo.sh ├── top_view.gif ├── top_view.tape └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── dist-workspace.toml ├── src ├── app.rs ├── event.rs ├── flame.rs ├── handler.rs ├── lib.rs ├── main.rs ├── py_spy.rs ├── py_spy_flamegraph.rs ├── state.rs ├── tui.rs ├── ui.rs └── view.rs └── tests ├── data ├── ignore-metadata-lines.txt ├── invalid-lines.txt ├── py-spy-simple.txt ├── readable.txt └── recursive.txt ├── fixtures ├── ignore-metadata-lines │ ├── expected_ordered_counts.json │ └── expected_stacks.json ├── invalid-lines │ ├── expected_ordered_counts.json │ └── expected_stacks.json ├── py-spy-simple │ ├── expected_ordered_counts.json │ └── expected_stacks.json └── recursive │ ├── expected_ordered_counts.json │ └── expected_stacks.json └── python └── long_running.py /.github/demo-live.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YS-L/flamelens/0b4dc3331422ffca061be48c2e8b6f4057e48152/.github/demo-live.gif -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YS-L/flamelens/0b4dc3331422ffca061be48c2e8b6f4057e48152/.github/demo.gif -------------------------------------------------------------------------------- /.github/demo.tape: -------------------------------------------------------------------------------- 1 | Output "demo.gif" 2 | Set Theme "Tomorrow Night" 3 | Set Width 2400 4 | Set Height 1200 5 | Set Framerate 60 6 | Set PlaybackSpeed 0.5 # Make output 2 times slower 7 | 8 | Type "flamelens csvlens.data" 9 | Sleep 0.5s 10 | Enter 11 | Sleep 2s 12 | 13 | Down @0.5s 9 14 | Sleep 2s 15 | 16 | Type @2s "/" 17 | Type @0.2s "read_record" 18 | Enter 19 | Sleep 5s 20 | 21 | Enter 22 | Sleep 5s 23 | 24 | Tab 25 | Sleep 2s 26 | 27 | Type "r" 28 | Sleep 5s 29 | 30 | Type "q" 31 | Sleep 1s 32 | -------------------------------------------------------------------------------- /.github/make_demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | vhs demo.tape 4 | ls -lah demo.gif -------------------------------------------------------------------------------- /.github/top_view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YS-L/flamelens/0b4dc3331422ffca061be48c2e8b6f4057e48152/.github/top_view.gif -------------------------------------------------------------------------------- /.github/top_view.tape: -------------------------------------------------------------------------------- 1 | Output "top_view.gif" 2 | Set Theme "Tomorrow Night" 3 | Set Width 2400 4 | Set Height 1200 5 | Set Framerate 60 6 | Set PlaybackSpeed 0.5 # Make output 2 times slower 7 | 8 | Hide 9 | Type "flamelens csvlens.data" 10 | Sleep 0.5s 11 | Enter 12 | Sleep 2s 13 | Show 14 | 15 | Sleep 3s 16 | Tab 17 | Sleep 3s -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Lint 17 | run: make lint -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-20.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-20.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | announce: 278 | needs: 279 | - plan 280 | - host 281 | # use "always() && ..." to allow us to wait for all publish jobs while 282 | # still allowing individual publish jobs to skip themselves (for prereleases). 283 | # "host" however must run to completion, no skipping allowed! 284 | if: ${{ always() && needs.host.result == 'success' }} 285 | runs-on: "ubuntu-20.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | submodules: recursive 292 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Install dependencies 17 | run: sudo apt-get update && sudo apt-get install -y libunwind-dev 18 | - name: Build 19 | run: cargo build --verbose --all-features 20 | - name: Test 21 | run: cargo test --verbose --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.3.1 2 | 3 | * Fix colors for light theme terminals 4 | * Fix sort column being reset incorrectly in live mode 5 | 6 | # v0.3.0 7 | 8 | * Add "top" view to show summary of slowest functions in a table: 9 | 10 | ![top_view](.github/top_view.gif) 11 | 12 | * Add `--echo` option to allow integration with `cargo flamegraph` as the output viewer: 13 | 14 | ``` 15 | cargo flamegraph --post-process 'flamelens --echo' [other cargo flamegraph arguments] 16 | ``` 17 | 18 | * Display key bindings in app 19 | 20 | # v0.2.0 21 | 22 | * Support selecting next and previous search results (`n` / `N`) 23 | * Make footer section mutli-line and color coded for better readability 24 | * Display an indicator if there are more frames to show by scrolling down 25 | 26 | # v0.1.0 27 | 28 | Initial release -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "cpp_demangle", 12 | "fallible-iterator", 13 | "gimli 0.28.1", 14 | "memmap2 0.5.10", 15 | "object 0.32.2", 16 | "rustc-demangle", 17 | "smallvec", 18 | ] 19 | 20 | [[package]] 21 | name = "addr2line" 22 | version = "0.24.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 25 | dependencies = [ 26 | "cpp_demangle", 27 | "fallible-iterator", 28 | "gimli 0.31.1", 29 | "memmap2 0.9.5", 30 | "object 0.36.7", 31 | "rustc-demangle", 32 | "smallvec", 33 | "typed-arena", 34 | ] 35 | 36 | [[package]] 37 | name = "adler" 38 | version = "1.0.2" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 41 | 42 | [[package]] 43 | name = "ahash" 44 | version = "0.8.11" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 47 | dependencies = [ 48 | "cfg-if", 49 | "getrandom", 50 | "once_cell", 51 | "version_check", 52 | "zerocopy", 53 | ] 54 | 55 | [[package]] 56 | name = "aho-corasick" 57 | version = "1.1.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 60 | dependencies = [ 61 | "memchr", 62 | ] 63 | 64 | [[package]] 65 | name = "allocator-api2" 66 | version = "0.2.18" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 69 | 70 | [[package]] 71 | name = "android-tzdata" 72 | version = "0.1.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 75 | 76 | [[package]] 77 | name = "android_system_properties" 78 | version = "0.1.5" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 81 | dependencies = [ 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "anstream" 87 | version = "0.6.14" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 90 | dependencies = [ 91 | "anstyle", 92 | "anstyle-parse", 93 | "anstyle-query", 94 | "anstyle-wincon", 95 | "colorchoice", 96 | "is_terminal_polyfill", 97 | "utf8parse", 98 | ] 99 | 100 | [[package]] 101 | name = "anstyle" 102 | version = "1.0.7" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 105 | 106 | [[package]] 107 | name = "anstyle-parse" 108 | version = "0.2.4" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 111 | dependencies = [ 112 | "utf8parse", 113 | ] 114 | 115 | [[package]] 116 | name = "anstyle-query" 117 | version = "1.0.3" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 120 | dependencies = [ 121 | "windows-sys 0.52.0", 122 | ] 123 | 124 | [[package]] 125 | name = "anstyle-wincon" 126 | version = "3.0.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 129 | dependencies = [ 130 | "anstyle", 131 | "windows-sys 0.52.0", 132 | ] 133 | 134 | [[package]] 135 | name = "anyhow" 136 | version = "1.0.86" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 139 | 140 | [[package]] 141 | name = "arrayvec" 142 | version = "0.7.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 145 | 146 | [[package]] 147 | name = "atty" 148 | version = "0.2.14" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 151 | dependencies = [ 152 | "hermit-abi 0.1.19", 153 | "libc", 154 | "winapi", 155 | ] 156 | 157 | [[package]] 158 | name = "autocfg" 159 | version = "1.3.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 162 | 163 | [[package]] 164 | name = "bindgen" 165 | version = "0.68.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" 168 | dependencies = [ 169 | "bitflags 2.5.0", 170 | "cexpr", 171 | "clang-sys", 172 | "lazy_static", 173 | "lazycell", 174 | "log", 175 | "peeking_take_while", 176 | "prettyplease", 177 | "proc-macro2", 178 | "quote", 179 | "regex", 180 | "rustc-hash", 181 | "shlex", 182 | "syn 2.0.98", 183 | "which", 184 | ] 185 | 186 | [[package]] 187 | name = "bindgen" 188 | version = "0.69.4" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 191 | dependencies = [ 192 | "bitflags 2.5.0", 193 | "cexpr", 194 | "clang-sys", 195 | "itertools 0.12.1", 196 | "lazy_static", 197 | "lazycell", 198 | "proc-macro2", 199 | "quote", 200 | "regex", 201 | "rustc-hash", 202 | "shlex", 203 | "syn 2.0.98", 204 | ] 205 | 206 | [[package]] 207 | name = "bindgen" 208 | version = "0.70.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 211 | dependencies = [ 212 | "bitflags 2.5.0", 213 | "cexpr", 214 | "clang-sys", 215 | "itertools 0.13.0", 216 | "log", 217 | "prettyplease", 218 | "proc-macro2", 219 | "quote", 220 | "regex", 221 | "rustc-hash", 222 | "shlex", 223 | "syn 2.0.98", 224 | ] 225 | 226 | [[package]] 227 | name = "bitflags" 228 | version = "1.3.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 231 | 232 | [[package]] 233 | name = "bitflags" 234 | version = "2.5.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 237 | 238 | [[package]] 239 | name = "bumpalo" 240 | version = "3.16.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 243 | 244 | [[package]] 245 | name = "bytemuck" 246 | version = "1.16.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" 249 | 250 | [[package]] 251 | name = "byteorder" 252 | version = "1.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 255 | 256 | [[package]] 257 | name = "cassowary" 258 | version = "0.3.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 261 | 262 | [[package]] 263 | name = "castaway" 264 | version = "0.2.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 267 | dependencies = [ 268 | "rustversion", 269 | ] 270 | 271 | [[package]] 272 | name = "cc" 273 | version = "1.0.98" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 276 | 277 | [[package]] 278 | name = "cexpr" 279 | version = "0.6.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 282 | dependencies = [ 283 | "nom", 284 | ] 285 | 286 | [[package]] 287 | name = "cfg-if" 288 | version = "1.0.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 291 | 292 | [[package]] 293 | name = "cfg_aliases" 294 | version = "0.1.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 297 | 298 | [[package]] 299 | name = "chrono" 300 | version = "0.4.38" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 303 | dependencies = [ 304 | "android-tzdata", 305 | "iana-time-zone", 306 | "js-sys", 307 | "num-traits", 308 | "wasm-bindgen", 309 | "windows-targets 0.52.5", 310 | ] 311 | 312 | [[package]] 313 | name = "clang-sys" 314 | version = "1.8.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 317 | dependencies = [ 318 | "glob", 319 | "libc", 320 | "libloading", 321 | ] 322 | 323 | [[package]] 324 | name = "clap" 325 | version = "3.2.25" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 328 | dependencies = [ 329 | "atty", 330 | "bitflags 1.3.2", 331 | "clap_derive 3.2.25", 332 | "clap_lex 0.2.4", 333 | "indexmap 1.9.3", 334 | "once_cell", 335 | "strsim 0.10.0", 336 | "termcolor", 337 | "terminal_size", 338 | "textwrap", 339 | ] 340 | 341 | [[package]] 342 | name = "clap" 343 | version = "4.5.4" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 346 | dependencies = [ 347 | "clap_builder", 348 | "clap_derive 4.5.4", 349 | ] 350 | 351 | [[package]] 352 | name = "clap_builder" 353 | version = "4.5.2" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 356 | dependencies = [ 357 | "anstream", 358 | "anstyle", 359 | "clap_lex 0.7.0", 360 | "strsim 0.11.1", 361 | ] 362 | 363 | [[package]] 364 | name = "clap_complete" 365 | version = "3.2.5" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" 368 | dependencies = [ 369 | "clap 3.2.25", 370 | ] 371 | 372 | [[package]] 373 | name = "clap_derive" 374 | version = "3.2.25" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" 377 | dependencies = [ 378 | "heck 0.4.1", 379 | "proc-macro-error", 380 | "proc-macro2", 381 | "quote", 382 | "syn 1.0.109", 383 | ] 384 | 385 | [[package]] 386 | name = "clap_derive" 387 | version = "4.5.4" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 390 | dependencies = [ 391 | "heck 0.5.0", 392 | "proc-macro2", 393 | "quote", 394 | "syn 2.0.98", 395 | ] 396 | 397 | [[package]] 398 | name = "clap_lex" 399 | version = "0.2.4" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 402 | dependencies = [ 403 | "os_str_bytes", 404 | ] 405 | 406 | [[package]] 407 | name = "clap_lex" 408 | version = "0.7.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 411 | 412 | [[package]] 413 | name = "colorchoice" 414 | version = "1.0.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 417 | 418 | [[package]] 419 | name = "compact_str" 420 | version = "0.8.1" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 423 | dependencies = [ 424 | "castaway", 425 | "cfg-if", 426 | "itoa", 427 | "rustversion", 428 | "ryu", 429 | "static_assertions", 430 | ] 431 | 432 | [[package]] 433 | name = "console" 434 | version = "0.15.8" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 437 | dependencies = [ 438 | "encode_unicode", 439 | "lazy_static", 440 | "libc", 441 | "unicode-width 0.1.12", 442 | "windows-sys 0.52.0", 443 | ] 444 | 445 | [[package]] 446 | name = "core-foundation-sys" 447 | version = "0.8.6" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 450 | 451 | [[package]] 452 | name = "cpp_demangle" 453 | version = "0.4.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119" 456 | dependencies = [ 457 | "cfg-if", 458 | ] 459 | 460 | [[package]] 461 | name = "crc32fast" 462 | version = "1.4.2" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 465 | dependencies = [ 466 | "cfg-if", 467 | ] 468 | 469 | [[package]] 470 | name = "crossbeam-channel" 471 | version = "0.5.13" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 474 | dependencies = [ 475 | "crossbeam-utils", 476 | ] 477 | 478 | [[package]] 479 | name = "crossbeam-utils" 480 | version = "0.8.20" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 483 | 484 | [[package]] 485 | name = "crossterm" 486 | version = "0.28.1" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 489 | dependencies = [ 490 | "bitflags 2.5.0", 491 | "crossterm_winapi", 492 | "filedescriptor", 493 | "mio", 494 | "parking_lot", 495 | "rustix 0.38.34", 496 | "signal-hook", 497 | "signal-hook-mio", 498 | "winapi", 499 | ] 500 | 501 | [[package]] 502 | name = "crossterm_winapi" 503 | version = "0.9.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 506 | dependencies = [ 507 | "winapi", 508 | ] 509 | 510 | [[package]] 511 | name = "ctrlc" 512 | version = "3.4.4" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" 515 | dependencies = [ 516 | "nix 0.28.0", 517 | "windows-sys 0.52.0", 518 | ] 519 | 520 | [[package]] 521 | name = "darling" 522 | version = "0.20.10" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 525 | dependencies = [ 526 | "darling_core", 527 | "darling_macro", 528 | ] 529 | 530 | [[package]] 531 | name = "darling_core" 532 | version = "0.20.10" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 535 | dependencies = [ 536 | "fnv", 537 | "ident_case", 538 | "proc-macro2", 539 | "quote", 540 | "strsim 0.11.1", 541 | "syn 2.0.98", 542 | ] 543 | 544 | [[package]] 545 | name = "darling_macro" 546 | version = "0.20.10" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 549 | dependencies = [ 550 | "darling_core", 551 | "quote", 552 | "syn 2.0.98", 553 | ] 554 | 555 | [[package]] 556 | name = "dashmap" 557 | version = "6.1.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 560 | dependencies = [ 561 | "cfg-if", 562 | "crossbeam-utils", 563 | "hashbrown 0.14.5", 564 | "lock_api", 565 | "once_cell", 566 | "parking_lot_core", 567 | ] 568 | 569 | [[package]] 570 | name = "derive_more" 571 | version = "0.99.17" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 574 | dependencies = [ 575 | "proc-macro2", 576 | "quote", 577 | "syn 1.0.109", 578 | ] 579 | 580 | [[package]] 581 | name = "either" 582 | version = "1.12.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 585 | 586 | [[package]] 587 | name = "encode_unicode" 588 | version = "0.3.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 591 | 592 | [[package]] 593 | name = "env_filter" 594 | version = "0.1.3" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 597 | dependencies = [ 598 | "log", 599 | ] 600 | 601 | [[package]] 602 | name = "env_logger" 603 | version = "0.10.2" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 606 | dependencies = [ 607 | "humantime", 608 | "is-terminal", 609 | "log", 610 | "regex", 611 | "termcolor", 612 | ] 613 | 614 | [[package]] 615 | name = "env_logger" 616 | version = "0.11.8" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 619 | dependencies = [ 620 | "env_filter", 621 | "log", 622 | ] 623 | 624 | [[package]] 625 | name = "equivalent" 626 | version = "1.0.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 629 | 630 | [[package]] 631 | name = "errno" 632 | version = "0.3.9" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 635 | dependencies = [ 636 | "libc", 637 | "windows-sys 0.52.0", 638 | ] 639 | 640 | [[package]] 641 | name = "fallible-iterator" 642 | version = "0.3.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 645 | 646 | [[package]] 647 | name = "fastrand" 648 | version = "2.1.0" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 651 | 652 | [[package]] 653 | name = "filedescriptor" 654 | version = "0.8.2" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" 657 | dependencies = [ 658 | "libc", 659 | "thiserror", 660 | "winapi", 661 | ] 662 | 663 | [[package]] 664 | name = "flamelens" 665 | version = "0.3.1" 666 | dependencies = [ 667 | "anyhow", 668 | "cfg-if", 669 | "clap 4.5.4", 670 | "crossterm", 671 | "py-spy", 672 | "ratatui", 673 | "regex", 674 | "remoteprocess 0.4.13", 675 | "serde", 676 | "serde_json", 677 | "tui-input", 678 | ] 679 | 680 | [[package]] 681 | name = "flate2" 682 | version = "1.0.30" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 685 | dependencies = [ 686 | "crc32fast", 687 | "miniz_oxide", 688 | ] 689 | 690 | [[package]] 691 | name = "fnv" 692 | version = "1.0.7" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 695 | 696 | [[package]] 697 | name = "getrandom" 698 | version = "0.2.15" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 701 | dependencies = [ 702 | "cfg-if", 703 | "libc", 704 | "wasi", 705 | ] 706 | 707 | [[package]] 708 | name = "gimli" 709 | version = "0.28.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 712 | dependencies = [ 713 | "fallible-iterator", 714 | "stable_deref_trait", 715 | ] 716 | 717 | [[package]] 718 | name = "gimli" 719 | version = "0.31.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 722 | dependencies = [ 723 | "fallible-iterator", 724 | "stable_deref_trait", 725 | ] 726 | 727 | [[package]] 728 | name = "glob" 729 | version = "0.3.1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 732 | 733 | [[package]] 734 | name = "goblin" 735 | version = "0.7.1" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134" 738 | dependencies = [ 739 | "log", 740 | "plain", 741 | "scroll 0.11.0", 742 | ] 743 | 744 | [[package]] 745 | name = "goblin" 746 | version = "0.9.3" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" 749 | dependencies = [ 750 | "log", 751 | "plain", 752 | "scroll 0.12.0", 753 | ] 754 | 755 | [[package]] 756 | name = "hashbrown" 757 | version = "0.12.3" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 760 | 761 | [[package]] 762 | name = "hashbrown" 763 | version = "0.13.2" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 766 | dependencies = [ 767 | "ahash", 768 | ] 769 | 770 | [[package]] 771 | name = "hashbrown" 772 | version = "0.14.5" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 775 | dependencies = [ 776 | "ahash", 777 | "allocator-api2", 778 | ] 779 | 780 | [[package]] 781 | name = "heck" 782 | version = "0.4.1" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 785 | 786 | [[package]] 787 | name = "heck" 788 | version = "0.5.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 791 | 792 | [[package]] 793 | name = "hermit-abi" 794 | version = "0.1.19" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 797 | dependencies = [ 798 | "libc", 799 | ] 800 | 801 | [[package]] 802 | name = "hermit-abi" 803 | version = "0.3.9" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 806 | 807 | [[package]] 808 | name = "home" 809 | version = "0.5.9" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 812 | dependencies = [ 813 | "windows-sys 0.52.0", 814 | ] 815 | 816 | [[package]] 817 | name = "humantime" 818 | version = "2.1.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 821 | 822 | [[package]] 823 | name = "iana-time-zone" 824 | version = "0.1.60" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 827 | dependencies = [ 828 | "android_system_properties", 829 | "core-foundation-sys", 830 | "iana-time-zone-haiku", 831 | "js-sys", 832 | "wasm-bindgen", 833 | "windows-core", 834 | ] 835 | 836 | [[package]] 837 | name = "iana-time-zone-haiku" 838 | version = "0.1.2" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 841 | dependencies = [ 842 | "cc", 843 | ] 844 | 845 | [[package]] 846 | name = "ident_case" 847 | version = "1.0.1" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 850 | 851 | [[package]] 852 | name = "indexmap" 853 | version = "1.9.3" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 856 | dependencies = [ 857 | "autocfg", 858 | "hashbrown 0.12.3", 859 | ] 860 | 861 | [[package]] 862 | name = "indexmap" 863 | version = "2.2.6" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 866 | dependencies = [ 867 | "equivalent", 868 | "hashbrown 0.14.5", 869 | ] 870 | 871 | [[package]] 872 | name = "indicatif" 873 | version = "0.17.11" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 876 | dependencies = [ 877 | "console", 878 | "number_prefix", 879 | "portable-atomic", 880 | "unicode-width 0.2.0", 881 | "web-time", 882 | ] 883 | 884 | [[package]] 885 | name = "indoc" 886 | version = "2.0.5" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 889 | 890 | [[package]] 891 | name = "inferno" 892 | version = "0.11.21" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" 895 | dependencies = [ 896 | "ahash", 897 | "clap 4.5.4", 898 | "crossbeam-channel", 899 | "crossbeam-utils", 900 | "dashmap", 901 | "env_logger 0.11.8", 902 | "indexmap 2.2.6", 903 | "is-terminal", 904 | "itoa", 905 | "log", 906 | "num-format", 907 | "once_cell", 908 | "quick-xml", 909 | "rgb", 910 | "str_stack", 911 | ] 912 | 913 | [[package]] 914 | name = "instability" 915 | version = "0.3.7" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 918 | dependencies = [ 919 | "darling", 920 | "indoc", 921 | "proc-macro2", 922 | "quote", 923 | "syn 2.0.98", 924 | ] 925 | 926 | [[package]] 927 | name = "io-lifetimes" 928 | version = "1.0.11" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 931 | dependencies = [ 932 | "hermit-abi 0.3.9", 933 | "libc", 934 | "windows-sys 0.48.0", 935 | ] 936 | 937 | [[package]] 938 | name = "is-terminal" 939 | version = "0.4.12" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 942 | dependencies = [ 943 | "hermit-abi 0.3.9", 944 | "libc", 945 | "windows-sys 0.52.0", 946 | ] 947 | 948 | [[package]] 949 | name = "is_terminal_polyfill" 950 | version = "1.70.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 953 | 954 | [[package]] 955 | name = "itertools" 956 | version = "0.12.1" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 959 | dependencies = [ 960 | "either", 961 | ] 962 | 963 | [[package]] 964 | name = "itertools" 965 | version = "0.13.0" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 968 | dependencies = [ 969 | "either", 970 | ] 971 | 972 | [[package]] 973 | name = "itoa" 974 | version = "1.0.11" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 977 | 978 | [[package]] 979 | name = "js-sys" 980 | version = "0.3.69" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 983 | dependencies = [ 984 | "wasm-bindgen", 985 | ] 986 | 987 | [[package]] 988 | name = "lazy_static" 989 | version = "1.5.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 992 | 993 | [[package]] 994 | name = "lazycell" 995 | version = "1.3.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 998 | 999 | [[package]] 1000 | name = "libc" 1001 | version = "0.2.169" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 1004 | 1005 | [[package]] 1006 | name = "libloading" 1007 | version = "0.8.3" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 1010 | dependencies = [ 1011 | "cfg-if", 1012 | "windows-targets 0.52.5", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "libm" 1017 | version = "0.2.8" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 1020 | 1021 | [[package]] 1022 | name = "libproc" 1023 | version = "0.14.8" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "ae9ea4b75e1a81675429dafe43441df1caea70081e82246a8cccf514884a88bb" 1026 | dependencies = [ 1027 | "bindgen 0.69.4", 1028 | "errno", 1029 | "libc", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "linux-raw-sys" 1034 | version = "0.3.8" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 1037 | 1038 | [[package]] 1039 | name = "linux-raw-sys" 1040 | version = "0.4.14" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1043 | 1044 | [[package]] 1045 | name = "lock_api" 1046 | version = "0.4.12" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1049 | dependencies = [ 1050 | "autocfg", 1051 | "scopeguard", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "log" 1056 | version = "0.4.21" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 1059 | 1060 | [[package]] 1061 | name = "lru" 1062 | version = "0.10.1" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" 1065 | dependencies = [ 1066 | "hashbrown 0.13.2", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "lru" 1071 | version = "0.12.3" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 1074 | dependencies = [ 1075 | "hashbrown 0.14.5", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "mach" 1080 | version = "0.3.2" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 1083 | dependencies = [ 1084 | "libc", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "mach2" 1089 | version = "0.4.2" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 1092 | dependencies = [ 1093 | "libc", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "mach_o_sys" 1098 | version = "0.1.1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "3e854583a83f20cf329bb9283366335387f7db59d640d1412167e05fedb98826" 1101 | 1102 | [[package]] 1103 | name = "memchr" 1104 | version = "2.7.2" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 1107 | 1108 | [[package]] 1109 | name = "memmap" 1110 | version = "0.7.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" 1113 | dependencies = [ 1114 | "libc", 1115 | "winapi", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "memmap2" 1120 | version = "0.5.10" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" 1123 | dependencies = [ 1124 | "libc", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "memmap2" 1129 | version = "0.9.5" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 1132 | dependencies = [ 1133 | "libc", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "minimal-lexical" 1138 | version = "0.2.1" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1141 | 1142 | [[package]] 1143 | name = "miniz_oxide" 1144 | version = "0.7.3" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 1147 | dependencies = [ 1148 | "adler", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "mio" 1153 | version = "1.0.3" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1156 | dependencies = [ 1157 | "libc", 1158 | "log", 1159 | "wasi", 1160 | "windows-sys 0.52.0", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "nix" 1165 | version = "0.26.4" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 1168 | dependencies = [ 1169 | "bitflags 1.3.2", 1170 | "cfg-if", 1171 | "libc", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "nix" 1176 | version = "0.28.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 1179 | dependencies = [ 1180 | "bitflags 2.5.0", 1181 | "cfg-if", 1182 | "cfg_aliases", 1183 | "libc", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "nom" 1188 | version = "7.1.3" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1191 | dependencies = [ 1192 | "memchr", 1193 | "minimal-lexical", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "num-format" 1198 | version = "0.4.4" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" 1201 | dependencies = [ 1202 | "arrayvec", 1203 | "itoa", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "num-traits" 1208 | version = "0.2.19" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1211 | dependencies = [ 1212 | "autocfg", 1213 | "libm", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "number_prefix" 1218 | version = "0.4.0" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 1221 | 1222 | [[package]] 1223 | name = "object" 1224 | version = "0.32.2" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 1227 | dependencies = [ 1228 | "flate2", 1229 | "memchr", 1230 | "ruzstd 0.5.0", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "object" 1235 | version = "0.36.7" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1238 | dependencies = [ 1239 | "flate2", 1240 | "memchr", 1241 | "ruzstd 0.7.3", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "once_cell" 1246 | version = "1.19.0" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 1249 | 1250 | [[package]] 1251 | name = "os_str_bytes" 1252 | version = "6.6.1" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 1255 | 1256 | [[package]] 1257 | name = "parking_lot" 1258 | version = "0.12.3" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1261 | dependencies = [ 1262 | "lock_api", 1263 | "parking_lot_core", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "parking_lot_core" 1268 | version = "0.9.10" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1271 | dependencies = [ 1272 | "cfg-if", 1273 | "libc", 1274 | "redox_syscall", 1275 | "smallvec", 1276 | "windows-targets 0.52.5", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "paste" 1281 | version = "1.0.15" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1284 | 1285 | [[package]] 1286 | name = "peeking_take_while" 1287 | version = "0.1.2" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 1290 | 1291 | [[package]] 1292 | name = "plain" 1293 | version = "0.2.3" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 1296 | 1297 | [[package]] 1298 | name = "portable-atomic" 1299 | version = "1.11.0" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1302 | 1303 | [[package]] 1304 | name = "ppv-lite86" 1305 | version = "0.2.17" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1308 | 1309 | [[package]] 1310 | name = "prettyplease" 1311 | version = "0.2.20" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" 1314 | dependencies = [ 1315 | "proc-macro2", 1316 | "syn 2.0.98", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "proc-macro-error" 1321 | version = "1.0.4" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1324 | dependencies = [ 1325 | "proc-macro-error-attr", 1326 | "proc-macro2", 1327 | "quote", 1328 | "syn 1.0.109", 1329 | "version_check", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "proc-macro-error-attr" 1334 | version = "1.0.4" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1337 | dependencies = [ 1338 | "proc-macro2", 1339 | "quote", 1340 | "version_check", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "proc-macro2" 1345 | version = "1.0.93" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1348 | dependencies = [ 1349 | "unicode-ident", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "proc-maps" 1354 | version = "0.3.2" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "0ec8fdc22cb95c02f6a26a91fb1cd60a7a115916c2ed3b09d0a312e11785bd57" 1357 | dependencies = [ 1358 | "anyhow", 1359 | "bindgen 0.68.1", 1360 | "libc", 1361 | "libproc", 1362 | "mach2", 1363 | "winapi", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "proc-maps" 1368 | version = "0.4.0" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "3db44c5aa60e193a25fcd93bb9ed27423827e8f118897866f946e2cf936c44fb" 1371 | dependencies = [ 1372 | "anyhow", 1373 | "bindgen 0.70.1", 1374 | "libc", 1375 | "libproc", 1376 | "mach2", 1377 | "winapi", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "py-spy" 1382 | version = "0.4.0" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "c4f4300b4ae8b012f43c3ec196a5c24dfb7921d293b3b47e6856617271ae6d3b" 1385 | dependencies = [ 1386 | "anyhow", 1387 | "chrono", 1388 | "clap 3.2.25", 1389 | "clap_complete", 1390 | "console", 1391 | "cpp_demangle", 1392 | "ctrlc", 1393 | "env_logger 0.10.2", 1394 | "goblin 0.9.3", 1395 | "indicatif", 1396 | "inferno", 1397 | "lazy_static", 1398 | "libc", 1399 | "log", 1400 | "lru 0.10.1", 1401 | "memmap2 0.9.5", 1402 | "num-traits", 1403 | "proc-maps 0.4.0", 1404 | "rand", 1405 | "rand_distr", 1406 | "regex", 1407 | "remoteprocess 0.5.0", 1408 | "serde", 1409 | "serde_derive", 1410 | "serde_json", 1411 | "tempfile", 1412 | "termios", 1413 | "winapi", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "quick-xml" 1418 | version = "0.26.0" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" 1421 | dependencies = [ 1422 | "memchr", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "quote" 1427 | version = "1.0.38" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1430 | dependencies = [ 1431 | "proc-macro2", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "rand" 1436 | version = "0.8.5" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1439 | dependencies = [ 1440 | "libc", 1441 | "rand_chacha", 1442 | "rand_core", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "rand_chacha" 1447 | version = "0.3.1" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1450 | dependencies = [ 1451 | "ppv-lite86", 1452 | "rand_core", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "rand_core" 1457 | version = "0.6.4" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1460 | dependencies = [ 1461 | "getrandom", 1462 | ] 1463 | 1464 | [[package]] 1465 | name = "rand_distr" 1466 | version = "0.4.3" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" 1469 | dependencies = [ 1470 | "num-traits", 1471 | "rand", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "ratatui" 1476 | version = "0.29.0" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 1479 | dependencies = [ 1480 | "bitflags 2.5.0", 1481 | "cassowary", 1482 | "compact_str", 1483 | "crossterm", 1484 | "indoc", 1485 | "instability", 1486 | "itertools 0.13.0", 1487 | "lru 0.12.3", 1488 | "paste", 1489 | "strum", 1490 | "unicode-segmentation", 1491 | "unicode-truncate", 1492 | "unicode-width 0.2.0", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "read-process-memory" 1497 | version = "0.1.6" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "8497683b2f0b6887786f1928c118f26ecc6bb3d78bbb6ed23e8e7ba110af3bb0" 1500 | dependencies = [ 1501 | "libc", 1502 | "log", 1503 | "mach", 1504 | "winapi", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "redox_syscall" 1509 | version = "0.5.1" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 1512 | dependencies = [ 1513 | "bitflags 2.5.0", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "regex" 1518 | version = "1.10.5" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 1521 | dependencies = [ 1522 | "aho-corasick", 1523 | "memchr", 1524 | "regex-automata", 1525 | "regex-syntax", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "regex-automata" 1530 | version = "0.4.6" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 1533 | dependencies = [ 1534 | "aho-corasick", 1535 | "memchr", 1536 | "regex-syntax", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "regex-syntax" 1541 | version = "0.8.3" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 1544 | 1545 | [[package]] 1546 | name = "remoteprocess" 1547 | version = "0.4.13" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "8928e7901c7fc6f1f8d697d77a9da0f749f96454714a8d097662531640647409" 1550 | dependencies = [ 1551 | "addr2line 0.21.0", 1552 | "goblin 0.7.1", 1553 | "lazy_static", 1554 | "libc", 1555 | "libproc", 1556 | "log", 1557 | "mach", 1558 | "mach_o_sys", 1559 | "memmap", 1560 | "nix 0.26.4", 1561 | "object 0.32.2", 1562 | "proc-maps 0.3.2", 1563 | "read-process-memory", 1564 | "regex", 1565 | "winapi", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "remoteprocess" 1570 | version = "0.5.0" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "e6194770c7afc1d2ca42acde19267938eb7d52ccb5b727f1a2eafa8d4d00ff20" 1573 | dependencies = [ 1574 | "addr2line 0.24.2", 1575 | "cfg-if", 1576 | "goblin 0.9.3", 1577 | "lazy_static", 1578 | "libc", 1579 | "libproc", 1580 | "log", 1581 | "mach", 1582 | "mach_o_sys", 1583 | "memmap2 0.9.5", 1584 | "nix 0.26.4", 1585 | "object 0.36.7", 1586 | "proc-maps 0.4.0", 1587 | "read-process-memory", 1588 | "regex", 1589 | "winapi", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "rgb" 1594 | version = "0.8.37" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" 1597 | dependencies = [ 1598 | "bytemuck", 1599 | ] 1600 | 1601 | [[package]] 1602 | name = "rustc-demangle" 1603 | version = "0.1.24" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1606 | 1607 | [[package]] 1608 | name = "rustc-hash" 1609 | version = "1.1.0" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1612 | 1613 | [[package]] 1614 | name = "rustix" 1615 | version = "0.37.27" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" 1618 | dependencies = [ 1619 | "bitflags 1.3.2", 1620 | "errno", 1621 | "io-lifetimes", 1622 | "libc", 1623 | "linux-raw-sys 0.3.8", 1624 | "windows-sys 0.48.0", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "rustix" 1629 | version = "0.38.34" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1632 | dependencies = [ 1633 | "bitflags 2.5.0", 1634 | "errno", 1635 | "libc", 1636 | "linux-raw-sys 0.4.14", 1637 | "windows-sys 0.52.0", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "rustversion" 1642 | version = "1.0.17" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 1645 | 1646 | [[package]] 1647 | name = "ruzstd" 1648 | version = "0.5.0" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" 1651 | dependencies = [ 1652 | "byteorder", 1653 | "derive_more", 1654 | "twox-hash", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "ruzstd" 1659 | version = "0.7.3" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" 1662 | dependencies = [ 1663 | "twox-hash", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "ryu" 1668 | version = "1.0.18" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1671 | 1672 | [[package]] 1673 | name = "scopeguard" 1674 | version = "1.2.0" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1677 | 1678 | [[package]] 1679 | name = "scroll" 1680 | version = "0.11.0" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" 1683 | dependencies = [ 1684 | "scroll_derive 0.11.1", 1685 | ] 1686 | 1687 | [[package]] 1688 | name = "scroll" 1689 | version = "0.12.0" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" 1692 | dependencies = [ 1693 | "scroll_derive 0.12.1", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "scroll_derive" 1698 | version = "0.11.1" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" 1701 | dependencies = [ 1702 | "proc-macro2", 1703 | "quote", 1704 | "syn 2.0.98", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "scroll_derive" 1709 | version = "0.12.1" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" 1712 | dependencies = [ 1713 | "proc-macro2", 1714 | "quote", 1715 | "syn 2.0.98", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "serde" 1720 | version = "1.0.210" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 1723 | dependencies = [ 1724 | "serde_derive", 1725 | ] 1726 | 1727 | [[package]] 1728 | name = "serde_derive" 1729 | version = "1.0.210" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 1732 | dependencies = [ 1733 | "proc-macro2", 1734 | "quote", 1735 | "syn 2.0.98", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "serde_json" 1740 | version = "1.0.128" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 1743 | dependencies = [ 1744 | "itoa", 1745 | "memchr", 1746 | "ryu", 1747 | "serde", 1748 | ] 1749 | 1750 | [[package]] 1751 | name = "shlex" 1752 | version = "1.3.0" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1755 | 1756 | [[package]] 1757 | name = "signal-hook" 1758 | version = "0.3.17" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1761 | dependencies = [ 1762 | "libc", 1763 | "signal-hook-registry", 1764 | ] 1765 | 1766 | [[package]] 1767 | name = "signal-hook-mio" 1768 | version = "0.2.4" 1769 | source = "registry+https://github.com/rust-lang/crates.io-index" 1770 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1771 | dependencies = [ 1772 | "libc", 1773 | "mio", 1774 | "signal-hook", 1775 | ] 1776 | 1777 | [[package]] 1778 | name = "signal-hook-registry" 1779 | version = "1.4.2" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1782 | dependencies = [ 1783 | "libc", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "smallvec" 1788 | version = "1.13.2" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1791 | 1792 | [[package]] 1793 | name = "stable_deref_trait" 1794 | version = "1.2.0" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1797 | 1798 | [[package]] 1799 | name = "static_assertions" 1800 | version = "1.1.0" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1803 | 1804 | [[package]] 1805 | name = "str_stack" 1806 | version = "0.1.0" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" 1809 | 1810 | [[package]] 1811 | name = "strsim" 1812 | version = "0.10.0" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1815 | 1816 | [[package]] 1817 | name = "strsim" 1818 | version = "0.11.1" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1821 | 1822 | [[package]] 1823 | name = "strum" 1824 | version = "0.26.3" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1827 | dependencies = [ 1828 | "strum_macros", 1829 | ] 1830 | 1831 | [[package]] 1832 | name = "strum_macros" 1833 | version = "0.26.4" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1836 | dependencies = [ 1837 | "heck 0.5.0", 1838 | "proc-macro2", 1839 | "quote", 1840 | "rustversion", 1841 | "syn 2.0.98", 1842 | ] 1843 | 1844 | [[package]] 1845 | name = "syn" 1846 | version = "1.0.109" 1847 | source = "registry+https://github.com/rust-lang/crates.io-index" 1848 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1849 | dependencies = [ 1850 | "proc-macro2", 1851 | "quote", 1852 | "unicode-ident", 1853 | ] 1854 | 1855 | [[package]] 1856 | name = "syn" 1857 | version = "2.0.98" 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" 1859 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1860 | dependencies = [ 1861 | "proc-macro2", 1862 | "quote", 1863 | "unicode-ident", 1864 | ] 1865 | 1866 | [[package]] 1867 | name = "tempfile" 1868 | version = "3.10.1" 1869 | source = "registry+https://github.com/rust-lang/crates.io-index" 1870 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1871 | dependencies = [ 1872 | "cfg-if", 1873 | "fastrand", 1874 | "rustix 0.38.34", 1875 | "windows-sys 0.52.0", 1876 | ] 1877 | 1878 | [[package]] 1879 | name = "termcolor" 1880 | version = "1.4.1" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1883 | dependencies = [ 1884 | "winapi-util", 1885 | ] 1886 | 1887 | [[package]] 1888 | name = "terminal_size" 1889 | version = "0.2.6" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" 1892 | dependencies = [ 1893 | "rustix 0.37.27", 1894 | "windows-sys 0.48.0", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "termios" 1899 | version = "0.3.3" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" 1902 | dependencies = [ 1903 | "libc", 1904 | ] 1905 | 1906 | [[package]] 1907 | name = "textwrap" 1908 | version = "0.16.1" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 1911 | dependencies = [ 1912 | "terminal_size", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "thiserror" 1917 | version = "1.0.61" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 1920 | dependencies = [ 1921 | "thiserror-impl", 1922 | ] 1923 | 1924 | [[package]] 1925 | name = "thiserror-impl" 1926 | version = "1.0.61" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 1929 | dependencies = [ 1930 | "proc-macro2", 1931 | "quote", 1932 | "syn 2.0.98", 1933 | ] 1934 | 1935 | [[package]] 1936 | name = "tui-input" 1937 | version = "0.11.1" 1938 | source = "registry+https://github.com/rust-lang/crates.io-index" 1939 | checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114" 1940 | dependencies = [ 1941 | "ratatui", 1942 | "unicode-width 0.2.0", 1943 | ] 1944 | 1945 | [[package]] 1946 | name = "twox-hash" 1947 | version = "1.6.3" 1948 | source = "registry+https://github.com/rust-lang/crates.io-index" 1949 | checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" 1950 | dependencies = [ 1951 | "cfg-if", 1952 | "static_assertions", 1953 | ] 1954 | 1955 | [[package]] 1956 | name = "typed-arena" 1957 | version = "2.0.2" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 1960 | 1961 | [[package]] 1962 | name = "unicode-ident" 1963 | version = "1.0.12" 1964 | source = "registry+https://github.com/rust-lang/crates.io-index" 1965 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1966 | 1967 | [[package]] 1968 | name = "unicode-segmentation" 1969 | version = "1.11.0" 1970 | source = "registry+https://github.com/rust-lang/crates.io-index" 1971 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1972 | 1973 | [[package]] 1974 | name = "unicode-truncate" 1975 | version = "1.0.0" 1976 | source = "registry+https://github.com/rust-lang/crates.io-index" 1977 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 1978 | dependencies = [ 1979 | "itertools 0.12.1", 1980 | "unicode-width 0.1.12", 1981 | ] 1982 | 1983 | [[package]] 1984 | name = "unicode-width" 1985 | version = "0.1.12" 1986 | source = "registry+https://github.com/rust-lang/crates.io-index" 1987 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 1988 | 1989 | [[package]] 1990 | name = "unicode-width" 1991 | version = "0.2.0" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1994 | 1995 | [[package]] 1996 | name = "utf8parse" 1997 | version = "0.2.1" 1998 | source = "registry+https://github.com/rust-lang/crates.io-index" 1999 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2000 | 2001 | [[package]] 2002 | name = "version_check" 2003 | version = "0.9.4" 2004 | source = "registry+https://github.com/rust-lang/crates.io-index" 2005 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2006 | 2007 | [[package]] 2008 | name = "wasi" 2009 | version = "0.11.0+wasi-snapshot-preview1" 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" 2011 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2012 | 2013 | [[package]] 2014 | name = "wasm-bindgen" 2015 | version = "0.2.92" 2016 | source = "registry+https://github.com/rust-lang/crates.io-index" 2017 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 2018 | dependencies = [ 2019 | "cfg-if", 2020 | "wasm-bindgen-macro", 2021 | ] 2022 | 2023 | [[package]] 2024 | name = "wasm-bindgen-backend" 2025 | version = "0.2.92" 2026 | source = "registry+https://github.com/rust-lang/crates.io-index" 2027 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 2028 | dependencies = [ 2029 | "bumpalo", 2030 | "log", 2031 | "once_cell", 2032 | "proc-macro2", 2033 | "quote", 2034 | "syn 2.0.98", 2035 | "wasm-bindgen-shared", 2036 | ] 2037 | 2038 | [[package]] 2039 | name = "wasm-bindgen-macro" 2040 | version = "0.2.92" 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" 2042 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 2043 | dependencies = [ 2044 | "quote", 2045 | "wasm-bindgen-macro-support", 2046 | ] 2047 | 2048 | [[package]] 2049 | name = "wasm-bindgen-macro-support" 2050 | version = "0.2.92" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 2053 | dependencies = [ 2054 | "proc-macro2", 2055 | "quote", 2056 | "syn 2.0.98", 2057 | "wasm-bindgen-backend", 2058 | "wasm-bindgen-shared", 2059 | ] 2060 | 2061 | [[package]] 2062 | name = "wasm-bindgen-shared" 2063 | version = "0.2.92" 2064 | source = "registry+https://github.com/rust-lang/crates.io-index" 2065 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 2066 | 2067 | [[package]] 2068 | name = "web-time" 2069 | version = "1.1.0" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2072 | dependencies = [ 2073 | "js-sys", 2074 | "wasm-bindgen", 2075 | ] 2076 | 2077 | [[package]] 2078 | name = "which" 2079 | version = "4.4.2" 2080 | source = "registry+https://github.com/rust-lang/crates.io-index" 2081 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 2082 | dependencies = [ 2083 | "either", 2084 | "home", 2085 | "once_cell", 2086 | "rustix 0.38.34", 2087 | ] 2088 | 2089 | [[package]] 2090 | name = "winapi" 2091 | version = "0.3.9" 2092 | source = "registry+https://github.com/rust-lang/crates.io-index" 2093 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2094 | dependencies = [ 2095 | "winapi-i686-pc-windows-gnu", 2096 | "winapi-x86_64-pc-windows-gnu", 2097 | ] 2098 | 2099 | [[package]] 2100 | name = "winapi-i686-pc-windows-gnu" 2101 | version = "0.4.0" 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" 2103 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2104 | 2105 | [[package]] 2106 | name = "winapi-util" 2107 | version = "0.1.8" 2108 | source = "registry+https://github.com/rust-lang/crates.io-index" 2109 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 2110 | dependencies = [ 2111 | "windows-sys 0.52.0", 2112 | ] 2113 | 2114 | [[package]] 2115 | name = "winapi-x86_64-pc-windows-gnu" 2116 | version = "0.4.0" 2117 | source = "registry+https://github.com/rust-lang/crates.io-index" 2118 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2119 | 2120 | [[package]] 2121 | name = "windows-core" 2122 | version = "0.52.0" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2125 | dependencies = [ 2126 | "windows-targets 0.52.5", 2127 | ] 2128 | 2129 | [[package]] 2130 | name = "windows-sys" 2131 | version = "0.48.0" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2134 | dependencies = [ 2135 | "windows-targets 0.48.5", 2136 | ] 2137 | 2138 | [[package]] 2139 | name = "windows-sys" 2140 | version = "0.52.0" 2141 | source = "registry+https://github.com/rust-lang/crates.io-index" 2142 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2143 | dependencies = [ 2144 | "windows-targets 0.52.5", 2145 | ] 2146 | 2147 | [[package]] 2148 | name = "windows-targets" 2149 | version = "0.48.5" 2150 | source = "registry+https://github.com/rust-lang/crates.io-index" 2151 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2152 | dependencies = [ 2153 | "windows_aarch64_gnullvm 0.48.5", 2154 | "windows_aarch64_msvc 0.48.5", 2155 | "windows_i686_gnu 0.48.5", 2156 | "windows_i686_msvc 0.48.5", 2157 | "windows_x86_64_gnu 0.48.5", 2158 | "windows_x86_64_gnullvm 0.48.5", 2159 | "windows_x86_64_msvc 0.48.5", 2160 | ] 2161 | 2162 | [[package]] 2163 | name = "windows-targets" 2164 | version = "0.52.5" 2165 | source = "registry+https://github.com/rust-lang/crates.io-index" 2166 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 2167 | dependencies = [ 2168 | "windows_aarch64_gnullvm 0.52.5", 2169 | "windows_aarch64_msvc 0.52.5", 2170 | "windows_i686_gnu 0.52.5", 2171 | "windows_i686_gnullvm", 2172 | "windows_i686_msvc 0.52.5", 2173 | "windows_x86_64_gnu 0.52.5", 2174 | "windows_x86_64_gnullvm 0.52.5", 2175 | "windows_x86_64_msvc 0.52.5", 2176 | ] 2177 | 2178 | [[package]] 2179 | name = "windows_aarch64_gnullvm" 2180 | version = "0.48.5" 2181 | source = "registry+https://github.com/rust-lang/crates.io-index" 2182 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2183 | 2184 | [[package]] 2185 | name = "windows_aarch64_gnullvm" 2186 | version = "0.52.5" 2187 | source = "registry+https://github.com/rust-lang/crates.io-index" 2188 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 2189 | 2190 | [[package]] 2191 | name = "windows_aarch64_msvc" 2192 | version = "0.48.5" 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" 2194 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2195 | 2196 | [[package]] 2197 | name = "windows_aarch64_msvc" 2198 | version = "0.52.5" 2199 | source = "registry+https://github.com/rust-lang/crates.io-index" 2200 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 2201 | 2202 | [[package]] 2203 | name = "windows_i686_gnu" 2204 | version = "0.48.5" 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" 2206 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2207 | 2208 | [[package]] 2209 | name = "windows_i686_gnu" 2210 | version = "0.52.5" 2211 | source = "registry+https://github.com/rust-lang/crates.io-index" 2212 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 2213 | 2214 | [[package]] 2215 | name = "windows_i686_gnullvm" 2216 | version = "0.52.5" 2217 | source = "registry+https://github.com/rust-lang/crates.io-index" 2218 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 2219 | 2220 | [[package]] 2221 | name = "windows_i686_msvc" 2222 | version = "0.48.5" 2223 | source = "registry+https://github.com/rust-lang/crates.io-index" 2224 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2225 | 2226 | [[package]] 2227 | name = "windows_i686_msvc" 2228 | version = "0.52.5" 2229 | source = "registry+https://github.com/rust-lang/crates.io-index" 2230 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 2231 | 2232 | [[package]] 2233 | name = "windows_x86_64_gnu" 2234 | version = "0.48.5" 2235 | source = "registry+https://github.com/rust-lang/crates.io-index" 2236 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2237 | 2238 | [[package]] 2239 | name = "windows_x86_64_gnu" 2240 | version = "0.52.5" 2241 | source = "registry+https://github.com/rust-lang/crates.io-index" 2242 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 2243 | 2244 | [[package]] 2245 | name = "windows_x86_64_gnullvm" 2246 | version = "0.48.5" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2249 | 2250 | [[package]] 2251 | name = "windows_x86_64_gnullvm" 2252 | version = "0.52.5" 2253 | source = "registry+https://github.com/rust-lang/crates.io-index" 2254 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 2255 | 2256 | [[package]] 2257 | name = "windows_x86_64_msvc" 2258 | version = "0.48.5" 2259 | source = "registry+https://github.com/rust-lang/crates.io-index" 2260 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2261 | 2262 | [[package]] 2263 | name = "windows_x86_64_msvc" 2264 | version = "0.52.5" 2265 | source = "registry+https://github.com/rust-lang/crates.io-index" 2266 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 2267 | 2268 | [[package]] 2269 | name = "zerocopy" 2270 | version = "0.7.34" 2271 | source = "registry+https://github.com/rust-lang/crates.io-index" 2272 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 2273 | dependencies = [ 2274 | "zerocopy-derive", 2275 | ] 2276 | 2277 | [[package]] 2278 | name = "zerocopy-derive" 2279 | version = "0.7.34" 2280 | source = "registry+https://github.com/rust-lang/crates.io-index" 2281 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 2282 | dependencies = [ 2283 | "proc-macro2", 2284 | "quote", 2285 | "syn 2.0.98", 2286 | ] 2287 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flamelens" 3 | version = "0.3.1" 4 | authors = ["Yung Siang Liau "] 5 | license = "MIT" 6 | description = "Flamegraph viewer in the terminal" 7 | readme = "README.md" 8 | homepage = "https://github.com/YS-L/flamelens" 9 | repository = "https://github.com/YS-L/flamelens" 10 | exclude = [".github/*", "tests/*", ".vscode/*"] 11 | keywords = ["flamegraph", "profiling", "cli", "tui"] 12 | edition = "2021" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | anyhow = "1.0.86" 18 | cfg-if = "1.0.0" 19 | clap = { version = "4.5.4", features = ["derive"] } 20 | crossterm = { version = "0.28.1", features = ["use-dev-tty"] } 21 | py-spy = { version = "0.4.0", optional = true } 22 | ratatui = { version = "0.29.0", features = ["unstable-rendered-line-info"] } 23 | regex = "1.10.5" 24 | remoteprocess = { version = "0.4.13", optional = true } 25 | serde = { version = "1.0.210", features = ["derive"] } 26 | serde_json = "1.0.128" 27 | tui-input = "0.11.1" 28 | 29 | [features] 30 | python = ["dep:py-spy", "dep:remoteprocess"] 31 | 32 | # The profile that 'cargo dist' will build with 33 | [profile.dist] 34 | inherits = "release" 35 | lto = "thin" 36 | 37 | [profile.release] 38 | strip = "debuginfo" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yung Siang Liau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | cargo fmt --check 3 | cargo clippy --all-features -- -Dwarnings 4 | 5 | test: 6 | cargo test 7 | cargo test --all-features -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flamelens 2 | 3 | `flamelens` is an interactive flamegraph viewer in the terminal. 4 | 5 | ![Demo](.github/demo.gif) 6 | 7 | ## What is it? 8 | 9 | Flamegraph tools such as [FlameGraph](https://github.com/brendangregg/FlameGraph) and 10 | [inferno](https://github.com/jonhoo/inferno) process output from various profiling tools and 11 | generate intermediate data in the "folded" format ready for flamegraph plotting. Instead of plotting 12 | the flamegraph as an SVG file, `flamelens` takes the folded stacks data and generate an interactive 13 | flamegraph in the terminal. 14 | 15 | No more hauling SVG files and opening a browser just to have a quick look at the profiling result! 16 | 17 | ## Usage 18 | 19 | Run `flamelens` with the filename of the profiling data in the form of "folded stacks": 20 | 21 | ``` 22 | flamelens 23 | ``` 24 | 25 | You can also pipe data directly to `flamelens` without providing a filename. 26 | 27 | 28 | ### cargo-flamegraph 29 | 30 | You can use `flamelens` as the viewer of [`cargo flamegraph`](https://github.com/flamegraph-rs/flamegraph) this way: 31 | 32 | ``` 33 | cargo flamegraph --post-process 'flamelens --echo' [other cargo flamegraph arguments] 34 | ``` 35 | 36 | The `--echo` flag ensures that the flamegraph SVG file is also generated by `cargo flamegraph` on 37 | exit. 38 | 39 | ### Viewing `perf` data 40 | If have a `perf.data` file generated by `perf` (e.g. by using `cargo flamegraph` to profile your 41 | program in Linux), you can visualize it in `flamelens` this way with the help of 42 | [inferno](https://crates.io/crates/inferno): 43 | 44 | ``` 45 | perf script -i perf.data | inferno-collapse-perf | flamelens 46 | ``` 47 | 48 | See [inferno](https://crates.io/crates/inferno) on generating folded stacks data from profiling data 49 | of different formats. 50 | 51 | ### Python 52 | 53 | Display a live flamegraph of a running Python program using 54 | [`py-spy`](https://github.com/benfred/py-spy) as the profiler: 55 | 56 | ``` 57 | flamelens --pid 58 | ``` 59 | 60 | This requires enabling the `python` feature when installing. 61 | 62 | Example of a live flamegraph: 63 | 64 | ![demo-live](.github/demo-live.gif) 65 | 66 | ## Key bindings 67 | Key | Action 68 | --- | --- 69 | `hjkl` (or `← ↓ ↑→ `) | Navigate cursor for frame selection 70 | `f` | Scroll down 71 | `b` | Scroll up 72 | `G` | Scroll to bottom 73 | `g` | Scroll to top 74 | `Enter` | Zoom in on the selected frame 75 | `Esc` | Reset zoom 76 | `/` | Find and highlight frames matching the regex 77 | `#` | Find and highlight frames matching the selected frame 78 | `n` | Jump to next match 79 | `N` | Jump to previous match 80 | `r` | Reset to default view 81 | `z` (in Live mode) | Freeze the flamegraph 82 | `q` (or `Ctrl + c`) | Exit 83 | 84 | ## Installation 85 | 86 | If you have [Rust](https://www.rust-lang.org/tools/install) installed, `flamelens` is available on 87 | [crates.io](https://crates.io/crates/flamelens) and you can install it using: 88 | 89 | ``` 90 | cargo install flamelens --locked 91 | ``` 92 | 93 | If you want the live flamegraph functionality, install with the `--all-features` option: 94 | ``` 95 | cargo install flamelens --locked --all-features 96 | ``` 97 | 98 | Note: Compiling with `--all-features` option may require `libunwind` to be installed on your system. 99 | 100 | Alternatively, build and install from source after cloning this repo: 101 | ``` 102 | cargo install --path $(pwd) --locked 103 | ``` 104 | 105 | ### Distro Packages 106 | 107 | #### Arch Linux 108 | 109 | You can install from the [extra repository](https://archlinux.org/packages/extra/x86_64/flamelens/) using `pacman`: 110 | 111 | ``` 112 | pacman -S flamelens 113 | ``` 114 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = [] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 14 | # Which actions to run on pull requests 15 | pr-run-mode = "plan" 16 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::flame::{FlameGraph, SearchPattern}; 2 | #[cfg(feature = "python")] 3 | use crate::py_spy::{record_samples, ProfilerOutput, SamplerState, SamplerStatus}; 4 | use crate::state::FlameGraphState; 5 | use crate::view::FlameGraphView; 6 | #[cfg(feature = "python")] 7 | use remoteprocess; 8 | use std::collections::HashMap; 9 | use std::error; 10 | use std::sync::{Arc, Mutex}; 11 | #[cfg(feature = "python")] 12 | use std::thread; 13 | use std::time::Duration; 14 | 15 | /// Application result type. 16 | pub type AppResult = std::result::Result>; 17 | 18 | #[derive(Debug)] 19 | pub enum FlameGraphInput { 20 | File(String), 21 | Pid(u64, Option), 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct ParsedFlameGraph { 26 | pub flamegraph: FlameGraph, 27 | pub elapsed: Duration, 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct InputBuffer { 32 | pub buffer: tui_input::Input, 33 | pub cursor: Option<(u16, u16)>, 34 | } 35 | 36 | /// Application. 37 | #[derive(Debug)] 38 | pub struct App { 39 | /// Is the application running? 40 | pub running: bool, 41 | /// Flamegraph view 42 | pub flamegraph_view: FlameGraphView, 43 | /// Flamegraph input information 44 | pub flamegraph_input: FlameGraphInput, 45 | /// User input buffer 46 | pub input_buffer: Option, 47 | /// Timing information for debugging 48 | pub elapsed: HashMap, 49 | /// Transient message 50 | pub transient_message: Option, 51 | /// Debug mode 52 | pub debug: bool, 53 | /// Next flamegraph to swap in 54 | next_flamegraph: Arc>>, 55 | #[cfg(feature = "python")] 56 | sampler_state: Option>>, 57 | } 58 | 59 | impl App { 60 | /// Constructs a new instance of [`App`]. 61 | pub fn with_flamegraph(filename: &str, flamegraph: FlameGraph) -> Self { 62 | Self { 63 | running: true, 64 | flamegraph_view: FlameGraphView::new(flamegraph), 65 | flamegraph_input: FlameGraphInput::File(filename.to_string()), 66 | input_buffer: None, 67 | elapsed: HashMap::new(), 68 | transient_message: None, 69 | debug: false, 70 | next_flamegraph: Arc::new(Mutex::new(None)), 71 | #[cfg(feature = "python")] 72 | sampler_state: None, 73 | } 74 | } 75 | 76 | #[cfg(feature = "python")] 77 | pub fn with_pid(pid: u64, py_spy_args: Option) -> Self { 78 | let next_flamegraph: Arc>> = Arc::new(Mutex::new(None)); 79 | let pyspy_data: Arc>> = Arc::new(Mutex::new(None)); 80 | let sampler_state = Arc::new(Mutex::new(SamplerState::default())); 81 | 82 | // Thread to poll data from pyspy and construct the next flamegraph 83 | { 84 | let next_flamegraph = next_flamegraph.clone(); 85 | let pyspy_data = pyspy_data.clone(); 86 | let _handle = thread::spawn(move || loop { 87 | if let Some(output) = pyspy_data.lock().unwrap().take() { 88 | let tic = std::time::Instant::now(); 89 | let flamegraph = FlameGraph::from_string(output.data, true); 90 | let parsed = ParsedFlameGraph { 91 | flamegraph, 92 | elapsed: tic.elapsed(), 93 | }; 94 | *next_flamegraph.lock().unwrap() = Some(parsed); 95 | } 96 | thread::sleep(std::time::Duration::from_millis(250)); 97 | }); 98 | } 99 | 100 | // pyspy live sampler thread 101 | { 102 | let pyspy_data = pyspy_data.clone(); 103 | let sampler_state = sampler_state.clone(); 104 | let _handle = thread::spawn(move || { 105 | // Note: mimic a record command's invocation vs simply getting default Config as 106 | // from_args does a lot of heavy lifting 107 | let mut args = [ 108 | "py-spy", 109 | "record", 110 | "--pid", 111 | pid.to_string().as_str(), 112 | "--format", 113 | "raw", 114 | ] 115 | .iter() 116 | .map(|s| s.to_string()) 117 | .collect::>(); 118 | if let Some(py_spy_args) = py_spy_args { 119 | args.extend(py_spy_args.split_whitespace().map(|s| s.to_string())); 120 | } 121 | let config = py_spy::Config::from_args(&args).unwrap(); 122 | let pid = pid as remoteprocess::Pid; 123 | record_samples(pid, &config, pyspy_data, sampler_state); 124 | }); 125 | } 126 | 127 | let flamegraph = FlameGraph::from_string("".to_string(), true); 128 | let process_info = remoteprocess::Process::new(pid as remoteprocess::Pid) 129 | .and_then(|p| p.cmdline()) 130 | .ok() 131 | .map(|c| c.join(" ")); 132 | Self { 133 | running: true, 134 | flamegraph_view: FlameGraphView::new(flamegraph), 135 | flamegraph_input: FlameGraphInput::Pid(pid, process_info), 136 | next_flamegraph: next_flamegraph.clone(), 137 | input_buffer: None, 138 | elapsed: HashMap::new(), 139 | transient_message: None, 140 | debug: false, 141 | sampler_state: Some(sampler_state), 142 | } 143 | } 144 | 145 | /// Handles the tick event of the terminal. 146 | pub fn tick(&mut self) { 147 | // Replace flamegraph 148 | if !self.flamegraph_view.state.freeze { 149 | if let Some(parsed) = self.next_flamegraph.lock().unwrap().take() { 150 | self.elapsed 151 | .insert("flamegraph".to_string(), parsed.elapsed); 152 | let tic = std::time::Instant::now(); 153 | self.flamegraph_view.replace_flamegraph(parsed.flamegraph); 154 | self.elapsed 155 | .insert("replacement".to_string(), tic.elapsed()); 156 | } 157 | } 158 | 159 | // Exit if fatal error in sampler 160 | #[cfg(feature = "python")] 161 | if let Some(SamplerStatus::Error(s)) = self 162 | .sampler_state 163 | .as_ref() 164 | .map(|s| s.lock().unwrap().status.clone()) 165 | { 166 | panic!("py-spy sampler exited with error: {}\n\nYou likely need to rerun this program with sudo.", s); 167 | } 168 | } 169 | 170 | /// Set running to false to quit the application. 171 | pub fn quit(&mut self) { 172 | self.running = false; 173 | } 174 | 175 | pub fn flamegraph(&self) -> &FlameGraph { 176 | &self.flamegraph_view.flamegraph 177 | } 178 | 179 | pub fn flamegraph_state(&self) -> &FlameGraphState { 180 | &self.flamegraph_view.state 181 | } 182 | 183 | #[cfg(feature = "python")] 184 | pub fn sampler_state(&self) -> Option { 185 | self.sampler_state 186 | .as_ref() 187 | .map(|s| s.lock().unwrap().clone()) 188 | } 189 | 190 | pub fn add_elapsed(&mut self, name: &str, elapsed: Duration) { 191 | self.elapsed.insert(name.to_string(), elapsed); 192 | } 193 | 194 | pub fn search_selected(&mut self) { 195 | if self.flamegraph_view.is_root_selected() { 196 | return; 197 | } 198 | let short_name = self.flamegraph_view.get_selected_stack().map(|s| { 199 | self.flamegraph() 200 | .get_stack_short_name_from_info(s) 201 | .to_string() 202 | }); 203 | if let Some(short_name) = short_name { 204 | self.set_manual_search_pattern(short_name.as_str(), false); 205 | } 206 | } 207 | 208 | pub fn search_selected_row(&mut self) { 209 | let short_name = self 210 | .flamegraph_view 211 | .get_selected_row_name() 212 | .map(|s| s.to_string()); 213 | if let Some(short_name) = short_name { 214 | self.set_manual_search_pattern(short_name.as_str(), false); 215 | } 216 | self.flamegraph_view.state.toggle_view_kind(); 217 | } 218 | 219 | pub fn set_manual_search_pattern(&mut self, pattern: &str, is_regex: bool) { 220 | match SearchPattern::new(pattern, is_regex, true) { 221 | Ok(p) => self.flamegraph_view.set_search_pattern(p), 222 | Err(_) => { 223 | self.set_transient_message(&format!("Invalid regex: {}", pattern)); 224 | } 225 | } 226 | } 227 | 228 | pub fn set_transient_message(&mut self, message: &str) { 229 | self.transient_message = Some(message.to_string()); 230 | } 231 | 232 | pub fn clear_transient_message(&mut self) { 233 | self.transient_message = None; 234 | } 235 | 236 | pub fn toggle_debug(&mut self) { 237 | self.debug = !self.debug; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::app::AppResult; 2 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time::{Duration, Instant}; 6 | 7 | /// Terminal events. 8 | #[derive(Clone, Copy, Debug)] 9 | pub enum Event { 10 | /// Terminal tick. 11 | Tick, 12 | /// Key press. 13 | Key(KeyEvent), 14 | /// Mouse click/scroll. 15 | Mouse(MouseEvent), 16 | /// Terminal resize. 17 | Resize(u16, u16), 18 | } 19 | 20 | /// Terminal event handler. 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | pub struct EventHandler { 24 | /// Event sender channel. 25 | sender: mpsc::Sender, 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | /// Event handler thread. 29 | handler: thread::JoinHandle<()>, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let tick_rate = Duration::from_millis(tick_rate); 36 | let (sender, receiver) = mpsc::channel(); 37 | let handler = { 38 | let sender = sender.clone(); 39 | thread::spawn(move || { 40 | let mut last_tick = Instant::now(); 41 | loop { 42 | let timeout = tick_rate 43 | .checked_sub(last_tick.elapsed()) 44 | .unwrap_or(tick_rate); 45 | 46 | if event::poll(timeout).expect("failed to poll new events") { 47 | match event::read().expect("unable to read event") { 48 | CrosstermEvent::Key(e) => { 49 | if e.kind == KeyEventKind::Press { 50 | sender.send(Event::Key(e)) 51 | } else { 52 | Ok(()) 53 | } 54 | } 55 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 56 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 57 | CrosstermEvent::FocusGained => Ok(()), 58 | CrosstermEvent::FocusLost => Ok(()), 59 | CrosstermEvent::Paste(_) => unimplemented!(), 60 | } 61 | .expect("failed to send terminal event") 62 | } 63 | 64 | if last_tick.elapsed() >= tick_rate { 65 | sender.send(Event::Tick).expect("failed to send tick event"); 66 | last_tick = Instant::now(); 67 | } 68 | } 69 | }) 70 | }; 71 | Self { 72 | sender, 73 | receiver, 74 | handler, 75 | } 76 | } 77 | 78 | /// Receive the next event from the handler thread. 79 | /// 80 | /// This function will always block the current thread if 81 | /// there is no data available and it's possible for more data to be sent. 82 | pub fn next(&self) -> AppResult { 83 | Ok(self.receiver.recv()?) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/flame.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use serde::Serialize; 4 | 5 | pub type StackIdentifier = usize; 6 | pub static ROOT: &str = "all"; 7 | pub static ROOT_ID: usize = 0; 8 | 9 | #[derive(Serialize, Debug, Clone, PartialEq)] 10 | pub struct StackInfo { 11 | pub id: StackIdentifier, 12 | pub line_index: usize, 13 | pub start_index: usize, 14 | pub end_index: usize, 15 | pub total_count: u64, 16 | pub self_count: u64, 17 | pub parent: Option, 18 | pub children: Vec, 19 | pub level: usize, 20 | pub width_factor: f64, 21 | pub hit: bool, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct SearchPattern { 26 | pub pattern: String, 27 | pub is_regex: bool, 28 | pub re: regex::Regex, 29 | pub is_manual: bool, 30 | } 31 | 32 | impl SearchPattern { 33 | pub fn new(pattern: &str, is_regex: bool, is_manual: bool) -> Result { 34 | let _pattern = if is_regex { 35 | pattern.to_string() 36 | } else { 37 | format!("^{}$", regex::escape(pattern)) 38 | }; 39 | let re = regex::Regex::new(&_pattern)?; 40 | Ok(Self { 41 | pattern: pattern.to_string(), 42 | is_regex, 43 | re, 44 | is_manual, 45 | }) 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct Hits { 51 | coverage_count: u64, 52 | ids: Vec, 53 | } 54 | 55 | #[derive(Serialize, Debug, Clone, Default)] 56 | pub struct Count { 57 | pub total: u64, 58 | pub own: u64, 59 | } 60 | 61 | #[derive(Serialize, Debug, Clone)] 62 | pub struct CountEntry { 63 | pub name: String, 64 | pub count: Count, 65 | pub visible: bool, 66 | } 67 | 68 | #[derive(Serialize, Debug, Clone, Eq, PartialEq, Copy)] 69 | pub enum SortColumn { 70 | Total, 71 | Own, 72 | } 73 | 74 | #[derive(Serialize, Debug, Clone)] 75 | pub struct Ordered { 76 | pub entries: Vec, 77 | pub num_rows: usize, 78 | pub sorted_column: SortColumn, 79 | pub search_pattern_ignored_because_of_no_match: bool, 80 | } 81 | 82 | impl Ordered { 83 | pub fn set_search_pattern(&mut self, p: &SearchPattern) { 84 | if p.is_manual { 85 | self.entries.iter_mut().for_each(|entry| { 86 | entry.visible = p.re.is_match(&entry.name); 87 | }); 88 | self.num_rows = self.entries.iter().filter(|entry| entry.visible).count(); 89 | if self.num_rows == 0 { 90 | self.clear_search_pattern(); 91 | self.search_pattern_ignored_because_of_no_match = true; 92 | } 93 | } else { 94 | self.clear_search_pattern(); 95 | } 96 | } 97 | 98 | pub fn clear_search_pattern(&mut self) { 99 | self.entries.iter_mut().for_each(|entry| { 100 | entry.visible = true; 101 | }); 102 | self.num_rows = self.entries.len(); 103 | self.search_pattern_ignored_because_of_no_match = false; 104 | } 105 | 106 | pub fn set_sort_column(&mut self, column: SortColumn) { 107 | if column == self.sorted_column { 108 | return; 109 | } 110 | self.sorted_column = column; 111 | match column { 112 | SortColumn::Total => { 113 | self.entries 114 | .sort_by_key(|entry| (entry.count.total, entry.name.clone())); 115 | } 116 | SortColumn::Own => { 117 | self.entries 118 | .sort_by_key(|entry| (entry.count.own, entry.name.clone())); 119 | } 120 | } 121 | self.entries.reverse(); 122 | } 123 | } 124 | 125 | #[derive(Debug, Clone)] 126 | pub struct FlameGraph { 127 | data: String, 128 | stacks: Vec, 129 | levels: Vec>, 130 | pub ordered_stacks: Ordered, 131 | hits: Option, 132 | sorted: bool, 133 | } 134 | 135 | impl FlameGraph { 136 | pub fn from_string(mut content: String, sorted: bool) -> Self { 137 | // Make sure content ends with newline to simplify parsing 138 | if !content.ends_with('\n') { 139 | content.push('\n'); 140 | } 141 | let mut stacks = Vec::::new(); 142 | stacks.push(StackInfo { 143 | id: ROOT_ID, 144 | line_index: 0, 145 | start_index: 0, 146 | end_index: 0, 147 | total_count: 0, 148 | self_count: 0, 149 | width_factor: 0.0, 150 | parent: None, 151 | children: Vec::::new(), 152 | level: 0, 153 | hit: false, 154 | }); 155 | let mut last_line_index = 0; 156 | let mut counts: HashMap = HashMap::new(); 157 | for line_index in content 158 | .char_indices() 159 | .filter(|(_, c)| *c == '\n') 160 | .map(|(i, _)| i) 161 | { 162 | let line = &content[last_line_index..line_index]; 163 | #[allow(clippy::unnecessary_unwrap)] 164 | let line_and_count = match line.rsplit_once(' ') { 165 | Some((line, count)) => { 166 | let parsed_count = count.parse::(); 167 | if line.is_empty() || parsed_count.is_err() { 168 | None 169 | } else { 170 | Some((line, parsed_count.unwrap())) 171 | } 172 | } 173 | _ => None, 174 | }; 175 | if line_and_count.is_none() || line.starts_with('#') { 176 | last_line_index = line_index + 1; 177 | continue; 178 | } 179 | let (line, count) = line_and_count.unwrap(); 180 | 181 | stacks[ROOT_ID].total_count += count; 182 | let mut parent_id = ROOT_ID; 183 | let mut level = 1; 184 | let mut last_delim_index = 0; 185 | let mut counted_names = HashSet::::new(); 186 | for delim_index in line 187 | .char_indices() 188 | .filter(|(_, c)| *c == ';') 189 | .map(|(i, _)| i) 190 | { 191 | let stack_id = FlameGraph::update_one( 192 | &mut stacks, 193 | &mut counts, 194 | &mut counted_names, 195 | &content, 196 | count, 197 | last_line_index, 198 | last_line_index + last_delim_index, 199 | last_line_index + delim_index, 200 | parent_id, 201 | level, 202 | false, 203 | ); 204 | parent_id = stack_id; 205 | level += 1; 206 | last_delim_index = delim_index + 1; 207 | } 208 | FlameGraph::update_one( 209 | &mut stacks, 210 | &mut counts, 211 | &mut counted_names, 212 | &content, 213 | count, 214 | last_line_index, 215 | last_line_index + last_delim_index, 216 | last_line_index + line.len(), 217 | parent_id, 218 | level, 219 | true, 220 | ); 221 | last_line_index = line_index + 1; 222 | } 223 | 224 | let ordered = FlameGraph::get_ordered_stacks(&counts); 225 | let mut out = Self { 226 | data: content, 227 | stacks, 228 | levels: vec![], 229 | ordered_stacks: ordered, 230 | hits: None, 231 | sorted, 232 | }; 233 | out.populate_levels(&ROOT_ID, 0, None); 234 | out 235 | } 236 | 237 | fn get_ordered_stacks(counts: &HashMap) -> Ordered { 238 | let mut counts = counts.iter().collect::>(); 239 | counts.sort_by_key(|(short_name, count)| (count.own, short_name.to_string())); 240 | let ordered_by_self_count = counts 241 | .iter() 242 | .rev() 243 | .map(|x| CountEntry { 244 | name: x.0.to_string(), 245 | count: x.1.clone(), 246 | visible: true, 247 | }) 248 | .collect::>(); 249 | let num_rows = ordered_by_self_count.len(); 250 | Ordered { 251 | entries: ordered_by_self_count, 252 | num_rows, 253 | sorted_column: SortColumn::Own, 254 | search_pattern_ignored_because_of_no_match: false, 255 | } 256 | } 257 | 258 | #[allow(clippy::too_many_arguments)] 259 | fn update_one( 260 | stacks: &mut Vec, 261 | counts: &mut HashMap, 262 | counted_names: &mut HashSet, 263 | content: &str, 264 | count: u64, 265 | line_index: usize, 266 | start_index: usize, 267 | end_index: usize, 268 | parent_id: StackIdentifier, 269 | level: usize, 270 | is_self: bool, 271 | ) -> StackIdentifier { 272 | let short_name = &content[start_index..end_index]; 273 | 274 | // Invariant: parent always exists. We can just check the short name to 275 | // check if the parent already contains the child, since the prior 276 | // prefixes should always match (definition of a parent). 277 | let parent_stack = stacks.get(parent_id).unwrap(); 278 | 279 | // Add or update the current stack 280 | let current_stack_id_if_exists = parent_stack 281 | .children 282 | .iter() 283 | .find(|child_id| { 284 | let child = stacks.get(**child_id).unwrap(); 285 | &content[child.start_index..child.end_index] == short_name 286 | }) 287 | .cloned(); 288 | let stack_id = if let Some(stack_id) = current_stack_id_if_exists { 289 | stack_id 290 | } else { 291 | stacks.push(StackInfo { 292 | id: stacks.len(), 293 | line_index, 294 | start_index, 295 | end_index, 296 | total_count: 0, 297 | self_count: 0, 298 | width_factor: 0.0, 299 | parent: Some(parent_id), 300 | children: Vec::::new(), 301 | level, 302 | hit: false, 303 | }); 304 | let stack_id = stacks.len() - 1; 305 | stacks.get_mut(parent_id).unwrap().children.push(stack_id); 306 | stack_id 307 | }; 308 | let info = stacks.get_mut(stack_id).unwrap(); 309 | info.total_count += count; 310 | if is_self { 311 | info.self_count += count; 312 | } 313 | 314 | // Update summarized counts 315 | let summarized_count = counts.entry(short_name.to_string()).or_default(); 316 | if !counted_names.contains(short_name) { 317 | counted_names.insert(short_name.to_string()); 318 | summarized_count.total += count; 319 | } 320 | if is_self { 321 | summarized_count.own += count; 322 | } 323 | 324 | stack_id 325 | } 326 | 327 | fn populate_levels( 328 | &mut self, 329 | stack_id: &StackIdentifier, 330 | level: usize, 331 | parent_total_count_and_width_factor: Option<(u64, f64)>, 332 | ) { 333 | // Update levels 334 | if self.levels.len() <= level { 335 | self.levels.push(vec![]); 336 | } 337 | self.levels[level].push(*stack_id); 338 | 339 | // Calculate width_factor of the current stack 340 | let stack = self.stacks.get(*stack_id).unwrap(); 341 | let total_count = stack.total_count; 342 | let width_factor = if let Some((parent_total_count, parent_width_factor)) = 343 | parent_total_count_and_width_factor 344 | { 345 | parent_width_factor * (total_count as f64 / parent_total_count as f64) 346 | } else { 347 | 1.0 348 | }; 349 | 350 | // Sort children 351 | let sorted_children = if self.sorted { 352 | let mut sorted_children = stack.children.clone(); 353 | sorted_children.sort_by_key(|child_id| { 354 | self.stacks 355 | .get(*child_id) 356 | .map(|child| child.total_count) 357 | .unwrap_or(0) 358 | }); 359 | sorted_children.reverse(); 360 | Some(sorted_children) 361 | } else { 362 | None 363 | }; 364 | 365 | // Make the updates to the current stack 366 | let stack = self.stacks.get_mut(*stack_id).unwrap(); 367 | stack.width_factor = width_factor; 368 | if let Some(sorted_children) = sorted_children { 369 | stack.children = sorted_children; 370 | } 371 | 372 | // Move on to children 373 | for child_id in stack.children.clone().iter() { 374 | self.populate_levels(child_id, level + 1, Some((total_count, width_factor))); 375 | } 376 | } 377 | 378 | pub fn get_stack(&self, stack_id: &StackIdentifier) -> Option<&StackInfo> { 379 | self.stacks.get(*stack_id) 380 | } 381 | 382 | pub fn get_stack_short_name(&self, stack_id: &StackIdentifier) -> Option<&str> { 383 | self.get_stack(stack_id) 384 | .map(|stack| self.get_stack_short_name_from_info(stack)) 385 | } 386 | 387 | pub fn get_stack_full_name(&self, stack_id: &StackIdentifier) -> Option<&str> { 388 | self.get_stack(stack_id) 389 | .map(|stack| self.get_stack_full_name_from_info(stack)) 390 | } 391 | 392 | pub fn get_stack_short_name_from_info(&self, stack: &StackInfo) -> &str { 393 | if stack.id == ROOT_ID { 394 | ROOT 395 | } else { 396 | &self.data[stack.start_index..stack.end_index] 397 | } 398 | } 399 | 400 | pub fn get_stack_full_name_from_info(&self, stack: &StackInfo) -> &str { 401 | if stack.id == ROOT_ID { 402 | ROOT 403 | } else { 404 | &self.data[stack.line_index..stack.end_index] 405 | } 406 | } 407 | 408 | pub fn get_stack_by_full_name(&self, full_name: &str) -> Option<&StackInfo> { 409 | self.stacks 410 | .iter() 411 | .find(|stack| self.get_stack_full_name_from_info(stack) == full_name) 412 | } 413 | 414 | pub fn get_stack_id_by_full_name(&self, full_name: &str) -> Option { 415 | self.get_stack_by_full_name(full_name).map(|stack| stack.id) 416 | } 417 | 418 | pub fn get_stacks_at_level(&self, level: usize) -> Option<&Vec> { 419 | self.levels.get(level) 420 | } 421 | 422 | pub fn root(&self) -> &StackInfo { 423 | self.get_stack(&ROOT_ID).unwrap() 424 | } 425 | 426 | pub fn total_count(&self) -> u64 { 427 | self.root().total_count 428 | } 429 | 430 | pub fn get_num_levels(&self) -> usize { 431 | self.levels.len() 432 | } 433 | 434 | pub fn get_ancestors(&self, stack_id: &StackIdentifier) -> Vec { 435 | let mut ancestors = vec![]; 436 | let mut current_id = *stack_id; 437 | while let Some(stack) = self.get_stack(¤t_id) { 438 | ancestors.push(current_id); 439 | if let Some(parent_id) = stack.parent { 440 | current_id = parent_id; 441 | } else { 442 | break; 443 | } 444 | } 445 | ancestors 446 | } 447 | 448 | pub fn get_descendants(&self, stack_id: &StackIdentifier) -> Vec { 449 | let mut descendants = vec![]; 450 | let mut stack_ids = vec![*stack_id]; 451 | while let Some(stack_id) = stack_ids.pop() { 452 | descendants.push(stack_id); 453 | if let Some(stack) = self.get_stack(&stack_id) { 454 | stack_ids.extend(stack.children.iter().copied()); 455 | } 456 | } 457 | descendants 458 | } 459 | 460 | pub fn set_hits(&mut self, p: &SearchPattern) { 461 | self.stacks.iter_mut().for_each(|stack| { 462 | stack.hit = 463 | p.re.is_match(&self.data[stack.start_index..stack.end_index]); 464 | }); 465 | self.hits = Some(Hits { 466 | coverage_count: self._count_hit_coverage(ROOT_ID), 467 | ids: self._collect_hit_ids(), 468 | }); 469 | self.ordered_stacks.set_search_pattern(p); 470 | } 471 | 472 | pub fn clear_hits(&mut self) { 473 | self.stacks.iter_mut().for_each(|stack| stack.hit = false); 474 | self.hits = None; 475 | self.ordered_stacks.clear_search_pattern(); 476 | } 477 | 478 | pub fn hit_coverage_count(&self) -> Option { 479 | self.hits.as_ref().map(|h| h.coverage_count) 480 | } 481 | 482 | pub fn hit_ids(&self) -> Option<&Vec> { 483 | self.hits.as_ref().map(|h| &h.ids) 484 | } 485 | 486 | fn _count_hit_coverage(&self, stack_id: StackIdentifier) -> u64 { 487 | let stack = self.get_stack(&stack_id).unwrap(); 488 | if stack.hit { 489 | return stack.total_count; 490 | } 491 | let mut count = 0; 492 | for child_id in stack.children.iter() { 493 | count += self._count_hit_coverage(*child_id); 494 | } 495 | count 496 | } 497 | 498 | fn _collect_hit_ids(&self) -> Vec { 499 | let mut hits = vec![]; 500 | for level in self.levels.iter() { 501 | for stacks in level.iter() { 502 | if let Some(stack) = self.get_stack(stacks) { 503 | if stack.hit { 504 | hits.push(*stacks); 505 | } 506 | } 507 | } 508 | } 509 | hits 510 | } 511 | } 512 | 513 | #[cfg(test)] 514 | mod tests { 515 | use super::*; 516 | 517 | const UPDATE_FIXTURES: bool = false; 518 | 519 | #[derive(Serialize, Debug, Clone, PartialEq)] 520 | pub struct StackInfoReadable<'a> { 521 | pub id: StackIdentifier, 522 | pub line_index: usize, 523 | pub start_index: usize, 524 | pub end_index: usize, 525 | pub total_count: u64, 526 | pub self_count: u64, 527 | pub parent: Option, 528 | pub children: Vec, 529 | pub level: usize, 530 | pub width_factor: f64, 531 | pub hit: bool, 532 | pub short_name: &'a str, 533 | pub full_name: &'a str, 534 | } 535 | 536 | impl FlameGraph { 537 | pub fn to_readable_stacks(&self) -> Vec { 538 | self.stacks 539 | .iter() 540 | .map(|stack| StackInfoReadable { 541 | id: stack.id, 542 | line_index: stack.line_index, 543 | start_index: stack.start_index, 544 | end_index: stack.end_index, 545 | total_count: stack.total_count, 546 | self_count: stack.self_count, 547 | parent: stack.parent, 548 | children: stack.children.clone(), 549 | level: stack.level, 550 | width_factor: stack.width_factor, 551 | hit: stack.hit, 552 | short_name: self.get_stack_short_name_from_info(stack), 553 | full_name: self.get_stack_full_name_from_info(stack), 554 | }) 555 | .collect() 556 | } 557 | } 558 | 559 | fn check_result>(data_filename: P) -> FlameGraph { 560 | let content = std::fs::read_to_string(&data_filename).unwrap(); 561 | let fg = FlameGraph::from_string(content, true); 562 | 563 | // Location to store all the fixtures for this test data 564 | let tag = data_filename 565 | .as_ref() 566 | .file_stem() 567 | .unwrap() 568 | .to_str() 569 | .unwrap(); 570 | let fixture_dir = format!("tests/fixtures/{}", tag); 571 | 572 | // Check expected stacks 573 | let serialized = serde_json::to_string_pretty(&fg.to_readable_stacks()).unwrap(); 574 | let filename = format!("{}/expected_stacks.json", fixture_dir.as_str()); 575 | if UPDATE_FIXTURES { 576 | std::fs::create_dir_all(fixture_dir.as_str()).unwrap(); 577 | std::fs::write(&filename, serialized.clone()).unwrap(); 578 | } 579 | let expected = std::fs::read_to_string(&filename).unwrap(); 580 | assert_eq!(serialized, expected); 581 | 582 | // Check ordered counts 583 | let serialized = serde_json::to_string_pretty(&fg.ordered_stacks).unwrap(); 584 | let filename = format!("{}/expected_ordered_counts.json", fixture_dir.as_str()); 585 | if UPDATE_FIXTURES { 586 | std::fs::create_dir_all(fixture_dir).unwrap(); 587 | std::fs::write(&filename, serialized.clone()).unwrap(); 588 | } 589 | let expected = std::fs::read_to_string(&filename).unwrap(); 590 | assert_eq!(serialized, expected); 591 | 592 | assert_eq!(UPDATE_FIXTURES, false, "Set UPDATE_FIXTURES to false"); 593 | fg 594 | } 595 | 596 | #[test] 597 | fn test_simple() { 598 | let fg = check_result("tests/data/py-spy-simple.txt"); 599 | assert_eq!(fg.total_count(), 657); 600 | assert_eq!( 601 | *fg.root(), 602 | StackInfo { 603 | id: ROOT_ID, 604 | line_index: 0, 605 | start_index: 0, 606 | end_index: 0, 607 | total_count: 657, 608 | self_count: 0, 609 | width_factor: 1.0, 610 | parent: None, 611 | children: vec![3, 1, 5], 612 | level: 0, 613 | hit: false, 614 | } 615 | ); 616 | } 617 | 618 | #[test] 619 | fn test_no_name_count() { 620 | let fg = check_result("tests/data/invalid-lines.txt"); 621 | assert_eq!(fg.total_count(), 428); 622 | } 623 | 624 | #[test] 625 | fn test_ignore_lines_starting_with_hash() { 626 | check_result("tests/data/ignore-metadata-lines.txt"); 627 | } 628 | 629 | #[test] 630 | fn test_recursive() { 631 | check_result("tests/data/recursive.txt"); 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use crate::{ 4 | app::{App, AppResult, InputBuffer}, 5 | state::ViewKind, 6 | }; 7 | use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; 8 | use tui_input::backend::crossterm::EventHandler; 9 | 10 | /// Handles the key events and updates the state of [`App`]. 11 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 12 | if app.input_buffer.is_none() { 13 | let tic = Instant::now(); 14 | handle_command(key_event, app)?; 15 | app.add_elapsed("handle_key_events", tic.elapsed()); 16 | Ok(()) 17 | } else { 18 | handle_input_buffer(key_event, app) 19 | } 20 | } 21 | 22 | /// Handle key events as commands 23 | pub fn handle_command(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 24 | let mut key_handled = handle_command_generic(key_event, app)?; 25 | if !key_handled { 26 | if app.flamegraph_state().view_kind == ViewKind::FlameGraph { 27 | key_handled = handle_command_flamegraph(key_event, app)?; 28 | } else { 29 | key_handled = handle_command_table(key_event, app)?; 30 | } 31 | } 32 | if key_handled && app.transient_message.is_some() { 33 | app.clear_transient_message(); 34 | } 35 | Ok(()) 36 | } 37 | 38 | pub fn handle_command_generic(key_event: KeyEvent, app: &mut App) -> AppResult { 39 | let mut key_handled = true; 40 | match key_event.code { 41 | // Exit application on `q` 42 | KeyCode::Char('q') => { 43 | app.quit(); 44 | } 45 | // Exit application on `Ctrl-C` 46 | KeyCode::Char('c') | KeyCode::Char('C') => { 47 | if key_event.modifiers == KeyModifiers::CONTROL { 48 | app.quit(); 49 | } 50 | } 51 | KeyCode::Char('z') => { 52 | app.flamegraph_view.state.toggle_freeze(); 53 | } 54 | KeyCode::Tab => { 55 | app.flamegraph_view.state.toggle_view_kind(); 56 | } 57 | KeyCode::Char('/') => { 58 | app.input_buffer = Some(InputBuffer { 59 | buffer: tui_input::Input::new("".to_string()), 60 | cursor: None, 61 | }); 62 | } 63 | KeyCode::Char('?') => { 64 | app.toggle_debug(); 65 | } 66 | _ => { 67 | key_handled = false; 68 | } 69 | } 70 | Ok(key_handled) 71 | } 72 | 73 | fn handle_command_flamegraph(key_event: KeyEvent, app: &mut App) -> AppResult { 74 | let mut key_handled = true; 75 | match key_event.code { 76 | KeyCode::Right | KeyCode::Char('l') => { 77 | app.flamegraph_view.to_next_sibling(); 78 | } 79 | KeyCode::Left | KeyCode::Char('h') => { 80 | app.flamegraph_view.to_previous_sibling(); 81 | } 82 | KeyCode::Down | KeyCode::Char('j') => { 83 | app.flamegraph_view.to_child_stack(); 84 | } 85 | KeyCode::Up | KeyCode::Char('k') => { 86 | app.flamegraph_view.to_parent_stack(); 87 | } 88 | KeyCode::Char('G') => { 89 | app.flamegraph_view.scroll_bottom(); 90 | } 91 | KeyCode::Char('g') => { 92 | app.flamegraph_view.scroll_top(); 93 | } 94 | KeyCode::Char('f') => { 95 | app.flamegraph_view.page_down(); 96 | } 97 | KeyCode::Char('b') => { 98 | app.flamegraph_view.page_up(); 99 | } 100 | KeyCode::Char('n') => { 101 | app.flamegraph_view.to_next_search_result(); 102 | } 103 | KeyCode::Char('N') => { 104 | app.flamegraph_view.to_previous_search_result(); 105 | } 106 | KeyCode::Enter => { 107 | app.flamegraph_view.set_zoom(); 108 | } 109 | KeyCode::Esc => { 110 | app.flamegraph_view.unset_zoom(); 111 | } 112 | KeyCode::Char('r') => { 113 | app.flamegraph_view.reset(); 114 | } 115 | KeyCode::Char('#') => { 116 | app.search_selected(); 117 | } 118 | _ => { 119 | key_handled = false; 120 | } 121 | } 122 | Ok(key_handled) 123 | } 124 | 125 | fn handle_command_table(key_event: KeyEvent, app: &mut App) -> AppResult { 126 | let mut key_handled = true; 127 | match key_event.code { 128 | KeyCode::Down | KeyCode::Char('j') => { 129 | app.flamegraph_view.to_next_row(); 130 | } 131 | KeyCode::Up | KeyCode::Char('k') => { 132 | app.flamegraph_view.to_previous_row(); 133 | } 134 | KeyCode::Char('f') => { 135 | app.flamegraph_view.scroll_next_rows(); 136 | } 137 | KeyCode::Char('b') => { 138 | app.flamegraph_view.scroll_previous_rows(); 139 | } 140 | KeyCode::Char('1') => { 141 | app.flamegraph_view.set_sort_by_total(); 142 | } 143 | KeyCode::Char('2') => { 144 | app.flamegraph_view.set_sort_by_own(); 145 | } 146 | KeyCode::Char('r') => { 147 | app.flamegraph_view.reset(); 148 | } 149 | KeyCode::Enter => { 150 | app.search_selected_row(); 151 | } 152 | _ => { 153 | key_handled = false; 154 | } 155 | } 156 | Ok(key_handled) 157 | } 158 | 159 | pub fn handle_input_buffer(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 160 | if let Some(input) = app.input_buffer.as_mut() { 161 | match key_event.code { 162 | KeyCode::Esc => { 163 | app.input_buffer = None; 164 | } 165 | KeyCode::Enter => { 166 | if input.buffer.value().is_empty() { 167 | app.flamegraph_view.unset_manual_search_pattern(); 168 | } else { 169 | let re_pattern = input.buffer.value().to_string(); 170 | app.set_manual_search_pattern(re_pattern.as_str(), true); 171 | } 172 | app.input_buffer = None; 173 | } 174 | _ => { 175 | input.buffer.handle_event(&Event::Key(key_event)); 176 | } 177 | } 178 | } 179 | Ok(()) 180 | } 181 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Application. 2 | pub mod app; 3 | 4 | /// Terminal events handler. 5 | pub mod event; 6 | 7 | /// Widget renderer. 8 | pub mod ui; 9 | 10 | /// Terminal user interface. 11 | pub mod tui; 12 | 13 | /// Event handler. 14 | pub mod handler; 15 | 16 | pub mod flame; 17 | 18 | pub mod state; 19 | 20 | pub mod view; 21 | 22 | #[cfg(feature = "python")] 23 | pub mod py_spy; 24 | 25 | #[cfg(feature = "python")] 26 | pub mod py_spy_flamegraph; 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{command, Parser}; 2 | use flamelens::app::{App, AppResult}; 3 | use flamelens::event::{Event, EventHandler}; 4 | use flamelens::flame::FlameGraph; 5 | use flamelens::handler::handle_key_events; 6 | use flamelens::tui::Tui; 7 | use ratatui::backend::CrosstermBackend; 8 | use ratatui::Terminal; 9 | use std::io::{self, Read}; 10 | 11 | #[derive(Parser, Debug)] 12 | #[command(version)] 13 | struct Args { 14 | /// Profile data filename 15 | filename: Option, 16 | 17 | /// Whether to sort the stacks by time spent 18 | #[clap(long, action, value_name = "sorted")] 19 | sorted: bool, 20 | 21 | /// Print data to stdout on exit. Useful when piping to other tools 22 | #[clap(long, action, value_name = "echo")] 23 | echo: bool, 24 | 25 | /// Pid for live flamegraph 26 | #[cfg(feature = "python")] 27 | #[clap(long, value_name = "pid")] 28 | pid: Option, 29 | 30 | /// Additional arguments to pass to "py-spy record" command 31 | #[cfg(feature = "python")] 32 | #[clap(long, value_name = "py-spy-args")] 33 | py_spy_args: Option, 34 | 35 | /// Show debug info 36 | #[clap(long)] 37 | debug: bool, 38 | } 39 | 40 | fn get_app_from_filename_or_stdin(args: &Args, echo: bool) -> App { 41 | let (filename, content) = if let Some(filename) = &args.filename { 42 | ( 43 | filename.as_str(), 44 | std::fs::read_to_string(filename).expect("Could not read file"), 45 | ) 46 | } else { 47 | let mut buf: Vec = Vec::new(); 48 | io::stdin() 49 | .read_to_end(&mut buf) 50 | .expect("Could not read stdin"); 51 | let content = String::from_utf8(buf).expect("Could not parse stdin"); 52 | ("stdin", content) 53 | }; 54 | if echo { 55 | println!("{}", content); 56 | } 57 | let tic = std::time::Instant::now(); 58 | let flamegraph = FlameGraph::from_string(content, args.sorted); 59 | let mut app = App::with_flamegraph(filename, flamegraph); 60 | app.add_elapsed("flamegraph", tic.elapsed()); 61 | app 62 | } 63 | 64 | fn main() -> AppResult<()> { 65 | let args = Args::parse(); 66 | 67 | // Create an application. 68 | cfg_if::cfg_if! { 69 | if #[cfg(feature = "python")] { 70 | let mut app = if let Some(_pid) = args.pid { 71 | App::with_pid( 72 | _pid.parse().expect("Could not parse pid"), 73 | args.py_spy_args.clone(), 74 | ) 75 | } else { 76 | get_app_from_filename_or_stdin(&args, args.echo) 77 | }; 78 | } else { 79 | let mut app = get_app_from_filename_or_stdin(&args, args.echo); 80 | } 81 | } 82 | app.debug = args.debug; 83 | 84 | // Initialize the terminal user interface. 85 | let backend = CrosstermBackend::new(io::stderr()); 86 | let terminal = Terminal::new(backend)?; 87 | let events = EventHandler::new(250); 88 | let mut tui = Tui::new(terminal, events); 89 | tui.init()?; 90 | 91 | // Start the main loop. 92 | while app.running { 93 | // Render the user interface. 94 | tui.draw(&mut app)?; 95 | // Handle events. 96 | match tui.events.next()? { 97 | Event::Tick => app.tick(), 98 | Event::Key(key_event) => handle_key_events(key_event, &mut app)?, 99 | Event::Mouse(_) => {} 100 | Event::Resize(_, _) => {} 101 | } 102 | } 103 | 104 | // Exit the user interface. 105 | tui.exit()?; 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /src/py_spy.rs: -------------------------------------------------------------------------------- 1 | // Part of this file is taken from the py-spy project main.rs 2 | // https://github.com/benfred/py-spy/blob/master/src/flamegraph.rs 3 | // licensed under the MIT License: 4 | /* 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2018-2019 Ben Frederickson 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | */ 27 | use crate::py_spy_flamegraph::Flamegraph as PySpyFlamegraph; 28 | use anyhow::Error; 29 | use py_spy::config::RecordDuration; 30 | use py_spy::sampler; 31 | use py_spy::Config; 32 | use py_spy::Frame; 33 | use remoteprocess; 34 | use std::sync::{Arc, Mutex}; 35 | use std::time::{Duration, Instant}; 36 | 37 | #[derive(Debug, Clone, Default)] 38 | pub enum SamplerStatus { 39 | #[default] 40 | Running, 41 | Error(String), 42 | Done, 43 | } 44 | 45 | #[derive(Debug, Clone, Default)] 46 | pub struct SamplerState { 47 | pub status: SamplerStatus, 48 | pub total_sampled_duration: Duration, 49 | pub late: Option, 50 | } 51 | 52 | impl SamplerState { 53 | pub fn set_status(&mut self, status: SamplerStatus) { 54 | self.status = status; 55 | } 56 | 57 | pub fn set_total_sampled_duration(&mut self, total_sampled_duration: Duration) { 58 | self.total_sampled_duration = total_sampled_duration; 59 | } 60 | 61 | pub fn set_late(&mut self, late: Duration) { 62 | self.late = Some(late); 63 | } 64 | 65 | pub fn unset_late(&mut self) { 66 | self.late = None; 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | pub struct ProfilerOutput { 72 | pub data: String, 73 | } 74 | 75 | pub fn record_samples( 76 | pid: remoteprocess::Pid, 77 | config: &Config, 78 | output_data: Arc>>, 79 | state: Arc>, 80 | ) { 81 | state.lock().unwrap().set_status(SamplerStatus::Running); 82 | let result = run(pid, config, output_data, state.clone()); 83 | match result { 84 | Ok(_) => { 85 | state.lock().unwrap().set_status(SamplerStatus::Done); 86 | } 87 | Err(e) => { 88 | state 89 | .lock() 90 | .unwrap() 91 | .set_status(SamplerStatus::Error(format!("{:?}", e))); 92 | } 93 | } 94 | } 95 | 96 | pub fn run( 97 | pid: remoteprocess::Pid, 98 | config: &Config, 99 | output_data: Arc>>, 100 | state: Arc>, 101 | ) -> Result<(), Error> { 102 | let mut output = PySpyFlamegraph::new(config.show_line_numbers); 103 | 104 | let start_tic = std::time::Instant::now(); 105 | let sampler = sampler::Sampler::new(pid, config)?; 106 | 107 | let max_intervals = match &config.duration { 108 | RecordDuration::Unlimited => None, 109 | RecordDuration::Seconds(sec) => Some(sec * config.sampling_rate), 110 | }; 111 | 112 | let mut _errors = 0; 113 | let mut intervals = 0; 114 | let mut _samples = 0; 115 | 116 | let mut last_late_message = std::time::Instant::now(); 117 | let mut last_data_dump: Option = None; 118 | 119 | for mut sample in sampler { 120 | if let Some(delay) = sample.late { 121 | if delay > Duration::from_secs(1) { 122 | let now = std::time::Instant::now(); 123 | if now - last_late_message > Duration::from_secs(1) { 124 | last_late_message = now; 125 | state.lock().unwrap().set_late(delay); 126 | } 127 | } else { 128 | state.lock().unwrap().unset_late(); 129 | } 130 | } else { 131 | state.lock().unwrap().unset_late(); 132 | } 133 | 134 | intervals += 1; 135 | if let Some(max_intervals) = max_intervals { 136 | if intervals >= max_intervals { 137 | break; 138 | } 139 | } 140 | 141 | for trace in sample.traces.iter_mut() { 142 | if !(config.include_idle || trace.active) { 143 | continue; 144 | } 145 | 146 | if config.gil_only && !trace.owns_gil { 147 | continue; 148 | } 149 | 150 | if config.include_thread_ids { 151 | let threadid = trace.format_threadid(); 152 | let thread_fmt = if let Some(thread_name) = &trace.thread_name { 153 | format!("thread ({}): {}", threadid, thread_name) 154 | } else { 155 | format!("thread ({})", threadid) 156 | }; 157 | trace.frames.push(Frame { 158 | name: thread_fmt, 159 | filename: String::from(""), 160 | module: None, 161 | short_filename: None, 162 | line: 0, 163 | locals: None, 164 | is_entry: true, 165 | }); 166 | } 167 | 168 | if let Some(process_info) = trace.process_info.as_ref() { 169 | trace.frames.push(process_info.to_frame()); 170 | let mut parent = process_info.parent.as_ref(); 171 | while parent.is_some() { 172 | if let Some(process_info) = parent { 173 | trace.frames.push(process_info.to_frame()); 174 | parent = process_info.parent.as_ref(); 175 | } 176 | } 177 | } 178 | 179 | _samples += 1; 180 | output.increment(trace)?; 181 | } 182 | 183 | if let Some(sampling_errors) = sample.sampling_errors { 184 | for (_pid, _e) in sampling_errors { 185 | _errors += 1; 186 | } 187 | } 188 | 189 | let should_dump = match last_data_dump { 190 | Some(last_data_dump) => { 191 | let elapsed = Instant::now() - last_data_dump; 192 | elapsed.as_millis() >= 250 193 | } 194 | None => true, 195 | }; 196 | if should_dump { 197 | last_data_dump = Some(Instant::now()); 198 | let data = output.get_data(); 199 | // let mut file = std::fs::File::create("data.txt")?; 200 | // std::io::Write::write_all(&mut file, data.as_bytes())?; 201 | let profiler_output = ProfilerOutput { data }; 202 | output_data.lock().unwrap().replace(profiler_output); 203 | state 204 | .lock() 205 | .unwrap() 206 | .set_total_sampled_duration(start_tic.elapsed()); 207 | } 208 | } 209 | 210 | Ok(()) 211 | } 212 | -------------------------------------------------------------------------------- /src/py_spy_flamegraph.rs: -------------------------------------------------------------------------------- 1 | // This code is again taken from the flamegraph.rs from py-spy 2 | // https://github.com/benfred/py-spy/blob/master/src/flamegraph.rs 3 | // licensed under the MIT License: 4 | /* 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2018-2019 Ben Frederickson 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | */ 27 | 28 | // This code is taken from the flamegraph.rs from rbspy 29 | // https://github.com/rbspy/rbspy/tree/master/src/ui/flamegraph.rs 30 | // licensed under the MIT License: 31 | /* 32 | MIT License 33 | 34 | Copyright (c) 2016 Julia Evans, Kamal Marhubi 35 | Portions (continuous integration setup) Copyright (c) 2016 Jorge Aparicio 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | */ 55 | 56 | use std::collections::HashMap; 57 | use std::io::Write; 58 | 59 | use anyhow::Error; 60 | 61 | use py_spy::StackTrace; 62 | 63 | pub struct Flamegraph { 64 | pub counts: HashMap, 65 | pub show_linenumbers: bool, 66 | } 67 | 68 | impl Flamegraph { 69 | pub fn new(show_linenumbers: bool) -> Flamegraph { 70 | Flamegraph { 71 | counts: HashMap::new(), 72 | show_linenumbers, 73 | } 74 | } 75 | 76 | pub fn increment(&mut self, trace: &StackTrace) -> std::io::Result<()> { 77 | // convert the frame into a single ';' delimited String 78 | let frame = trace 79 | .frames 80 | .iter() 81 | .rev() 82 | .map(|frame| { 83 | let filename = match &frame.short_filename { 84 | Some(f) => f, 85 | None => &frame.filename, 86 | }; 87 | if self.show_linenumbers && frame.line != 0 { 88 | format!("{} ({}:{})", frame.name, filename, frame.line) 89 | } else if !filename.is_empty() { 90 | format!("{} ({})", frame.name, filename) 91 | } else { 92 | frame.name.clone() 93 | } 94 | }) 95 | .collect::>() 96 | .join(";"); 97 | // update counts for that frame 98 | *self.counts.entry(frame).or_insert(0) += 1; 99 | Ok(()) 100 | } 101 | 102 | fn get_lines(&self) -> Vec { 103 | self.counts 104 | .iter() 105 | .map(|(k, v)| format!("{} {}", k, v)) 106 | .collect() 107 | } 108 | 109 | pub fn write_raw(&self, w: &mut dyn Write) -> Result<(), Error> { 110 | for line in self.get_lines() { 111 | w.write_all(line.as_bytes())?; 112 | w.write_all(b"\n")?; 113 | } 114 | Ok(()) 115 | } 116 | 117 | pub fn get_data(&self) -> String { 118 | self.get_lines().join("\n") 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::flame::{FlameGraph, SearchPattern, StackIdentifier, ROOT_ID}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct ZoomState { 5 | pub stack_id: StackIdentifier, 6 | pub ancestors: Vec, 7 | pub descendants: Vec, 8 | pub zoom_factor: f64, 9 | } 10 | 11 | impl ZoomState { 12 | pub fn is_ancestor_or_descendant(&self, stack_id: &StackIdentifier) -> bool { 13 | self.ancestors.contains(stack_id) || self.descendants.contains(stack_id) 14 | } 15 | } 16 | 17 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 18 | pub enum ViewKind { 19 | FlameGraph, 20 | Table, 21 | } 22 | 23 | #[derive(Default, Debug, Clone)] 24 | pub struct TableState { 25 | pub selected: usize, 26 | pub offset: usize, 27 | } 28 | 29 | impl TableState { 30 | pub fn reset(&mut self) { 31 | self.selected = 0; 32 | self.offset = 0; 33 | } 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct FlameGraphState { 38 | pub selected: StackIdentifier, 39 | pub level_offset: usize, 40 | pub frame_height: Option, 41 | pub frame_width: Option, 42 | pub zoom: Option, 43 | pub search_pattern: Option, 44 | pub freeze: bool, 45 | pub view_kind: ViewKind, 46 | pub table_state: TableState, 47 | } 48 | 49 | impl Default for FlameGraphState { 50 | fn default() -> Self { 51 | Self { 52 | selected: ROOT_ID, 53 | level_offset: 0, 54 | frame_height: None, 55 | frame_width: None, 56 | zoom: None, 57 | search_pattern: None, 58 | freeze: false, 59 | view_kind: ViewKind::FlameGraph, 60 | table_state: TableState::default(), 61 | } 62 | } 63 | } 64 | 65 | impl FlameGraphState { 66 | pub fn select_root(&mut self) { 67 | self.selected = ROOT_ID; 68 | } 69 | 70 | pub fn select_id(&mut self, stack_id: &StackIdentifier) { 71 | self.selected.clone_from(stack_id); 72 | } 73 | 74 | pub fn set_zoom(&mut self, zoom: ZoomState) { 75 | self.zoom = Some(zoom); 76 | } 77 | 78 | pub fn unset_zoom(&mut self) { 79 | self.zoom = None; 80 | } 81 | 82 | pub fn set_search_pattern(&mut self, search_pattern: SearchPattern) { 83 | self.search_pattern = Some(search_pattern); 84 | } 85 | 86 | pub fn unset_search_pattern(&mut self) { 87 | self.search_pattern = None; 88 | } 89 | 90 | pub fn toggle_freeze(&mut self) { 91 | self.freeze = !self.freeze; 92 | } 93 | 94 | pub fn toggle_view_kind(&mut self) { 95 | self.view_kind = match self.view_kind { 96 | ViewKind::FlameGraph => ViewKind::Table, 97 | ViewKind::Table => ViewKind::FlameGraph, 98 | }; 99 | } 100 | 101 | /// Update StackIdentifiers to point to the correct ones in the new flamegraph 102 | pub fn handle_flamegraph_replacement(&mut self, old: &FlameGraph, new: &mut FlameGraph) { 103 | if self.selected != ROOT_ID { 104 | if let Some(new_stack_id) = Self::get_new_stack_id(&self.selected, old, new) { 105 | self.selected = new_stack_id; 106 | } else { 107 | self.select_root(); 108 | } 109 | } 110 | if let Some(zoom) = &mut self.zoom { 111 | if let Some(new_stack_id) = Self::get_new_stack_id(&zoom.stack_id, old, new) { 112 | zoom.stack_id = new_stack_id; 113 | } else { 114 | self.unset_zoom(); 115 | } 116 | } 117 | // Preserve search pattern. If expensive, can move this to next flamegraph construction 118 | // thread and share SearchPattern via Arc but let's keep it simple for now. 119 | if let Some(p) = &self.search_pattern { 120 | new.set_hits(p); 121 | } 122 | } 123 | 124 | fn get_new_stack_id( 125 | stack_id: &StackIdentifier, 126 | old: &FlameGraph, 127 | new: &FlameGraph, 128 | ) -> Option { 129 | old.get_stack(stack_id).and_then(|stack| { 130 | new.get_stack_by_full_name(old.get_stack_full_name_from_info(stack)) 131 | .map(|stack| stack.id) 132 | }) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crate::event::EventHandler; 3 | use crate::ui; 4 | use crossterm::event::DisableMouseCapture; 5 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 6 | use ratatui::backend::Backend; 7 | use ratatui::Terminal; 8 | use std::io; 9 | use std::panic; 10 | 11 | /// Representation of a terminal user interface. 12 | /// 13 | /// It is responsible for setting up the terminal, 14 | /// initializing the interface and handling the draw events. 15 | #[derive(Debug)] 16 | pub struct Tui { 17 | /// Interface to the Terminal. 18 | terminal: Terminal, 19 | /// Terminal event handler. 20 | pub events: EventHandler, 21 | } 22 | 23 | impl Tui { 24 | /// Constructs a new instance of [`Tui`]. 25 | pub fn new(terminal: Terminal, events: EventHandler) -> Self { 26 | Self { terminal, events } 27 | } 28 | 29 | /// Initializes the terminal interface. 30 | /// 31 | /// It enables the raw mode and sets terminal properties. 32 | pub fn init(&mut self) -> AppResult<()> { 33 | terminal::enable_raw_mode()?; 34 | crossterm::execute!(io::stderr(), EnterAlternateScreen)?; 35 | 36 | // Define a custom panic hook to reset the terminal properties. 37 | // This way, you won't have your terminal messed up if an unexpected error happens. 38 | let panic_hook = panic::take_hook(); 39 | panic::set_hook(Box::new(move |panic| { 40 | Self::reset().expect("failed to reset the terminal"); 41 | panic_hook(panic); 42 | })); 43 | 44 | self.terminal.hide_cursor()?; 45 | self.terminal.clear()?; 46 | Ok(()) 47 | } 48 | 49 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 50 | /// 51 | /// [`Draw`]: ratatui::Terminal::draw 52 | /// [`rendering`]: crate::ui::render 53 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 54 | self.terminal.draw(|frame| { 55 | ui::render(app, frame); 56 | if let Some(input_buffer) = &app.input_buffer { 57 | if let Some(cursor) = input_buffer.cursor { 58 | frame.set_cursor_position((cursor.0, cursor.1)); 59 | } 60 | } 61 | })?; 62 | Ok(()) 63 | } 64 | 65 | /// Resets the terminal interface. 66 | /// 67 | /// This function is also used for the panic hook to revert 68 | /// the terminal properties if unexpected errors occur. 69 | fn reset() -> AppResult<()> { 70 | terminal::disable_raw_mode()?; 71 | crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; 72 | Ok(()) 73 | } 74 | 75 | /// Exits the terminal interface. 76 | /// 77 | /// It disables the raw mode and reverts back the terminal properties. 78 | pub fn exit(&mut self) -> AppResult<()> { 79 | Self::reset()?; 80 | self.terminal.show_cursor()?; 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "python")] 2 | use crate::py_spy::SamplerStatus; 3 | use crate::{ 4 | app::{App, FlameGraphInput}, 5 | flame::{SortColumn, StackIdentifier, StackInfo}, 6 | state::ViewKind, 7 | }; 8 | use ratatui::{ 9 | buffer::Buffer, 10 | layout::{Alignment, Constraint, Direction, Layout, Offset, Rect}, 11 | style::{Color, Modifier, Style, Stylize}, 12 | text::{Line, Span, Text}, 13 | widgets::{ 14 | block::Position, Block, Borders, Paragraph, Row, StatefulWidget, Table, TableState, Widget, 15 | Wrap, 16 | }, 17 | Frame, 18 | }; 19 | use std::time::Duration; 20 | use std::{ 21 | collections::hash_map::DefaultHasher, 22 | hash::{Hash, Hasher}, 23 | }; 24 | 25 | const SEARCH_PREFIX: &str = ""; 26 | const COLOR_SELECTED_STACK: Color = Color::Rgb(250, 250, 250); 27 | const COLOR_MATCHED_BACKGROUND: Color = Color::Rgb(10, 35, 150); 28 | const COLOR_TABLE_SELECTED_ROW: Color = Color::Rgb(65, 65, 65); 29 | 30 | #[derive(Debug, Clone, Default)] 31 | pub struct FlamelensWidgetState { 32 | frame_height: u16, 33 | frame_width: u16, 34 | render_time: Duration, 35 | cursor_position: Option<(u16, u16)>, 36 | } 37 | 38 | pub struct ZoomState { 39 | pub zoom_stack: StackIdentifier, 40 | pub ancestors: Vec, 41 | } 42 | 43 | pub struct FlamelensWidget<'a> { 44 | pub app: &'a App, 45 | } 46 | 47 | impl<'a> FlamelensWidget<'a> { 48 | pub fn new(app: &'a App) -> Self { 49 | Self { app } 50 | } 51 | } 52 | 53 | impl StatefulWidget for FlamelensWidget<'_> { 54 | type State = FlamelensWidgetState; 55 | 56 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 57 | self.render_all(area, buf, state); 58 | } 59 | } 60 | 61 | impl<'a> FlamelensWidget<'a> { 62 | fn render_all(self, area: Rect, buf: &mut Buffer, state: &mut FlamelensWidgetState) { 63 | let view_kind_indicator = self.get_view_kind_indicator(); 64 | let version_indicator = self.get_version_indicator(); 65 | 66 | let header_text = Text::from(self.get_header_text(area.width)); 67 | let header = Paragraph::new(header_text) 68 | .wrap(Wrap { trim: false }) 69 | .alignment(Alignment::Center); 70 | let indicator_width = std::cmp::max(view_kind_indicator.width(), version_indicator.width()); 71 | let filename_width = area 72 | .width 73 | .saturating_sub(indicator_width as u16) 74 | .saturating_sub(indicator_width as u16); 75 | let header_line_count_with_borders = header.line_count(filename_width) as u16 + 2; 76 | 77 | // Context such as search, selected stack, etc. 78 | let context_bars = self 79 | .get_status_text(area.width) 80 | .iter() 81 | .map(|(title, line)| { 82 | Paragraph::new(line.clone()) 83 | .wrap(Wrap { trim: true }) 84 | .block( 85 | Block::new() 86 | .borders(Borders::TOP) 87 | .title(format!("{} ", title)) 88 | .title_style(Style::default().add_modifier(Modifier::BOLD).yellow()) 89 | .title_position(Position::Top), 90 | ) 91 | }) 92 | .collect::>(); 93 | 94 | // Help tags to be displayed at the bottom 95 | let help_tags = self.get_help_tags(); 96 | let help_bar = Paragraph::new(help_tags.get_line()) 97 | .block( 98 | Block::new() 99 | .borders(Borders::TOP) 100 | .border_style(Style::default()), 101 | ) 102 | .alignment(Alignment::Center); 103 | 104 | let mut constraints = vec![ 105 | Constraint::Length(header_line_count_with_borders), 106 | Constraint::Fill(1), 107 | ]; 108 | 109 | // Constraints for context bars 110 | let context_bar_index_start = constraints.len(); 111 | for bar in context_bars.iter() { 112 | constraints.push(Constraint::Length(bar.line_count(area.width) as u16)); 113 | } 114 | 115 | // Constraint for help bar 116 | constraints.push(Constraint::Length(2)); 117 | let help_bar_index = constraints.len() - 1; 118 | 119 | let layout = Layout::default() 120 | .direction(Direction::Vertical) 121 | .constraints(constraints) 122 | .split(area); 123 | 124 | // Header area 125 | let header_layout = Layout::default() 126 | .direction(Direction::Horizontal) 127 | .constraints(vec![ 128 | Constraint::Length(view_kind_indicator.width() as u16), 129 | Constraint::Fill(1), 130 | Constraint::Length(version_indicator.width() as u16), 131 | ]) 132 | .split(layout[0]); 133 | let header_block = Block::default().borders(Borders::BOTTOM | Borders::TOP); 134 | let header_offset = Offset { x: 0, y: 1 }; 135 | header_block.render(layout[0], buf); 136 | view_kind_indicator.render(header_layout[0].offset(header_offset), buf); 137 | header.render(header_layout[1].offset(header_offset), buf); 138 | version_indicator.render(header_layout[2].offset(header_offset), buf); 139 | 140 | // Main area for flamegraph / top view 141 | let tic = std::time::Instant::now(); 142 | let main_area = layout[1]; 143 | if self.is_flamegraph_view() { 144 | self.render_flamegraph(main_area, buf) 145 | } else { 146 | self.render_table(main_area, buf); 147 | false 148 | }; 149 | let flamegraph_render_time = tic.elapsed(); 150 | 151 | // Context bars 152 | for (i, bar) in context_bars.iter().enumerate() { 153 | bar.render(layout[context_bar_index_start + i], buf); 154 | } 155 | 156 | // Help bar 157 | help_bar.render(layout[help_bar_index], buf); 158 | 159 | // Update widget state 160 | state.frame_height = main_area.height; 161 | state.frame_width = main_area.width; 162 | state.render_time = flamegraph_render_time; 163 | state.cursor_position = self.get_cursor_position(layout[help_bar_index - 1]); 164 | } 165 | 166 | fn get_help_tags(&self) -> HelpTags { 167 | let mut help_tags = HelpTags::new(); 168 | if self.is_flamegraph_view() { 169 | help_tags.add("hjkl", "move cursor"); 170 | help_tags.add("f/b", "scroll"); 171 | help_tags.add("enter/esc", "zoom"); 172 | help_tags.add("/", "search"); 173 | help_tags.add("#", "search like cursor"); 174 | if let Some(p) = &self.app.flamegraph_state().search_pattern { 175 | if p.is_manual { 176 | help_tags.add("n/N", "next/prev search"); 177 | } 178 | } 179 | #[cfg(feature = "python")] 180 | if let FlameGraphInput::Pid(_, _) = self.app.flamegraph_input { 181 | if self.app.flamegraph_state().freeze { 182 | help_tags.add("z", "unfreeze"); 183 | } else { 184 | help_tags.add("z", "freeze"); 185 | } 186 | } 187 | } else { 188 | help_tags.add("j/k", "move cursor"); 189 | help_tags.add("f/b", "scroll"); 190 | help_tags.add("1", "sort by total"); 191 | help_tags.add("2", "sort by own"); 192 | help_tags.add("/", "filter"); 193 | } 194 | help_tags 195 | } 196 | 197 | fn render_flamegraph(&self, area: Rect, buf: &mut Buffer) -> bool { 198 | let zoom_state = self 199 | .app 200 | .flamegraph_state() 201 | .zoom 202 | .as_ref() 203 | .map(|zoom| ZoomState { 204 | zoom_stack: zoom.stack_id, 205 | ancestors: self.app.flamegraph().get_ancestors(&zoom.stack_id), 206 | }); 207 | let re = self 208 | .app 209 | .flamegraph_state() 210 | .search_pattern 211 | .as_ref() 212 | .and_then(|p| { 213 | if p.is_manual { 214 | Some(&p.re) 215 | } else { 216 | // Don't highlight if the whole stack is expected to be matched (this is 217 | // when auto-searching while navigating between stacks) 218 | None 219 | } 220 | }); 221 | let has_more_rows_to_render = self.render_stacks( 222 | self.app.flamegraph().root(), 223 | buf, 224 | area.x, 225 | area.y, 226 | area.width as f64, 227 | area.bottom(), 228 | &zoom_state, 229 | &re, 230 | ); 231 | has_more_rows_to_render 232 | } 233 | 234 | fn render_table(&self, area: Rect, buf: &mut Buffer) { 235 | let ordered_stacks_table = self.get_ordered_stacks_table(); 236 | let mut table_state = TableState::default() 237 | .with_selected(self.app.flamegraph_state().table_state.selected) 238 | .with_offset(self.app.flamegraph_state().table_state.offset); 239 | StatefulWidget::render(ordered_stacks_table, area, buf, &mut table_state); 240 | } 241 | 242 | #[allow(clippy::too_many_arguments)] 243 | fn render_stacks( 244 | &self, 245 | stack: &'a StackInfo, 246 | buf: &mut Buffer, 247 | x: u16, 248 | y: u16, 249 | x_budget: f64, 250 | y_max: u16, 251 | zoom_state: &Option, 252 | re: &Option<®ex::Regex>, 253 | ) -> bool { 254 | let after_level_offset = stack.level >= self.app.flamegraph_state().level_offset; 255 | 256 | // Only render if the stack is visible 257 | let effective_x_budget = x_budget as u16; 258 | if y < y_max && effective_x_budget > 0 { 259 | if after_level_offset { 260 | let stack_color = self.get_stack_color(stack, zoom_state); 261 | let text_color = FlamelensWidget::<'a>::get_text_color(stack_color); 262 | let style = Style::default().fg(text_color).bg(stack_color); 263 | let line = self.get_line_for_stack(stack, effective_x_budget, style, re); 264 | buf.set_line(x, y, &line, effective_x_budget); 265 | } 266 | } else { 267 | // Can skip rendering children if the stack is already not visible 268 | let has_more_rows_to_render = (y >= y_max) && effective_x_budget > 0; 269 | return has_more_rows_to_render; 270 | } 271 | 272 | // Render children 273 | let mut x_offset = 0; 274 | let zoomed_child = stack 275 | .children 276 | .iter() 277 | .position(|child_id| { 278 | if let Some(zoom_state) = zoom_state { 279 | *child_id == zoom_state.zoom_stack || zoom_state.ancestors.contains(child_id) 280 | } else { 281 | false 282 | } 283 | }) 284 | .map(|idx| stack.children[idx]); 285 | 286 | let mut has_more_rows_to_render = false; 287 | for child in &stack.children { 288 | let child_stack = self.app.flamegraph().get_stack(child).unwrap(); 289 | let child_x_budget = if let Some(zoomed_child_id) = zoomed_child { 290 | // Zoomer takes all 291 | if zoomed_child_id == *child { 292 | x_budget 293 | } else { 294 | 0.0 295 | } 296 | } else { 297 | x_budget * (child_stack.total_count as f64 / stack.total_count as f64) 298 | }; 299 | has_more_rows_to_render |= self.render_stacks( 300 | child_stack, 301 | buf, 302 | x + x_offset, 303 | y + if after_level_offset { 1 } else { 0 }, 304 | child_x_budget, 305 | y_max, 306 | zoom_state, 307 | re, 308 | ); 309 | x_offset += child_x_budget as u16; 310 | } 311 | 312 | has_more_rows_to_render 313 | } 314 | 315 | fn get_ordered_stacks_table(&self) -> Table { 316 | let add_sorted_indicator = |label: &str, sort_column: SortColumn| { 317 | let suffix = if sort_column == self.app.flamegraph().ordered_stacks.sorted_column { 318 | " [▼]" 319 | } else { 320 | "" 321 | }; 322 | format!("{}{}", label, suffix) 323 | }; 324 | let header = Row::new(vec![ 325 | add_sorted_indicator("Total", SortColumn::Total), 326 | add_sorted_indicator("Own", SortColumn::Own), 327 | "Name".to_string(), 328 | ]) 329 | .style( 330 | Style::default() 331 | .add_modifier(Modifier::BOLD) 332 | .add_modifier(Modifier::REVERSED), 333 | ); 334 | let counts = &self.app.flamegraph().ordered_stacks.entries; 335 | let mut rows = vec![]; 336 | let total_count = self.app.flamegraph().total_count(); 337 | let mut total_max_width: u16 = 0; 338 | let mut own_max_width: u16 = 0; 339 | 340 | fn format_count(count: u64, total_count: u64) -> String { 341 | format!( 342 | "{} ({:.2}%) ", 343 | count, 344 | 100.0 * count as f64 / total_count as f64 345 | ) 346 | } 347 | 348 | for entry in counts.iter().filter(|entry| entry.visible) { 349 | let total_formatted = Line::from(format_count(entry.count.total, total_count)); 350 | let own_formatted = Line::from(format_count(entry.count.own, total_count)); 351 | total_max_width = total_max_width.max(total_formatted.width() as u16); 352 | own_max_width = own_max_width.max(own_formatted.width() as u16); 353 | let name_formatted = if let Some(p) = &self.app.flamegraph_state().search_pattern { 354 | if p.is_manual { 355 | Line::from(self.get_highlighted_spans( 356 | entry.name.as_str(), 357 | &p.re, 358 | Style::default(), 359 | )) 360 | } else { 361 | Line::from(entry.name.as_str()) 362 | } 363 | } else { 364 | Line::from(entry.name.as_str()) 365 | }; 366 | rows.push(Row::new(vec![ 367 | total_formatted, 368 | own_formatted, 369 | name_formatted, 370 | ])); 371 | } 372 | let widths = [ 373 | Constraint::Max(total_max_width), 374 | Constraint::Max(own_max_width), 375 | Constraint::Fill(1), 376 | ]; 377 | Table::new(rows, widths) 378 | .header(header) 379 | .row_highlight_style(Style::default().bg(COLOR_TABLE_SELECTED_ROW)) 380 | } 381 | 382 | fn get_highlighted_spans<'b>( 383 | &self, 384 | text: &'b str, 385 | re: ®ex::Regex, 386 | style: Style, 387 | ) -> Vec> { 388 | let mut spans = Vec::new(); 389 | let mut matches = re.find_iter(text); 390 | for part in re.split(text) { 391 | // Non-match, regular style 392 | spans.push(Span::styled(part, style)); 393 | // Match, highlighted style 394 | if let Some(matched) = matches.next() { 395 | spans.push(Span::styled( 396 | matched.as_str(), 397 | style 398 | .fg(Color::Rgb(225, 10, 10)) 399 | .add_modifier(Modifier::BOLD), 400 | )); 401 | } 402 | } 403 | spans 404 | } 405 | 406 | fn get_line_for_stack( 407 | &self, 408 | stack: &StackInfo, 409 | width: u16, 410 | style: Style, 411 | re: &Option<®ex::Regex>, 412 | ) -> Line { 413 | let short_name = self.app.flamegraph().get_stack_short_name_from_info(stack); 414 | 415 | // Empty space separator at the beginning 416 | let mut spans = vec![Span::styled(if width > 1 { " " } else { "." }, style)]; 417 | 418 | // Stack name with highlighted search terms if needed 419 | let short_name_spans = if let (true, &Some(re)) = (stack.hit, re) { 420 | self.get_highlighted_spans(short_name, re, style) 421 | } else { 422 | vec![Span::styled(short_name, style)] 423 | }; 424 | spans.extend(short_name_spans); 425 | 426 | // Padding to fill the rest of the width 427 | let pad_length = width 428 | .saturating_sub(short_name.len() as u16) 429 | .saturating_sub(1) as usize; 430 | spans.push(Span::styled( 431 | format!("{:width$}", "", width = pad_length), 432 | style, 433 | )); 434 | 435 | Line::from(spans) 436 | } 437 | 438 | fn get_stack_color(&self, stack: &StackInfo, zoom_state: &Option) -> Color { 439 | if self.app.flamegraph_state().selected == stack.id { 440 | return COLOR_SELECTED_STACK; 441 | } 442 | // Roughly based on flamegraph.pl 443 | fn hash_name(name: &str) -> f64 { 444 | let mut hasher = DefaultHasher::new(); 445 | name.hash(&mut hasher); 446 | hasher.finish() as f64 / u64::MAX as f64 447 | } 448 | let full_name = self.app.flamegraph().get_stack_full_name_from_info(stack); 449 | let v1 = hash_name(full_name); 450 | let v2 = hash_name(full_name); 451 | let mut r; 452 | let mut g; 453 | let mut b; 454 | if !stack.hit { 455 | r = 205 + (50.0 * v2) as u8; 456 | g = (230.0 * v1) as u8; 457 | b = (55.0 * v2) as u8; 458 | } else if let Color::Rgb(r_, g_, b_) = COLOR_MATCHED_BACKGROUND { 459 | r = r_; 460 | g = g_; 461 | b = b_; 462 | } else { 463 | unreachable!(); 464 | } 465 | if let Some(zoom_state) = zoom_state { 466 | if zoom_state.ancestors.contains(&stack.id) { 467 | r = (r as f64 / 2.5) as u8; 468 | g = (g as f64 / 2.5) as u8; 469 | b = (b as f64 / 2.5) as u8; 470 | } 471 | } 472 | Color::Rgb(r, g, b) 473 | } 474 | 475 | fn get_text_color(c: Color) -> Color { 476 | match c { 477 | Color::Rgb(r, g, b) => { 478 | let luma = 0.2126 * r as f64 + 0.7152 * g as f64 + 0.0722 * b as f64; 479 | if luma > 128.0 { 480 | Color::Rgb(10, 10, 10) 481 | } else { 482 | Color::Rgb(225, 225, 225) 483 | } 484 | } 485 | _ => Color::Black, 486 | } 487 | } 488 | 489 | fn get_view_kind_indicator(&self) -> Line { 490 | let mut header_bottom_title_spans = vec![Span::from(" ")]; 491 | 492 | fn _get_view_kind_span( 493 | label: &str, 494 | view_kind: ViewKind, 495 | current_view_kind: ViewKind, 496 | ) -> Span { 497 | let (content, style) = if view_kind == current_view_kind { 498 | (format!("[{}]", label), Style::default().bold().yellow()) 499 | } else { 500 | (label.to_string(), Style::default().bold()) 501 | }; 502 | Span::styled(content, style) 503 | } 504 | 505 | header_bottom_title_spans.push(_get_view_kind_span( 506 | "Flamegraph", 507 | ViewKind::FlameGraph, 508 | self.app.flamegraph_state().view_kind, 509 | )); 510 | header_bottom_title_spans.push(Span::from(" | ")); 511 | header_bottom_title_spans.push(_get_view_kind_span( 512 | "Top", 513 | ViewKind::Table, 514 | self.app.flamegraph_state().view_kind, 515 | )); 516 | header_bottom_title_spans.push(Span::from(" ")); 517 | Line::from(header_bottom_title_spans) 518 | } 519 | 520 | fn get_version_indicator(&self) -> Line { 521 | Line::from(format!("flamelens v{}", env!("CARGO_PKG_VERSION"))) 522 | .style(Style::default().bold()) 523 | } 524 | 525 | fn get_header_text(&self, _width: u16) -> Line { 526 | let header_text = match &self.app.flamegraph_input { 527 | FlameGraphInput::File(path) => path.to_string(), 528 | FlameGraphInput::Pid(pid, info) => { 529 | let mut out = format!("Process: {}", pid); 530 | if let Some(info) = info { 531 | out += format!(" [{}]", info).as_str(); 532 | } 533 | #[cfg(feature = "python")] 534 | if let Some(state) = &self.app.sampler_state() { 535 | out += match state.status { 536 | SamplerStatus::Running => " [Running]".to_string(), 537 | _ => " [Exited]".to_string(), 538 | } 539 | .as_str(); 540 | let duration = state.total_sampled_duration; 541 | let seconds = duration.as_secs() % 60; 542 | let minutes = (duration.as_secs() / 60) % 60; 543 | let hours = (duration.as_secs() / 60) / 60; 544 | out += format!(" [Duration: {:0>2}:{:0>2}:{:0>2}]", hours, minutes, seconds) 545 | .as_str(); 546 | if self.app.flamegraph_state().freeze { 547 | out += " [Frozen; press 'z' again to unfreeze]"; 548 | } 549 | } 550 | out 551 | } 552 | }; 553 | Line::from(header_text).style(Style::default().bold()) 554 | } 555 | 556 | fn get_status_text(&self, width: u16) -> Vec<(&'static str, Line)> { 557 | if self.app.input_buffer.is_some() { 558 | self.get_status_text_buffer() 559 | } else { 560 | self.get_status_text_command(width) 561 | } 562 | } 563 | 564 | fn get_status_text_buffer(&self) -> Vec<(&'static str, Line)> { 565 | let input_buffer = self.app.input_buffer.as_ref().unwrap(); 566 | let status_text = format!("{}{}", SEARCH_PREFIX, input_buffer.buffer); 567 | vec![("Search", Line::from(status_text))] 568 | } 569 | 570 | fn get_cursor_position(&self, status_area: Rect) -> Option<(u16, u16)> { 571 | self.app.input_buffer.as_ref().map(|input_buffer| { 572 | ( 573 | (input_buffer.buffer.cursor() + SEARCH_PREFIX.len()) as u16, 574 | status_area.bottom().saturating_sub(1), 575 | ) 576 | }) 577 | } 578 | 579 | fn get_status_text_command(&self, width: u16) -> Vec<(&'static str, Line)> { 580 | let stack = self 581 | .app 582 | .flamegraph() 583 | .get_stack(&self.app.flamegraph_state().selected); 584 | let root_total_count = self.app.flamegraph().root().total_count; 585 | let mut lines = vec![]; 586 | match stack { 587 | Some(stack) => { 588 | let zoom_total_count = self.app.flamegraph_state().zoom.as_ref().map(|zoom| { 589 | self.app 590 | .flamegraph() 591 | .get_stack(&zoom.stack_id) 592 | .unwrap() 593 | .total_count 594 | }); 595 | if let Some(p) = &self.app.flamegraph_state().search_pattern { 596 | if let (true, Some(hit_coverage_count)) = 597 | (p.is_manual, self.app.flamegraph().hit_coverage_count()) 598 | { 599 | let mut match_text = format!( 600 | "\"{}\" {}", 601 | p.re.as_str(), 602 | FlamelensWidget::get_count_stats_str( 603 | None, 604 | hit_coverage_count, 605 | root_total_count, 606 | zoom_total_count, 607 | ) 608 | ); 609 | if self.is_table_view() 610 | && self 611 | .app 612 | .flamegraph() 613 | .ordered_stacks 614 | .search_pattern_ignored_because_of_no_match 615 | { 616 | match_text += " (no match; showing all)"; 617 | } 618 | let match_text = format!("{:width$}", match_text, width = width as usize,); 619 | lines.push(("Match", Line::from(match_text))); 620 | } 621 | } 622 | let selected_text = format!( 623 | "{} {}", 624 | self.app.flamegraph().get_stack_short_name_from_info(stack), 625 | FlamelensWidget::get_count_stats_str( 626 | None, 627 | stack.total_count, 628 | root_total_count, 629 | zoom_total_count 630 | ), 631 | ); 632 | let status_text = format!("{:width$}", selected_text, width = width as usize,); 633 | if self.is_flamegraph_view() { 634 | lines.push(("Selected", Line::from(status_text))); 635 | } 636 | if self.app.debug { 637 | let elapsed_str = format!( 638 | "Debug: {}", 639 | self.app 640 | .elapsed 641 | .iter() 642 | .map(|(k, v)| format!("{}:{:.2}ms", k, v.as_micros() as f64 / 1000.0)) 643 | .collect::>() 644 | .join(" ") 645 | ); 646 | lines.push(("Debug", Line::from(elapsed_str))); 647 | } 648 | if let Some(transient_message) = &self.app.transient_message { 649 | lines.push(("Info", Line::from(transient_message.as_str()))); 650 | } 651 | lines 652 | } 653 | None => vec![("Info", Line::from("No stack selected"))], 654 | } 655 | } 656 | 657 | fn get_count_stats_str( 658 | name: Option<&str>, 659 | count: u64, 660 | total_count: u64, 661 | zoomed_total_count: Option, 662 | ) -> String { 663 | format!( 664 | "[{}{} samples, {:.2}% of all{}]", 665 | name.map(|n| format!("{}: ", n)).unwrap_or_default(), 666 | count, 667 | (count as f64 / total_count as f64) * 100.0, 668 | if let Some(zoomed_total_count) = zoomed_total_count { 669 | format!( 670 | ", {:.2}% of zoomed", 671 | (count as f64 / zoomed_total_count as f64) * 100.0 672 | ) 673 | } else { 674 | "".to_string() 675 | } 676 | ) 677 | } 678 | 679 | fn view_kind(&self) -> ViewKind { 680 | self.app.flamegraph_state().view_kind 681 | } 682 | 683 | fn is_table_view(&self) -> bool { 684 | self.view_kind() == ViewKind::Table 685 | } 686 | 687 | fn is_flamegraph_view(&self) -> bool { 688 | self.view_kind() == ViewKind::FlameGraph 689 | } 690 | } 691 | 692 | struct HelpTags { 693 | tags: Vec<(&'static str, &'static str)>, 694 | default: Vec<(&'static str, &'static str)>, 695 | } 696 | 697 | impl HelpTags { 698 | fn new() -> Self { 699 | Self { 700 | tags: vec![], 701 | default: vec![("r", "reset"), ("tab", "switch view"), ("q", "quit")], 702 | } 703 | } 704 | 705 | fn add(&mut self, tag: &'static str, description: &'static str) { 706 | self.tags.push((tag, description)); 707 | } 708 | 709 | fn get_line(&self) -> Line<'static> { 710 | let mut spans = vec![Span::from(" ")]; 711 | for (tag, description) in self.tags.iter().chain(self.default.iter()) { 712 | spans.push(Span::from("[")); 713 | spans.push(Span::styled( 714 | *tag, 715 | Style::default().add_modifier(Modifier::BOLD).yellow(), 716 | )); 717 | spans.push(Span::from(format!(": {}", description))); 718 | spans.push(Span::from("] ")); 719 | } 720 | Line::from(spans) 721 | } 722 | } 723 | 724 | /// Renders the user interface widgets. 725 | pub fn render(app: &mut App, frame: &mut Frame) { 726 | // This is where you add new widgets. 727 | // See the following resources: 728 | // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html 729 | // - https://github.com/ratatui-org/ratatui/tree/master/examples 730 | let flamelens_widget = FlamelensWidget::new(app); 731 | let mut flamelens_state = FlamelensWidgetState::default(); 732 | frame.render_stateful_widget(flamelens_widget, frame.area(), &mut flamelens_state); 733 | app.flamegraph_view 734 | .set_frame_height(flamelens_state.frame_height); 735 | app.flamegraph_view 736 | .set_frame_width(flamelens_state.frame_width); 737 | app.add_elapsed("render", flamelens_state.render_time); 738 | if let Some(input_buffer) = &mut app.input_buffer { 739 | input_buffer.cursor = flamelens_state.cursor_position; 740 | } 741 | } 742 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use crate::{ 4 | flame::{FlameGraph, SearchPattern, SortColumn, StackIdentifier, StackInfo, ROOT_ID}, 5 | state::{FlameGraphState, ZoomState}, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct FlameGraphView { 10 | pub flamegraph: FlameGraph, 11 | pub state: FlameGraphState, 12 | pub updated_at: std::time::Instant, 13 | } 14 | 15 | impl FlameGraphView { 16 | pub fn new(flamegraph: FlameGraph) -> Self { 17 | Self { 18 | flamegraph, 19 | state: FlameGraphState::default(), 20 | updated_at: std::time::Instant::now(), 21 | } 22 | } 23 | 24 | pub fn select_id(&mut self, stack_id: &StackIdentifier) { 25 | self.state.select_id(stack_id); 26 | if let Some(p) = self.state.search_pattern.as_ref() { 27 | if p.is_manual { 28 | return; 29 | } 30 | } 31 | let pattern = self.flamegraph.get_stack_short_name(stack_id); 32 | if let Some(pattern) = pattern { 33 | let search_pattern = SearchPattern::new(pattern, false, false).unwrap(); 34 | self.set_search_pattern(search_pattern); 35 | } 36 | } 37 | 38 | pub fn replace_flamegraph(&mut self, mut new_flamegraph: FlameGraph) { 39 | self.state 40 | .handle_flamegraph_replacement(&self.flamegraph, &mut new_flamegraph); 41 | // Preserve the sort column 42 | new_flamegraph 43 | .ordered_stacks 44 | .set_sort_column(self.flamegraph.ordered_stacks.sorted_column); 45 | self.flamegraph = new_flamegraph; 46 | // Now the id in ZoomState points to the one in new flamegraph, but the ancestors and 47 | // descendants are not. Set the zoom again to update them. 48 | if let Some(zoom) = &self.state.zoom { 49 | self.set_zoom_for_id(zoom.stack_id); 50 | } 51 | self.updated_at = std::time::Instant::now(); 52 | } 53 | 54 | pub fn set_frame_height(&mut self, frame_height: u16) { 55 | self.state.frame_height = Some(frame_height); 56 | self.keep_selected_stack_in_view_port(); 57 | } 58 | 59 | pub fn set_frame_width(&mut self, frame_width: u16) { 60 | self.state.frame_width = Some(frame_width); 61 | } 62 | 63 | pub fn set_level_offset(&mut self, level_offset: usize) { 64 | let max_level_offset = self 65 | .flamegraph 66 | .get_num_levels() 67 | .saturating_sub(self.state.frame_height.unwrap_or(1) as usize); 68 | self.state.level_offset = min(level_offset, max_level_offset); 69 | } 70 | 71 | pub fn to_child_stack(&mut self) { 72 | if let Some(stack) = self.flamegraph.get_stack(&self.state.selected) { 73 | let mut children_stacks = stack 74 | .children 75 | .iter() 76 | .filter_map(|x| self.flamegraph.get_stack(x)) 77 | .collect::>(); 78 | // Visit the widest child first 79 | children_stacks.sort_by_key(|x| x.total_count); 80 | let mut selected_child = None; 81 | for child_stack in children_stacks.iter().rev() { 82 | if self.is_stack_visibly_wide(child_stack, None) { 83 | selected_child = Some(child_stack.id); 84 | if !self.is_stack_in_view_port(child_stack) { 85 | self.state.level_offset += 1; 86 | } 87 | break; 88 | } 89 | } 90 | if let Some(selected_child) = selected_child { 91 | self.select_id(&selected_child); 92 | } 93 | } else { 94 | self.state.select_root(); 95 | } 96 | } 97 | 98 | pub fn to_parent_stack(&mut self) { 99 | // TODO: maybe also check parent visibility to handle resizing / edge cases 100 | if let Some(parent) = self 101 | .flamegraph 102 | .get_stack(&self.state.selected) 103 | .map(|x| x.parent) 104 | { 105 | if let Some(parent) = parent { 106 | if let Some(parent_stack) = self.flamegraph.get_stack(&parent) { 107 | if !self.is_stack_in_view_port(parent_stack) { 108 | self.state.level_offset -= 1; 109 | } 110 | } 111 | self.select_id(&parent); 112 | } 113 | } else { 114 | self.state.select_root(); 115 | } 116 | } 117 | 118 | fn is_stack_in_view_port(&self, stack: &StackInfo) -> bool { 119 | if let Some(frame_height) = self.state.frame_height { 120 | let min_level = self.state.level_offset; 121 | let max_level = min_level + frame_height as usize - 1; 122 | min_level <= stack.level && stack.level <= max_level 123 | } else { 124 | true 125 | } 126 | } 127 | 128 | fn is_stack_visibly_wide(&self, stack: &StackInfo, zoom_factor: Option) -> bool { 129 | if let Some(frame_width) = self.state.frame_width { 130 | let mut expected_frame_width = stack.width_factor * frame_width as f64; 131 | if let Some(zoom_factor) = zoom_factor { 132 | // Use manually specified zoom factor as the descendants / ancentors logic are 133 | // handled by the caller 134 | expected_frame_width *= zoom_factor; 135 | } else if let Some(zoom) = &self.state.zoom { 136 | let adjusted_frame_width = expected_frame_width * zoom.zoom_factor; 137 | // Important: Must short circuit by checking the adjusted_frame_width >= 1.0 138 | // condition first because the is_ancestor_or_descendant check is expensive for very 139 | // deep call stacks. 140 | if adjusted_frame_width >= 1.0 && zoom.is_ancestor_or_descendant(&stack.id) { 141 | expected_frame_width = adjusted_frame_width; 142 | } else { 143 | return false; 144 | } 145 | } 146 | expected_frame_width >= 1.0 147 | } else { 148 | true 149 | } 150 | } 151 | 152 | fn select_stack_in_view_port(&mut self) { 153 | if let Some(stacks) = self.flamegraph.get_stacks_at_level(self.state.level_offset) { 154 | for stack_id in stacks { 155 | if let Some(stack) = self.flamegraph.get_stack(stack_id) { 156 | if self.is_stack_visibly_wide(stack, None) { 157 | self.state.select_id(stack_id); 158 | break; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | 165 | fn keep_selected_stack_in_view_port(&mut self) { 166 | if let Some(stack) = self.flamegraph.get_stack(&self.state.selected) { 167 | if !self.is_stack_in_view_port(stack) { 168 | self.select_stack_in_view_port(); 169 | } 170 | } 171 | } 172 | 173 | pub fn get_selected_stack(&self) -> Option<&StackInfo> { 174 | // TODO: refactor places to call this 175 | self.flamegraph.get_stack(&self.state.selected) 176 | } 177 | 178 | pub fn is_root_selected(&self) -> bool { 179 | self.state.selected == ROOT_ID 180 | } 181 | 182 | pub fn get_next_sibling(&self, stack_id: &StackIdentifier) -> Option { 183 | let stack = self.flamegraph.get_stack(stack_id)?; 184 | let level = self.flamegraph.get_stacks_at_level(stack.level)?; 185 | let level_idx = level.iter().position(|x| x == stack_id)?; 186 | for sibling_id in level[level_idx + 1..].iter() { 187 | if let Some(stack) = self.flamegraph.get_stack(sibling_id) { 188 | if self.is_stack_visibly_wide(stack, None) { 189 | return Some(sibling_id).cloned(); 190 | } 191 | } 192 | } 193 | None 194 | } 195 | 196 | pub fn get_previous_sibling(&self, stack_id: &StackIdentifier) -> Option { 197 | let stack = self.flamegraph.get_stack(stack_id)?; 198 | let level = self.flamegraph.get_stacks_at_level(stack.level)?; 199 | let level_idx = level.iter().position(|x| x == stack_id)?; 200 | for sibling_id in level[..level_idx].iter().rev() { 201 | if let Some(stack) = self.flamegraph.get_stack(sibling_id) { 202 | if self.is_stack_visibly_wide(stack, None) { 203 | return Some(sibling_id).cloned(); 204 | } 205 | } 206 | } 207 | None 208 | } 209 | 210 | /// Get number of visible levels in the flamegraph. This prevents scrolling far down to an 211 | /// offset with no visible stacks as they are all too tiny. 212 | pub fn get_num_visible_levels(&self) -> usize { 213 | // Scaling factor to apply 214 | let zoom_factor = self 215 | .state 216 | .zoom 217 | .as_ref() 218 | .map(|z| z.zoom_factor) 219 | .unwrap_or(1.0); 220 | 221 | // Count the number of unique levels that are visible 222 | let starting_stack_id = if let Some(zoom) = &self.state.zoom { 223 | zoom.stack_id 224 | } else { 225 | ROOT_ID 226 | }; 227 | self.flamegraph 228 | .get_descendants(&starting_stack_id) 229 | .iter() 230 | .filter_map(|id| self.flamegraph.get_stack(id)) 231 | .filter(|stack| self.is_stack_visibly_wide(stack, Some(zoom_factor))) 232 | .map(|stack| stack.level) 233 | .max() 234 | .map(|x| x + 1) // e.g. if max level is 0, there is 1 level 235 | .unwrap_or_else(|| self.flamegraph.get_num_levels()) 236 | } 237 | 238 | pub fn get_bottom_level_offset(&self) -> Option { 239 | self.state.frame_height.map(|frame_height| { 240 | self.get_num_visible_levels() 241 | .saturating_sub(frame_height as usize) 242 | }) 243 | } 244 | 245 | pub fn to_previous_sibling(&mut self) { 246 | if let Some(stack_id) = self.get_previous_sibling(&self.state.selected) { 247 | self.select_id(&stack_id) 248 | } 249 | } 250 | 251 | pub fn to_next_sibling(&mut self) { 252 | if let Some(stack_id) = self.get_next_sibling(&self.state.selected) { 253 | self.select_id(&stack_id) 254 | } 255 | } 256 | 257 | pub fn to_previous_search_result(&mut self) { 258 | if let Some(previous_id) = self.get_previous_hit() { 259 | self.select_id(&previous_id); 260 | self.scroll_to_selected(); 261 | } 262 | } 263 | 264 | pub fn to_next_search_result(&mut self) { 265 | if let Some(next_id) = self.get_next_hit() { 266 | self.select_id(&next_id); 267 | self.scroll_to_selected(); 268 | } 269 | } 270 | 271 | fn get_next_hit(&self) -> Option { 272 | // Nothing to do if not searching 273 | let _ = self.state.search_pattern.as_ref()?; 274 | 275 | // Get from the current level 276 | let selected_stack = self.flamegraph.get_stack(&self.state.selected)?; 277 | let level_stacks = self.flamegraph.get_stacks_at_level(selected_stack.level)?; 278 | let next_hit = self.get_next_hit_same_level(level_stacks.iter()); 279 | if next_hit.is_some() { 280 | return next_hit; 281 | } 282 | 283 | // Get from the next level 284 | self.flamegraph.hit_ids().and_then(|hit_ids| { 285 | hit_ids 286 | .iter() 287 | .filter_map(|x| self.flamegraph.get_stack(x)) 288 | .filter(|x| x.level > selected_stack.level && self.is_stack_visibly_wide(x, None)) 289 | .map(|x| x.id) 290 | .next() 291 | }) 292 | } 293 | 294 | pub fn get_previous_hit(&self) -> Option { 295 | // Nothing to do if not searching 296 | let _ = self.state.search_pattern.as_ref()?; 297 | 298 | // Get from the current level 299 | let selected_stack = self.flamegraph.get_stack(&self.state.selected)?; 300 | let level_stacks = self.flamegraph.get_stacks_at_level(selected_stack.level)?; 301 | let hit = self.get_next_hit_same_level(level_stacks.iter().rev()); 302 | if hit.is_some() { 303 | return hit; 304 | } 305 | 306 | // Get from the previous level 307 | self.flamegraph.hit_ids().and_then(|hit_ids| { 308 | hit_ids 309 | .iter() 310 | .rev() 311 | .filter_map(|x| self.flamegraph.get_stack(x)) 312 | .filter(|x| x.level < selected_stack.level && self.is_stack_visibly_wide(x, None)) 313 | .map(|x| x.id) 314 | .next() 315 | }) 316 | } 317 | 318 | fn get_next_hit_same_level<'a, I>(&self, level_stacks: I) -> Option 319 | where 320 | I: Iterator, 321 | { 322 | let same_level_candidates = level_stacks 323 | .filter_map(|x| self.flamegraph.get_stack(x)) 324 | .skip_while(|x| x.id != self.state.selected) 325 | .skip(1); // skip the selected stack 326 | same_level_candidates 327 | .filter(|x| x.hit) 328 | .find(|x| self.is_stack_visibly_wide(x, None)) 329 | .map(|x| x.id) 330 | } 331 | 332 | pub fn scroll_bottom(&mut self) { 333 | if let Some(bottom_offset) = self.get_bottom_level_offset() { 334 | self.state.level_offset = bottom_offset; 335 | self.keep_selected_stack_in_view_port(); 336 | } 337 | } 338 | 339 | pub fn scroll_top(&mut self) { 340 | self.state.level_offset = 0; 341 | self.keep_selected_stack_in_view_port(); 342 | } 343 | 344 | pub fn scroll_to_selected(&mut self) { 345 | if let Some(stack) = self.get_selected_stack() { 346 | if !self.is_stack_in_view_port(stack) { 347 | self.set_level_offset(stack.level); 348 | } 349 | } 350 | } 351 | 352 | pub fn page_down(&mut self) { 353 | if let (Some(frame_height), Some(bottom_offset)) = 354 | (self.state.frame_height, self.get_bottom_level_offset()) 355 | { 356 | self.set_level_offset(min( 357 | self.state.level_offset + frame_height as usize, 358 | bottom_offset, 359 | )); 360 | self.keep_selected_stack_in_view_port(); 361 | } 362 | } 363 | 364 | pub fn page_up(&mut self) { 365 | if let Some(frame_height) = self.state.frame_height { 366 | self.set_level_offset( 367 | self.state 368 | .level_offset 369 | .saturating_sub(frame_height as usize), 370 | ); 371 | self.keep_selected_stack_in_view_port(); 372 | } 373 | } 374 | 375 | pub fn set_zoom_for_id(&mut self, stack_id: StackIdentifier) { 376 | if let Some(selected_stack) = self.flamegraph.get_stack(&stack_id) { 377 | let zoom_factor = 378 | self.flamegraph.total_count() as f64 / selected_stack.total_count as f64; 379 | let ancestors = self.flamegraph.get_ancestors(&stack_id); 380 | let descendants = self.flamegraph.get_descendants(&stack_id); 381 | if stack_id == ROOT_ID { 382 | self.unset_zoom(); 383 | } else { 384 | let zoom = ZoomState { 385 | stack_id, 386 | zoom_factor, 387 | ancestors, 388 | descendants, 389 | }; 390 | self.state.set_zoom(zoom); 391 | } 392 | } 393 | } 394 | 395 | pub fn set_zoom(&mut self) { 396 | self.set_zoom_for_id(self.state.selected); 397 | } 398 | 399 | pub fn unset_zoom(&mut self) { 400 | if let Some(zoom_stack_id) = self.state.zoom.as_ref().map(|z| z.stack_id) { 401 | // Restore selected to previous zoom point 402 | self.select_id(&zoom_stack_id); 403 | } 404 | self.state.unset_zoom(); 405 | } 406 | 407 | pub fn set_search_pattern(&mut self, search_pattern: SearchPattern) { 408 | self.flamegraph.set_hits(&search_pattern); 409 | self.state.set_search_pattern(search_pattern); 410 | } 411 | 412 | pub fn unset_search_pattern(&mut self) { 413 | self.flamegraph.clear_hits(); 414 | self.state.unset_search_pattern(); 415 | } 416 | 417 | pub fn unset_manual_search_pattern(&mut self) { 418 | if let Some(p) = self.state.search_pattern.as_ref() { 419 | if p.is_manual { 420 | self.unset_search_pattern(); 421 | } 422 | } 423 | } 424 | 425 | pub fn reset(&mut self) { 426 | self.state.select_root(); 427 | self.state.level_offset = 0; 428 | self.state.unset_zoom(); 429 | self.state.table_state.reset(); 430 | self.unset_search_pattern(); 431 | } 432 | 433 | pub fn to_next_row(&mut self) { 434 | let new_value = min( 435 | self.state.table_state.selected.saturating_add(1), 436 | self.flamegraph.ordered_stacks.num_rows.saturating_sub(1), 437 | ); 438 | self.state.table_state.selected = new_value; 439 | } 440 | 441 | pub fn scroll_next_rows(&mut self) { 442 | let delta = self.state.frame_height.unwrap_or(10) as usize; 443 | let new_value = min( 444 | self.state.table_state.selected.saturating_add(delta), 445 | self.flamegraph.ordered_stacks.num_rows.saturating_sub(1), 446 | ); 447 | self.state.table_state.selected = new_value; 448 | self.state.table_state.offset = new_value; 449 | } 450 | 451 | pub fn to_previous_row(&mut self) { 452 | let new_value = self.state.table_state.selected.saturating_sub(1); 453 | self.state.table_state.selected = new_value; 454 | } 455 | 456 | pub fn scroll_previous_rows(&mut self) { 457 | let delta = self.state.frame_height.unwrap_or(10) as usize; 458 | let new_value = self.state.table_state.selected.saturating_sub(delta); 459 | self.state.table_state.selected = new_value; 460 | self.state.table_state.offset = new_value; 461 | } 462 | 463 | pub fn set_sort_by_own(&mut self) { 464 | self.flamegraph 465 | .ordered_stacks 466 | .set_sort_column(SortColumn::Own); 467 | } 468 | 469 | pub fn set_sort_by_total(&mut self) { 470 | self.flamegraph 471 | .ordered_stacks 472 | .set_sort_column(SortColumn::Total); 473 | } 474 | 475 | pub fn get_selected_row_name(&mut self) -> Option<&str> { 476 | self.flamegraph 477 | .ordered_stacks 478 | .entries 479 | .get(self.state.table_state.selected) 480 | .map(|x| x.name.as_str()) 481 | } 482 | } 483 | 484 | #[cfg(test)] 485 | mod tests { 486 | use crate::flame::ROOT_ID; 487 | 488 | use super::*; 489 | 490 | fn get_id(view: &FlameGraphView, full_name: &str) -> StackIdentifier { 491 | view.flamegraph 492 | .get_stack_by_full_name(full_name) 493 | .unwrap() 494 | .id 495 | } 496 | 497 | fn get_selected_short_name(view: &FlameGraphView) -> &str { 498 | view.flamegraph 499 | .get_stack_short_name(&view.state.selected) 500 | .unwrap() 501 | } 502 | 503 | #[test] 504 | fn test_get_next_sibling() { 505 | let content = std::fs::read_to_string("tests/data/py-spy-simple.txt").unwrap(); 506 | let fg = FlameGraph::from_string(content, true); 507 | let view = FlameGraphView::new(fg); 508 | 509 | let result = view.get_next_sibling(&ROOT_ID); 510 | assert_eq!(result, None); 511 | 512 | let result = view.get_next_sibling(&get_id(&view, " (long_running.py:25)")); 513 | assert_eq!( 514 | result.unwrap(), 515 | get_id(&view, " (long_running.py:24)") 516 | ); 517 | 518 | let result = view.get_next_sibling(&get_id( 519 | &view, 520 | " (long_running.py:25);work (long_running.py:7)", 521 | )); 522 | assert_eq!( 523 | result.unwrap(), 524 | get_id( 525 | &view, 526 | " (long_running.py:24);quick_work (long_running.py:17)" 527 | ), 528 | ); 529 | } 530 | 531 | #[test] 532 | fn test_get_previous_sibling() { 533 | let content = std::fs::read_to_string("tests/data/py-spy-simple.txt").unwrap(); 534 | let fg = FlameGraph::from_string(content, true); 535 | let view = FlameGraphView::new(fg); 536 | 537 | let result = view.get_previous_sibling(&ROOT_ID); 538 | assert_eq!(result, None); 539 | 540 | let result = view.get_previous_sibling(&get_id(&view, " (long_running.py:24)")); 541 | assert_eq!( 542 | result.unwrap(), 543 | get_id(&view, " (long_running.py:25)") 544 | ); 545 | 546 | let result = view.get_previous_sibling(&get_id( 547 | &view, 548 | " (long_running.py:24);quick_work (long_running.py:17)", 549 | )); 550 | assert_eq!( 551 | result.unwrap(), 552 | get_id( 553 | &view, 554 | " (long_running.py:25);work (long_running.py:7)", 555 | ), 556 | ); 557 | } 558 | 559 | #[test] 560 | fn test_get_next_and_previous_search_result() { 561 | let content = std::fs::read_to_string("tests/data/readable.txt").unwrap(); 562 | let fg = FlameGraph::from_string(content, false); 563 | 564 | // No-op if no search pattern 565 | let mut view = FlameGraphView::new(fg); 566 | view.to_next_search_result(); 567 | view.to_previous_search_result(); 568 | assert_eq!(get_selected_short_name(&view), "all"); 569 | 570 | // Set a search pattern 571 | view.set_search_pattern( 572 | SearchPattern::new("1-b$|2-a$|2-c$|2-e$", true, true) 573 | .expect("Could not create search pattern"), 574 | ); 575 | assert_eq!(get_selected_short_name(&view), "all"); 576 | 577 | // Check going to the next search result 578 | view.to_next_search_result(); 579 | assert_eq!(get_selected_short_name(&view), "level1-b"); 580 | 581 | view.to_next_search_result(); 582 | assert_eq!(get_selected_short_name(&view), "level2-a"); 583 | 584 | view.to_next_search_result(); 585 | assert_eq!(get_selected_short_name(&view), "level2-c"); 586 | 587 | view.to_next_search_result(); 588 | assert_eq!(get_selected_short_name(&view), "level2-e"); 589 | 590 | view.to_next_search_result(); 591 | assert_eq!(get_selected_short_name(&view), "level2-e"); 592 | 593 | // Check going to the previous search result 594 | view.to_previous_search_result(); 595 | assert_eq!(get_selected_short_name(&view), "level2-c"); 596 | 597 | view.to_previous_search_result(); 598 | assert_eq!(get_selected_short_name(&view), "level2-a"); 599 | 600 | view.to_previous_search_result(); 601 | assert_eq!(get_selected_short_name(&view), "level1-b"); 602 | 603 | view.to_previous_search_result(); 604 | assert_eq!(get_selected_short_name(&view), "level1-b"); 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /tests/data/ignore-metadata-lines.txt: -------------------------------------------------------------------------------- 1 | 2 | # some metadata for human: 42 3 | 4 | (long_running.py:24) 7 -------------------------------------------------------------------------------- /tests/data/invalid-lines.txt: -------------------------------------------------------------------------------- 1 | (long_running.py:24) 7 2 | 123 3 | (long_running.py:123123123) 4 | (long_running.py:25) 421 5 | nothing to see here -------------------------------------------------------------------------------- /tests/data/py-spy-simple.txt: -------------------------------------------------------------------------------- 1 | (long_running.py:24);quick_work (long_running.py:16) 7 2 | (long_running.py:25);work (long_running.py:8) 421 3 | (long_running.py:26) 1 4 | (long_running.py:25);work (long_running.py:7) 218 5 | (long_running.py:24);quick_work (long_running.py:17) 10 -------------------------------------------------------------------------------- /tests/data/readable.txt: -------------------------------------------------------------------------------- 1 | # ---------- level1-a ---------- | ---- level1-b ----- | 2 | # 3 | # level2-a | level2-b | level2-c | level2-d | level2-e | 4 | 5 | level1-a;level2-a 10 6 | level1-a;level2-b 20 7 | level1-a;level2-c 10 8 | level1-b;level2-d 10 9 | level1-b;level2-e 20 -------------------------------------------------------------------------------- /tests/fixtures/ignore-metadata-lines/expected_ordered_counts.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "name": " (long_running.py:24)", 5 | "count": { 6 | "total": 7, 7 | "own": 7 8 | }, 9 | "visible": true 10 | } 11 | ], 12 | "num_rows": 1, 13 | "sorted_column": "Own", 14 | "search_pattern_ignored_because_of_no_match": false 15 | } -------------------------------------------------------------------------------- /tests/fixtures/ignore-metadata-lines/expected_stacks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "line_index": 0, 5 | "start_index": 0, 6 | "end_index": 0, 7 | "total_count": 7, 8 | "self_count": 0, 9 | "parent": null, 10 | "children": [ 11 | 1 12 | ], 13 | "level": 0, 14 | "width_factor": 1.0, 15 | "hit": false, 16 | "short_name": "all", 17 | "full_name": "all" 18 | }, 19 | { 20 | "id": 1, 21 | "line_index": 32, 22 | "start_index": 32, 23 | "end_index": 61, 24 | "total_count": 7, 25 | "self_count": 7, 26 | "parent": 0, 27 | "children": [], 28 | "level": 1, 29 | "width_factor": 1.0, 30 | "hit": false, 31 | "short_name": " (long_running.py:24)", 32 | "full_name": " (long_running.py:24)" 33 | } 34 | ] -------------------------------------------------------------------------------- /tests/fixtures/invalid-lines/expected_ordered_counts.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "name": " (long_running.py:25)", 5 | "count": { 6 | "total": 421, 7 | "own": 421 8 | }, 9 | "visible": true 10 | }, 11 | { 12 | "name": " (long_running.py:24)", 13 | "count": { 14 | "total": 7, 15 | "own": 7 16 | }, 17 | "visible": true 18 | } 19 | ], 20 | "num_rows": 2, 21 | "sorted_column": "Own", 22 | "search_pattern_ignored_because_of_no_match": false 23 | } -------------------------------------------------------------------------------- /tests/fixtures/invalid-lines/expected_stacks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "line_index": 0, 5 | "start_index": 0, 6 | "end_index": 0, 7 | "total_count": 428, 8 | "self_count": 0, 9 | "parent": null, 10 | "children": [ 11 | 2, 12 | 1 13 | ], 14 | "level": 0, 15 | "width_factor": 1.0, 16 | "hit": false, 17 | "short_name": "all", 18 | "full_name": "all" 19 | }, 20 | { 21 | "id": 1, 22 | "line_index": 0, 23 | "start_index": 0, 24 | "end_index": 29, 25 | "total_count": 7, 26 | "self_count": 7, 27 | "parent": 0, 28 | "children": [], 29 | "level": 1, 30 | "width_factor": 0.016355140186915886, 31 | "hit": false, 32 | "short_name": " (long_running.py:24)", 33 | "full_name": " (long_running.py:24)" 34 | }, 35 | { 36 | "id": 2, 37 | "line_index": 74, 38 | "start_index": 74, 39 | "end_index": 103, 40 | "total_count": 421, 41 | "self_count": 421, 42 | "parent": 0, 43 | "children": [], 44 | "level": 1, 45 | "width_factor": 0.9836448598130841, 46 | "hit": false, 47 | "short_name": " (long_running.py:25)", 48 | "full_name": " (long_running.py:25)" 49 | } 50 | ] -------------------------------------------------------------------------------- /tests/fixtures/py-spy-simple/expected_ordered_counts.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "name": "work (long_running.py:8)", 5 | "count": { 6 | "total": 421, 7 | "own": 421 8 | }, 9 | "visible": true 10 | }, 11 | { 12 | "name": "work (long_running.py:7)", 13 | "count": { 14 | "total": 218, 15 | "own": 218 16 | }, 17 | "visible": true 18 | }, 19 | { 20 | "name": "quick_work (long_running.py:17)", 21 | "count": { 22 | "total": 10, 23 | "own": 10 24 | }, 25 | "visible": true 26 | }, 27 | { 28 | "name": "quick_work (long_running.py:16)", 29 | "count": { 30 | "total": 7, 31 | "own": 7 32 | }, 33 | "visible": true 34 | }, 35 | { 36 | "name": " (long_running.py:26)", 37 | "count": { 38 | "total": 1, 39 | "own": 1 40 | }, 41 | "visible": true 42 | }, 43 | { 44 | "name": " (long_running.py:25)", 45 | "count": { 46 | "total": 639, 47 | "own": 0 48 | }, 49 | "visible": true 50 | }, 51 | { 52 | "name": " (long_running.py:24)", 53 | "count": { 54 | "total": 17, 55 | "own": 0 56 | }, 57 | "visible": true 58 | } 59 | ], 60 | "num_rows": 7, 61 | "sorted_column": "Own", 62 | "search_pattern_ignored_because_of_no_match": false 63 | } -------------------------------------------------------------------------------- /tests/fixtures/py-spy-simple/expected_stacks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "line_index": 0, 5 | "start_index": 0, 6 | "end_index": 0, 7 | "total_count": 657, 8 | "self_count": 0, 9 | "parent": null, 10 | "children": [ 11 | 3, 12 | 1, 13 | 5 14 | ], 15 | "level": 0, 16 | "width_factor": 1.0, 17 | "hit": false, 18 | "short_name": "all", 19 | "full_name": "all" 20 | }, 21 | { 22 | "id": 1, 23 | "line_index": 0, 24 | "start_index": 0, 25 | "end_index": 29, 26 | "total_count": 17, 27 | "self_count": 0, 28 | "parent": 0, 29 | "children": [ 30 | 7, 31 | 2 32 | ], 33 | "level": 1, 34 | "width_factor": 0.0258751902587519, 35 | "hit": false, 36 | "short_name": " (long_running.py:24)", 37 | "full_name": " (long_running.py:24)" 38 | }, 39 | { 40 | "id": 2, 41 | "line_index": 0, 42 | "start_index": 30, 43 | "end_index": 61, 44 | "total_count": 7, 45 | "self_count": 7, 46 | "parent": 1, 47 | "children": [], 48 | "level": 2, 49 | "width_factor": 0.0106544901065449, 50 | "hit": false, 51 | "short_name": "quick_work (long_running.py:16)", 52 | "full_name": " (long_running.py:24);quick_work (long_running.py:16)" 53 | }, 54 | { 55 | "id": 3, 56 | "line_index": 64, 57 | "start_index": 64, 58 | "end_index": 93, 59 | "total_count": 639, 60 | "self_count": 0, 61 | "parent": 0, 62 | "children": [ 63 | 4, 64 | 6 65 | ], 66 | "level": 1, 67 | "width_factor": 0.9726027397260274, 68 | "hit": false, 69 | "short_name": " (long_running.py:25)", 70 | "full_name": " (long_running.py:25)" 71 | }, 72 | { 73 | "id": 4, 74 | "line_index": 64, 75 | "start_index": 94, 76 | "end_index": 118, 77 | "total_count": 421, 78 | "self_count": 421, 79 | "parent": 3, 80 | "children": [], 81 | "level": 2, 82 | "width_factor": 0.6407914764079147, 83 | "hit": false, 84 | "short_name": "work (long_running.py:8)", 85 | "full_name": " (long_running.py:25);work (long_running.py:8)" 86 | }, 87 | { 88 | "id": 5, 89 | "line_index": 123, 90 | "start_index": 123, 91 | "end_index": 152, 92 | "total_count": 1, 93 | "self_count": 1, 94 | "parent": 0, 95 | "children": [], 96 | "level": 1, 97 | "width_factor": 0.0015220700152207, 98 | "hit": false, 99 | "short_name": " (long_running.py:26)", 100 | "full_name": " (long_running.py:26)" 101 | }, 102 | { 103 | "id": 6, 104 | "line_index": 155, 105 | "start_index": 185, 106 | "end_index": 209, 107 | "total_count": 218, 108 | "self_count": 218, 109 | "parent": 3, 110 | "children": [], 111 | "level": 2, 112 | "width_factor": 0.3318112633181126, 113 | "hit": false, 114 | "short_name": "work (long_running.py:7)", 115 | "full_name": " (long_running.py:25);work (long_running.py:7)" 116 | }, 117 | { 118 | "id": 7, 119 | "line_index": 214, 120 | "start_index": 244, 121 | "end_index": 275, 122 | "total_count": 10, 123 | "self_count": 10, 124 | "parent": 1, 125 | "children": [], 126 | "level": 2, 127 | "width_factor": 0.015220700152207, 128 | "hit": false, 129 | "short_name": "quick_work (long_running.py:17)", 130 | "full_name": " (long_running.py:24);quick_work (long_running.py:17)" 131 | } 132 | ] -------------------------------------------------------------------------------- /tests/fixtures/recursive/expected_ordered_counts.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "name": "deep_work (long_running.py:26)", 5 | "count": { 6 | "total": 109, 7 | "own": 109 8 | }, 9 | "visible": true 10 | }, 11 | { 12 | "name": "deep_work (long_running.py:27)", 13 | "count": { 14 | "total": 72, 15 | "own": 72 16 | }, 17 | "visible": true 18 | }, 19 | { 20 | "name": "work (long_running.py:8)", 21 | "count": { 22 | "total": 5, 23 | "own": 5 24 | }, 25 | "visible": true 26 | }, 27 | { 28 | "name": "work (long_running.py:7)", 29 | "count": { 30 | "total": 4, 31 | "own": 4 32 | }, 33 | "visible": true 34 | }, 35 | { 36 | "name": "deep_work (long_running.py:29)", 37 | "count": { 38 | "total": 9, 39 | "own": 0 40 | }, 41 | "visible": true 42 | }, 43 | { 44 | "name": "deep_work (long_running.py:28)", 45 | "count": { 46 | "total": 190, 47 | "own": 0 48 | }, 49 | "visible": true 50 | }, 51 | { 52 | "name": " (long_running.py:36)", 53 | "count": { 54 | "total": 190, 55 | "own": 0 56 | }, 57 | "visible": true 58 | } 59 | ], 60 | "num_rows": 7, 61 | "sorted_column": "Own", 62 | "search_pattern_ignored_because_of_no_match": false 63 | } -------------------------------------------------------------------------------- /tests/python/long_running.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | 5 | PID = None 6 | 7 | 8 | def get_pid(): 9 | global PID 10 | if PID is None: 11 | PID = os.getpid() 12 | return PID 13 | 14 | 15 | def log(message): 16 | print("[PID: {}] {}".format(get_pid(), message)) 17 | 18 | 19 | def work(): 20 | log("Starting work") 21 | i = 0 22 | while i < 10_000_000: 23 | i += 1 24 | log("Done work") 25 | return i 26 | 27 | 28 | def quick_work(): 29 | log("Starting quick work") 30 | i = 0 31 | while i < 100_000: 32 | i += 1 33 | log("Done quick work") 34 | return i 35 | 36 | 37 | def deep_work(n): 38 | log("Starting deep work") 39 | if n > 0: 40 | i = 0 41 | while i < 10_000 * n: 42 | i += 1 43 | return deep_work(n - 1) 44 | work() 45 | 46 | 47 | if __name__ == '__main__': 48 | while True: 49 | quick_work() 50 | work() 51 | deep_work(100) 52 | time.sleep(0.1) --------------------------------------------------------------------------------