├── .bumpversion.cfg
├── .flake8
├── .github
└── workflows
│ ├── lint.yaml
│ ├── pytest.yaml
│ └── release.yaml
├── .gitignore
├── README.md
├── docs
└── flameshow.gif
├── flake.lock
├── flake.nix
├── flameshow
├── __init__.py
├── colors.py
├── const.py
├── exceptions.py
├── main.py
├── models.py
├── parsers
│ ├── __init__.py
│ └── stackcollapse_parser.py
├── pprof_parser
│ ├── __init__.py
│ ├── parser.py
│ └── profile_pb2.py
├── render
│ ├── __init__.py
│ ├── app.py
│ ├── flamegraph.py
│ ├── framedetail.py
│ ├── header.py
│ └── tabs.py
├── runtime.py
└── utils.py
├── makefile
├── poetry.lock
├── proto
└── profile.proto
├── pyproject.toml
└── tests
├── __init__.py
├── conftest.py
├── pprof_data
├── goroutine.out
├── goroutine_frametree.json
├── heap.out
├── profile-10seconds.out
├── profile10s_frametree.json
├── profile10s_node_exporter.json
└── sample_location_line_multiple.json
├── stackcollapse_data
├── flameshow-pyspy-dump.txt
├── perf-vertx-stacks-01-collapsed-all.txt
├── simple.txt
├── simple_square.txt
└── with_comment.txt
├── test_cli_click.py
├── test_colors.py
├── test_integration
└── test_app.py
├── test_main.py
├── test_models.py
├── test_pprof_parse
├── __init__.py
└── test_golang_pprof.py
├── test_profile_parser.py
├── test_render
├── __init__.py
├── test_app.py
├── test_flamegraph.py
├── test_render_detail.py
└── test_render_header.py
├── test_stackcollapse_parse
├── __init__.py
├── test_model.py
└── test_parser.py
├── test_utils.py
└── utils.py
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.1.4
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:pyproject.toml]
7 | search = version = "{current_version}"
8 | replace = version = "{new_version}"
9 |
10 | [bumpversion:file:flameshow/__init__.py]
11 | search = __version__ = "{current_version}"
12 | replace = __version__ = "{new_version}"
13 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | __pycache__,
5 | dist,
6 | ./flameshow/pprof_parser/profile_pb2.py,
7 | tests/
8 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | flake8:
11 | name: flake8
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Cache venv
17 | uses: actions/cache@v2
18 | with:
19 | path: venv
20 | key: lintenv-flake8
21 |
22 | - name: Install Dependencies
23 | run: |
24 | python3 -m venv venv
25 | . venv/bin/activate
26 | pip install -U pip flake8
27 |
28 | - name: Flake8 test
29 | run: |
30 | . venv/bin/activate
31 | flake8 flameshow
32 |
33 | spell:
34 | name: Spell Check
35 | runs-on: ubuntu-latest
36 |
37 | steps:
38 | - uses: actions/checkout@v4
39 |
40 | - uses: codespell-project/actions-codespell@v2
41 | with:
42 | check_filenames: true
43 | skip: "*.log,./tests/pprof_data/*,poetry.lock,./tests/stackcollapse_data"
44 | ignore_words_list: buildin
45 |
46 | - uses: actions/setup-python@v4
47 | with:
48 | ignore_words_list: hello,world
49 | python-version: "3.10"
50 | architecture: "x64"
51 |
52 | black:
53 | name: black
54 | runs-on: ubuntu-latest
55 |
56 | steps:
57 | - uses: actions/checkout@v4
58 | - name: Cache venv
59 | uses: actions/cache@v2
60 | with:
61 | path: venv
62 | key: lintenv-black
63 |
64 | - name: Install Dependencies
65 | run: |
66 | python3 -m venv venv
67 | . venv/bin/activate
68 | pip install -U pip black
69 |
70 | - name: Black test
71 | run: |
72 | . venv/bin/activate
73 | black --check --diff .
74 |
--------------------------------------------------------------------------------
/.github/workflows/pytest.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | # os: [ubuntu-latest, macos-latest, windows-latest]
15 | os: [ubuntu-latest, macos-latest]
16 | # python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
17 | python-version: ["3.10", "3.11", "3.12"]
18 |
19 | exclude:
20 | # see https://github.com/actions/setup-python/issues/948
21 | - os: macos-latest
22 | python-version: "3.10"
23 | # see https://github.com/actions/setup-python/issues/948
24 | - os: macos-latest
25 | python-version: "3.11"
26 |
27 | # https://github.com/python/typing_extensions/issues/377
28 | - os: macos-latest
29 | python-version: "3.12"
30 | defaults:
31 | run:
32 | shell: bash
33 | steps:
34 | - uses: actions/checkout@v3.5.2
35 | - name: Install and configure Poetry # This could be cached, too...
36 | uses: snok/install-poetry@v1.3.3
37 | with:
38 | version: 1.4.2
39 | virtualenvs-in-project: true
40 | - name: Set up Python ${{ matrix.python-version }}
41 | uses: actions/setup-python@v5
42 | with:
43 | python-version: ${{ matrix.python-version }}
44 | architecture: x64
45 | allow-prereleases: true
46 | - name: Load cached venv
47 | id: cached-poetry-dependencies
48 | uses: actions/cache@v3
49 | with:
50 | path: .venv
51 | key:
52 | venv-${{ runner.os }}-${{ matrix.python-version }}-${{
53 | hashFiles('**/poetry.lock') }}
54 | - name: Install dependencies
55 | run: poetry install
56 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
57 |
58 | - name: Test with pytest
59 | run: |
60 | source $VENV
61 | pytest tests -v --cov=./flameshow --cov-report=xml:./coverage.xml --cov-report term-missing
62 |
63 | - name: Upload coverage reports to Codecov
64 | uses: codecov/codecov-action@v3
65 | env:
66 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
67 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release-pypi:
10 | name: release-pypi
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-python@v4
16 | - name: Install Dependencies
17 | run: |
18 | python3 -m venv venv
19 | . venv/bin/activate
20 | pip install -U pip
21 | pip install poetry
22 | poetry install
23 | python -c "import sys; print(sys.version)"
24 | pip list
25 |
26 | - name: Poetry Build
27 | run: |
28 | . venv/bin/activate
29 | poetry build
30 |
31 | - name: Test Build
32 | run: |
33 | python3 -m venv fresh_env
34 | . fresh_env/bin/activate
35 | pip install dist/*.whl
36 |
37 | flameshow --version
38 |
39 | - name: Upload to Pypi
40 | env:
41 | PASSWORD: ${{ secrets.FLAMESHOW_PYPI_TOKEN }}
42 | run: |
43 | . venv/bin/activate
44 | poetry publish --username __token__ --password ${PASSWORD}
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 | *.log
162 | goroutine.json
163 | debug/
164 | flameshow/pprof_parser/_libpprofparser.cpython-310-darwin.h
165 | *.svg
166 | *.out
167 | flameshow/pprof_parser/_libpprofparser*
168 | *.tar.gz
169 | a.json
170 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flameshow
2 |
3 | [](https://github.com/laixintao/flameshow/actions/workflows/pytest.yaml)
4 | [](https://codecov.io/gh/laixintao/flameshow)
5 | [](https://pypi.org/project/flameshow/)
6 | 
7 | 
8 | [](https://github.com/psf/black)
9 |
10 | Flameshow is a terminal Flamegraph viewer.
11 |
12 | 
13 |
14 | ## Features
15 |
16 | - Renders Flamegraphs in your terminal
17 | - Supports zooming in and displaying percentages
18 | - Keyboard input is prioritized
19 | - All operations can also be performed using the mouse.
20 | - Can switch to different sample types
21 |
22 | ## Install
23 |
24 | Flameshow is written in pure Python, so you can install via `pip`:
25 |
26 | ```shell
27 | pip install flameshow
28 | ```
29 |
30 | But you can also run it through [nix](https://nixos.org/):
31 |
32 | ```shell
33 | nix run github:laixintao/flameshow
34 | # Or if you want to install it imperatively:
35 | nix profile install github:laixintao/flameshow
36 | ```
37 |
38 | ## Usage
39 |
40 | View golang's goroutine dump:
41 |
42 | ```shell
43 | $ curl http://localhost:9100/debug/pprof/goroutine -o goroutine.out
44 | $ flameshow goroutine.out
45 | ```
46 |
47 | After entering the TUI, the available actions are listed on Footer:
48 |
49 | - q for quit
50 | - h j k l or ←
51 | ↓ ↑ → for moving around, and Enter
52 | for zoom in, then Esc for zoom out.
53 | - You can also use a mouse, hover on a span will show it details, and click will
54 | zoom it.
55 |
56 | ## Supported Formats
57 |
58 | As far as I know, there is no standard specification for profiles. Different
59 | languages or tools might generate varying profile formats. I'm actively working
60 | on supporting more formats. Admittedly, I might not be familiar with every tool
61 | and its specific format. So, if you'd like Flameshow to integrate with a tool
62 | you love, please feel free to reach out and submit an issue.
63 |
64 | - Golang pprof
65 | - [Brendan Gregg's Flamegraph](https://www.brendangregg.com/flamegraphs.html)
66 | - Python [Austin](https://github.com/P403n1x87/austin)
67 |
68 | ## Development
69 |
70 | If you want to dive into the code and make some changes, start with:
71 |
72 | ```shell
73 | git clone git@github.com:laixintao/flameshow.git
74 | cd flameshow
75 | pip install poetry
76 | poetry install
77 | ```
78 |
79 | ---
80 |
81 | This project is proudly powered by
82 | [textual](https://github.com/Textualize/textual).
83 |
--------------------------------------------------------------------------------
/docs/flameshow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/docs/flameshow.gif
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1701680307,
9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "flake-utils_2": {
22 | "inputs": {
23 | "systems": "systems_2"
24 | },
25 | "locked": {
26 | "lastModified": 1694529238,
27 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
28 | "owner": "numtide",
29 | "repo": "flake-utils",
30 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
31 | "type": "github"
32 | },
33 | "original": {
34 | "owner": "numtide",
35 | "repo": "flake-utils",
36 | "type": "github"
37 | }
38 | },
39 | "nix-github-actions": {
40 | "inputs": {
41 | "nixpkgs": [
42 | "poetry2nix",
43 | "nixpkgs"
44 | ]
45 | },
46 | "locked": {
47 | "lastModified": 1698974481,
48 | "narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
49 | "owner": "nix-community",
50 | "repo": "nix-github-actions",
51 | "rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
52 | "type": "github"
53 | },
54 | "original": {
55 | "owner": "nix-community",
56 | "repo": "nix-github-actions",
57 | "type": "github"
58 | }
59 | },
60 | "nixpkgs": {
61 | "locked": {
62 | "lastModified": 1704626572,
63 | "narHash": "sha256-VwRTEKzK4wSSv64G+g3RLF3t6yBHrhR2VK3kZ5UWisU=",
64 | "owner": "NixOS",
65 | "repo": "nixpkgs",
66 | "rev": "24fe8bb4f552ad3926274d29e083b79d84707da6",
67 | "type": "github"
68 | },
69 | "original": {
70 | "id": "nixpkgs",
71 | "ref": "nixpkgs-unstable",
72 | "type": "indirect"
73 | }
74 | },
75 | "poetry2nix": {
76 | "inputs": {
77 | "flake-utils": "flake-utils_2",
78 | "nix-github-actions": "nix-github-actions",
79 | "nixpkgs": [
80 | "nixpkgs"
81 | ],
82 | "systems": "systems_3",
83 | "treefmt-nix": "treefmt-nix"
84 | },
85 | "locked": {
86 | "lastModified": 1704540236,
87 | "narHash": "sha256-VKQ7JUjINd34sYhH7DKTtqnARvRySJ808cW9hoYA8NQ=",
88 | "owner": "nix-community",
89 | "repo": "poetry2nix",
90 | "rev": "74921da7e0cc8918adc2e9989bd3e9c127b25ff6",
91 | "type": "github"
92 | },
93 | "original": {
94 | "owner": "nix-community",
95 | "repo": "poetry2nix",
96 | "type": "github"
97 | }
98 | },
99 | "root": {
100 | "inputs": {
101 | "flake-utils": "flake-utils",
102 | "nixpkgs": "nixpkgs",
103 | "poetry2nix": "poetry2nix"
104 | }
105 | },
106 | "systems": {
107 | "locked": {
108 | "lastModified": 1681028828,
109 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
110 | "owner": "nix-systems",
111 | "repo": "default",
112 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
113 | "type": "github"
114 | },
115 | "original": {
116 | "owner": "nix-systems",
117 | "repo": "default",
118 | "type": "github"
119 | }
120 | },
121 | "systems_2": {
122 | "locked": {
123 | "lastModified": 1681028828,
124 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
125 | "owner": "nix-systems",
126 | "repo": "default",
127 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
128 | "type": "github"
129 | },
130 | "original": {
131 | "owner": "nix-systems",
132 | "repo": "default",
133 | "type": "github"
134 | }
135 | },
136 | "systems_3": {
137 | "locked": {
138 | "lastModified": 1681028828,
139 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
140 | "owner": "nix-systems",
141 | "repo": "default",
142 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
143 | "type": "github"
144 | },
145 | "original": {
146 | "id": "systems",
147 | "type": "indirect"
148 | }
149 | },
150 | "treefmt-nix": {
151 | "inputs": {
152 | "nixpkgs": [
153 | "poetry2nix",
154 | "nixpkgs"
155 | ]
156 | },
157 | "locked": {
158 | "lastModified": 1699786194,
159 | "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
160 | "owner": "numtide",
161 | "repo": "treefmt-nix",
162 | "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
163 | "type": "github"
164 | },
165 | "original": {
166 | "owner": "numtide",
167 | "repo": "treefmt-nix",
168 | "type": "github"
169 | }
170 | }
171 | },
172 | "root": "root",
173 | "version": 7
174 | }
175 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A terminal Flamegraph viewer.";
3 |
4 | inputs = {
5 | nixpkgs.url = "nixpkgs/nixpkgs-unstable";
6 | flake-utils.url = "github:numtide/flake-utils";
7 | poetry2nix = {
8 | url = "github:nix-community/poetry2nix";
9 | inputs.nixpkgs.follows = "nixpkgs";
10 | };
11 | };
12 |
13 | outputs = { self, nixpkgs, flake-utils, poetry2nix }:
14 | flake-utils.lib.eachDefaultSystem (system: let
15 | pkgs = import nixpkgs { inherit system; };
16 | inherit (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; }) mkPoetryApplication;
17 | in {
18 | packages = {
19 | flameshow = mkPoetryApplication { projectDir = self; };
20 | default = self.packages.${system}.flameshow;
21 | };
22 |
23 | devShells.default = pkgs.mkShell {
24 | packages =
25 | (with pkgs; [ nil python3 mypy ruff poetry ])
26 | ++
27 | (with pkgs.python311Packages; [ pip python-lsp-server pylsp-mypy python-lsp-ruff ]);
28 | };
29 | }
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/flameshow/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.1.4"
2 |
--------------------------------------------------------------------------------
/flameshow/colors.py:
--------------------------------------------------------------------------------
1 | import random
2 | import logging
3 |
4 | from textual.color import Color
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | class ColorPlatteBase:
10 | def __init__(self):
11 | self.assigned_color = {}
12 |
13 | def get_color(self, key):
14 | if key not in self.assigned_color:
15 | self.assigned_color[key] = self.assign_color(key)
16 | return self.assigned_color[key]
17 |
18 | def assign_color(self, key):
19 | raise NotImplementedError
20 |
21 |
22 | class LinaerColorPlatte(ColorPlatteBase):
23 | def __init__(
24 | self,
25 | start_color=Color.parse("#CD0000"),
26 | end_color=Color.parse("#FFE637"),
27 | ) -> None:
28 | super().__init__()
29 | self.assigned_color = {}
30 | self.start_color = start_color
31 | self.end_color = end_color
32 | self.index = 0
33 | self.platte = self.generate_platte()
34 |
35 | def assign_color(self, key):
36 | color = self.platte[self.index]
37 | self.index += 1
38 | if self.index == len(self.platte):
39 | self.index = 0
40 |
41 | logger.debug("assign color=%s", color)
42 | return color
43 |
44 | def generate_platte(self):
45 | color_platte = []
46 | for factor in range(0, 100, 5):
47 | color_platte.append(
48 | self.start_color.blend(self.end_color, factor / 100)
49 | )
50 | return color_platte
51 |
52 |
53 | class FlameGraphRandomColorPlatte(ColorPlatteBase):
54 | def __init__(self) -> None:
55 | super().__init__()
56 | self.assigned_color = {}
57 |
58 | def assign_color(self, *args):
59 | return Color(
60 | 205 + int(50 * random.random()),
61 | 0 + int(230 * random.random()),
62 | 0 + int(55 * random.random()),
63 | )
64 |
65 |
66 | flamegraph_random_color_platte = FlameGraphRandomColorPlatte()
67 | linaer_color_platte = LinaerColorPlatte()
68 |
--------------------------------------------------------------------------------
/flameshow/const.py:
--------------------------------------------------------------------------------
1 | VIEW_INFO_COLOR = "#ffffff"
2 | VIEW_INFO_OTHER_COLOR = "#8884FF"
3 | SELECTED_PARENTS_BG_COLOR_BLEND_TO = "#8b0000"
4 | SELECTED_PARENTS_BG_COLOR_BLEND_FACTOR = 0.5
5 |
--------------------------------------------------------------------------------
/flameshow/exceptions.py:
--------------------------------------------------------------------------------
1 | class FlameshowException(Exception):
2 | """FlameShow base Exception"""
3 |
4 |
5 | class ProfileParseException(FlameshowException):
6 | """Can not parse the profile"""
7 |
8 |
9 | class UsageError(FlameshowException):
10 | """Usage Error"""
11 |
12 |
13 | class RenderException(FlameshowException):
14 | """Got error when render, this usually means code bug of Flameshow, you can open an issue""" # noqa: E501
15 |
--------------------------------------------------------------------------------
/flameshow/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import time
4 |
5 | import sys
6 |
7 | import click
8 |
9 | from flameshow import __version__
10 | from flameshow.parsers import parse
11 | from flameshow.render import FlameshowApp
12 |
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | def setup_log(enabled, level, loglocation):
18 | if enabled:
19 | logging.basicConfig(
20 | filename=os.path.expanduser(loglocation),
21 | filemode="a",
22 | format=(
23 | "%(asctime)s %(levelname)5s (%(module)sL%(lineno)d)"
24 | " %(message)s"
25 | ),
26 | level=level,
27 | )
28 | else:
29 | logging.disable(logging.CRITICAL)
30 | logger.info("------ flameshow ------")
31 |
32 |
33 | LOG_LEVEL = {
34 | 0: logging.CRITICAL,
35 | 1: logging.WARNING,
36 | 2: logging.INFO,
37 | 3: logging.DEBUG,
38 | }
39 |
40 |
41 | def ensure_tty():
42 | if os.isatty(0):
43 | return
44 |
45 | logger.info("stdin is not a tty, replace it to fd=2")
46 | sys.stdin.close()
47 | sys.stdin = os.fdopen(2)
48 |
49 |
50 | def run_app(verbose, log_to, profile_f, _debug_exit_after_rednder):
51 | log_level = LOG_LEVEL[verbose]
52 | setup_log(log_to is not None, log_level, log_to)
53 |
54 | t0 = time.time()
55 | profile_data = profile_f.read()
56 |
57 | profile = parse(profile_data, profile_f.name)
58 |
59 | t01 = time.time()
60 | logger.info("Parse profile, took %.3fs", t01 - t0)
61 |
62 | ensure_tty()
63 | app = FlameshowApp(
64 | profile,
65 | _debug_exit_after_rednder,
66 | )
67 | app.run()
68 |
69 |
70 | def print_version(ctx, _, value):
71 | if not value or ctx.resilient_parsing:
72 | return
73 | click.echo(__version__)
74 | ctx.exit()
75 |
76 |
77 | @click.command()
78 | @click.option(
79 | "-v",
80 | "--verbose",
81 | count=True,
82 | default=2,
83 | help="Add log verbose level, using -v, -vv, -vvv for printing more logs.",
84 | )
85 | @click.option(
86 | "-l",
87 | "--log-to",
88 | type=click.Path(),
89 | default=None,
90 | help="Printing logs to a file, for debugging, default is no logs.",
91 | )
92 | @click.option(
93 | "--version",
94 | is_flag=True,
95 | callback=print_version,
96 | expose_value=False,
97 | is_eager=True,
98 | )
99 | @click.argument("profile", type=click.File("rb"))
100 | def main(verbose, log_to, profile):
101 | run_app(verbose, log_to, profile, _debug_exit_after_rednder=False)
102 |
103 |
104 | if __name__ == "__main__":
105 | main()
106 |
--------------------------------------------------------------------------------
/flameshow/models.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | import datetime
3 | import logging
4 | import time
5 | from typing import Dict, List, Set
6 | from typing_extensions import Self
7 |
8 | from rich.style import Style
9 | from rich.text import Text
10 |
11 | from flameshow.utils import sizeof
12 |
13 | from .runtime import r
14 |
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | class Frame:
20 | def __init__(
21 | self, name, _id, children=None, parent=None, values=None, root=None
22 | ) -> None:
23 | self.name = name
24 | self._id = _id
25 | if children:
26 | self.children = children
27 | else:
28 | self.children = []
29 | self.parent = parent
30 | if not values:
31 | self.values = []
32 | else:
33 | self.values = values
34 |
35 | self.root = root
36 |
37 | def pile_up(self, childstack: Self):
38 | childstack.parent = self
39 |
40 | for exist_child in self.children:
41 | # added to exist, no need to create one
42 | if exist_child.name == childstack.name:
43 | # some cases, childstack.children total value not equal to
44 | # childstack.values
45 | # so, we need to add values of "parent" instead of add values
46 | # by every child
47 | exist_child.values = list(
48 | map(sum, zip(exist_child.values, childstack.values))
49 | )
50 |
51 | for new_child in childstack.children:
52 | exist_child.pile_up(new_child)
53 | return
54 |
55 | self.children.append(childstack)
56 |
57 | def __eq__(self, other):
58 | if isinstance(other, Frame):
59 | return self._id == other._id
60 | return False
61 |
62 | @property
63 | def display_color(self):
64 | return r.get_color(self.color_key)
65 |
66 | def humanize(self, sample_unit, value):
67 | display_value = value
68 | if sample_unit == "bytes":
69 | display_value = sizeof(value)
70 |
71 | return display_value
72 |
73 | def __repr__(self) -> str:
74 | return f""
75 |
76 | def render_detail(self, sample_index: int, sample_unit: str):
77 | """
78 | render stacked information
79 | """
80 | detail = []
81 | frame = self
82 | while frame:
83 | lines = self.render_one_frame_detail(
84 | frame, sample_index, sample_unit
85 | )
86 | for line in lines:
87 | detail.append(
88 | Text.assemble(
89 | (" ", Style(bgcolor=frame.display_color.rich_color)),
90 | " ",
91 | line,
92 | )
93 | )
94 | frame = frame.parent
95 |
96 | return Text.assemble(*detail)
97 |
98 | def render_one_frame_detail(
99 | self, frame, sample_index: int, sample_unit: str
100 | ):
101 | raise NotImplementedError
102 |
103 | @property
104 | def title(self) -> Text:
105 | """Full name which will be displayed in the frame detail panel"""
106 | return Text(self.name)
107 |
108 | @property
109 | def color_key(self):
110 | """Same key will get the same color"""
111 | return self.name
112 |
113 | @property
114 | def display_name(self):
115 | """The name display on the flamegraph"""
116 | return self.name
117 |
118 |
119 | @dataclass
120 | class SampleType:
121 | sample_type: str = ""
122 | sample_unit: str = ""
123 |
124 |
125 | @dataclass
126 | class Profile:
127 | # required
128 | filename: str
129 | root_stack: Frame
130 | highest_lines: int
131 | # total samples is one top most sample, it's a list that contains all
132 | # its parents all the way up
133 | total_sample: int
134 | sample_types: List[SampleType]
135 | # int id mapping to Frame
136 | id_store: Dict[int, Frame]
137 |
138 | # optional
139 | default_sample_type_index: int = -1
140 | period_type: SampleType | None = None
141 | period: int = 0
142 | created_at: datetime.datetime | None = None
143 |
144 | # init by post_init
145 | lines: List = field(init=False)
146 |
147 | frameid_to_lineno: Dict[int, int] = field(init=False)
148 |
149 | # Frame grouped by same name
150 | name_aggr: Dict[str, List[Frame]] = field(init=False)
151 |
152 | def __post_init__(self):
153 | """
154 | init_lines must be called before render
155 | """
156 | t1 = time.time()
157 | logger.info("start to create lines...")
158 |
159 | root = self.root_stack
160 |
161 | lines = [
162 | [root],
163 | ]
164 | frameid_to_lineno = {0: 0}
165 | current = root.children
166 | line_no = 1
167 |
168 | while len(current) > 0:
169 | line = []
170 | next_line = []
171 |
172 | for child in current:
173 | line.append(child)
174 | frameid_to_lineno[child._id] = line_no
175 | next_line.extend(child.children)
176 |
177 | lines.append(line)
178 | line_no += 1
179 | current = next_line
180 |
181 | t2 = time.time()
182 | logger.info("create lines done, took %.2f seconds", t2 - t1)
183 | self.lines = lines
184 | self.frameid_to_lineno = frameid_to_lineno
185 |
186 | self.name_aggr = self.get_name_aggr(self.root_stack)
187 |
188 | def get_name_aggr(
189 | self, start_frame: Frame, names: Set[str] | None = None
190 | ) -> Dict[str, List[Frame]]:
191 | name = start_frame.name
192 |
193 | result = {}
194 | if names is None:
195 | names = set()
196 | if name not in names:
197 | result[name] = [start_frame]
198 |
199 | for child in start_frame.children:
200 | name_aggr = self.get_name_aggr(child, names | set([name]))
201 | for key, value in name_aggr.items():
202 | result.setdefault(key, []).extend(value)
203 |
204 | return result
205 |
--------------------------------------------------------------------------------
/flameshow/parsers/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flameshow.exceptions import ProfileParseException
3 |
4 | from flameshow.pprof_parser.parser import ProfileParser as PprofParser
5 |
6 | from .stackcollapse_parser import StackCollapseParser
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | ALL_PARSERS = [PprofParser, StackCollapseParser]
12 |
13 |
14 | def choose_parser(content: bytes):
15 | for p in ALL_PARSERS:
16 | if p.validate(content):
17 | return p
18 | raise ProfileParseException("Can not match any parser")
19 |
20 |
21 | def parse(filecontent: bytes, filename):
22 | parser_cls = choose_parser(filecontent)
23 | logger.info("Using %s...", parser_cls)
24 | parser = parser_cls(filename)
25 | profile = parser.parse(filecontent)
26 | return profile
27 |
--------------------------------------------------------------------------------
/flameshow/parsers/stackcollapse_parser.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import re
4 | from typing import Dict
5 |
6 | from rich.text import Text
7 |
8 | from flameshow.models import Frame, Profile, SampleType
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class StackCollapseFrame(Frame):
14 | def render_one_frame_detail(
15 | self, frame, sample_index: int, sample_unit: str
16 | ):
17 | return [Text(f"{frame.name}\n")]
18 |
19 |
20 | class StackCollapseParser:
21 | def __init__(self, filename) -> None:
22 | self.filename = filename
23 | self.next_id = 0
24 | self.root = StackCollapseFrame(
25 | "root", _id=self.idgenerator(), values=[0]
26 | )
27 | self.root.root = self.root
28 |
29 | self.highest = 0
30 | self.id_store: Dict[int, Frame] = {self.root._id: self.root}
31 | self.line_regex = r"(.*?) (\d+)$"
32 | self.line_matcher = re.compile(self.line_regex)
33 |
34 | def idgenerator(self):
35 | i = self.next_id
36 | self.next_id += 1
37 |
38 | return i
39 |
40 | def parse(self, text_data):
41 | text_data = text_data.decode()
42 | lines = text_data.split(os.linesep)
43 | for line in lines:
44 | self.parse_line(line)
45 |
46 | logger.info("root: %s, %s", self.root, self.root.values)
47 | logger.debug("root.children: %s", self.root.children)
48 |
49 | profile = Profile(
50 | filename=self.filename,
51 | root_stack=self.root,
52 | highest_lines=self.highest,
53 | total_sample=len(lines),
54 | sample_types=[SampleType("samples", "count")],
55 | id_store=self.id_store,
56 | )
57 | logger.info("profile.lines = %s", profile.lines)
58 | logger.info("profile.id_store = %s", profile.id_store)
59 | return profile
60 |
61 | def parse_line(self, line) -> None:
62 | line = line.strip()
63 | if not line:
64 | return
65 | if line.startswith("#"): # comments
66 | return
67 | matcher = self.line_matcher.match(line)
68 | if not matcher:
69 | logger.warning(
70 | "Can not parse {} with regex {}".format(line, self.line_regex)
71 | )
72 | return
73 | frame_str = matcher.group(1)
74 | count = int(matcher.group(2))
75 | frame_names = frame_str.split(";")
76 | logger.info("frame names:%s, count: %s", frame_names, count)
77 |
78 | pre = None
79 | head = None
80 | for name in frame_names:
81 | frame = StackCollapseFrame(
82 | name,
83 | self.idgenerator(),
84 | children=[],
85 | parent=pre,
86 | values=[count],
87 | root=self.root,
88 | )
89 | self.id_store[frame._id] = frame
90 | if pre:
91 | pre.children = [frame]
92 | frame.parent = pre
93 | if not head:
94 | head = frame
95 | pre = frame
96 |
97 | if head:
98 | self.root.pile_up(head)
99 | self.root.values[0] += head.values[0]
100 |
101 | if len(frame_names) > self.highest:
102 | self.highest = len(frame_names)
103 | logger.debug("over")
104 |
105 | @classmethod
106 | def validate(cls, content: bytes) -> bool:
107 | try:
108 | to_check = content.decode("utf-8")
109 | except: # noqa E722
110 | return False
111 |
112 | # only validate the first 100 lines
113 | lines = to_check.split(os.linesep)
114 | to_validate_lines = [
115 | line.strip() for line in lines[:100] if line.strip()
116 | ]
117 |
118 | if not to_validate_lines:
119 | logger.info("The file is empty, skip StackCollapseParser")
120 | return False
121 |
122 | for index, line in enumerate(to_validate_lines):
123 | if line.startswith("#"): # comments
124 | continue
125 | if not re.match(r"(.* )?\d+", line):
126 | logger.info(
127 | "line %d not match regex, line:%s not suitable for"
128 | " StackCollapseParser!",
129 | index + 1,
130 | line,
131 | )
132 | return False
133 |
134 | return True
135 |
--------------------------------------------------------------------------------
/flameshow/pprof_parser/__init__.py:
--------------------------------------------------------------------------------
1 | from .parser import parse_profile
2 |
3 | __all__ = ["parse_profile"]
4 |
--------------------------------------------------------------------------------
/flameshow/pprof_parser/parser.py:
--------------------------------------------------------------------------------
1 | """
2 | Parse golang's pprof format into flameshow.models which can be rendered.
3 |
4 | Ref:
5 | https://github.com/google/pprof/tree/main/proto
6 | """
7 |
8 | from dataclasses import dataclass, field
9 | import datetime
10 | import gzip
11 | import logging
12 | from typing import Dict, List
13 |
14 | from rich.text import Text
15 |
16 | from flameshow.models import Frame, Profile, SampleType
17 |
18 | from . import profile_pb2
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | @dataclass
24 | class Function:
25 | id: int = 0
26 | filename: str = ""
27 | name: str = ""
28 | start_line: int = 0
29 | system_name: str = ""
30 |
31 |
32 | @dataclass
33 | class Line:
34 | line_no: int = 0
35 | function: Function = field(default_factory=Function)
36 |
37 |
38 | @dataclass
39 | class Mapping:
40 | id: int = 0
41 | memory_start: int = 0
42 | memory_limit: int = 0
43 | file_offset: int = 0
44 |
45 | filename: str = ""
46 | build_id: str = ""
47 |
48 | has_functions: bool | None = None
49 | has_filenames: bool | None = None
50 | has_line_numbers: bool | None = None
51 | has_inline_frames: bool | None = None
52 |
53 |
54 | @dataclass
55 | class Location:
56 | id: int = 0
57 | mapping: Mapping = field(default_factory=Mapping)
58 | address: int = 0
59 | lines: List[Line] = field(default_factory=list)
60 | is_folded: bool = False
61 |
62 |
63 | class PprofFrame(Frame):
64 | def __init__(
65 | self,
66 | name,
67 | _id,
68 | children=None,
69 | parent=None,
70 | values=None,
71 | root=None,
72 | line=None,
73 | mapping=None,
74 | ) -> None:
75 | super().__init__(name, _id, children, parent, values, root)
76 |
77 | parts = self.name.split("/")
78 | if len(parts) > 1:
79 | self.golang_package = "/".join(parts[:-1])
80 | else:
81 | self.golang_package = "buildin"
82 |
83 | self.golang_module_function = parts[-1]
84 |
85 | self.golang_module = self.golang_module_function.split(".")[0]
86 |
87 | self.mapping_file = ""
88 | self.line = line
89 | self.mapping = mapping
90 |
91 | @property
92 | def color_key(self):
93 | return self.golang_module
94 |
95 | @property
96 | def display_name(self):
97 | return self.golang_module_function
98 |
99 | def render_one_frame_detail(self, frame, sample_index, sample_unit):
100 | if frame._id == 0: # root
101 | total = sum([c.values[sample_index] for c in frame.children])
102 | value = frame.humanize(sample_unit, total)
103 | if frame.children:
104 | binary_name = f"Binary: {frame.children[0].mapping.filename}"
105 | else:
106 | binary_name = "root"
107 | detail = f"{binary_name} [b red]{value}[/b red]\n"
108 | return [Text.from_markup(detail)]
109 |
110 | value = frame.humanize(sample_unit, frame.values[sample_index])
111 | line1 = f"{frame.line.function.name}: [b red]{value}[/b red]\n"
112 |
113 | line2 = (
114 | f" [grey37]{frame.line.function.filename}, [b]line"
115 | f" {frame.line.line_no}[/b][/grey37]\n"
116 | )
117 | return [Text.from_markup(line1), Text.from_markup(line2)]
118 |
119 | @property
120 | def title(self) -> str:
121 | return self.display_name
122 |
123 |
124 | def unmarshal(content) -> profile_pb2.Profile:
125 | if len(content) < 2:
126 | raise Exception(
127 | "Profile content length is too short: {} bytes".format(
128 | len(content)
129 | )
130 | )
131 | is_gzip = content[0] == 31 and content[1] == 139
132 |
133 | if is_gzip:
134 | content = gzip.decompress(content)
135 |
136 | profile = profile_pb2.Profile()
137 | profile.ParseFromString(content)
138 |
139 | return profile
140 |
141 |
142 | class ProfileParser:
143 | def __init__(self, filename):
144 | self.filename = filename
145 | # uniq id
146 | self.next_id = 0
147 |
148 | self.root = PprofFrame("root", _id=self.idgenerator())
149 | # need to set PprofFrame.root for every frame
150 | self.root.root = self.root
151 |
152 | # store the pprof's string table
153 | self._t = []
154 | # parse cached locations, profile do not need this, so only store
155 | # them on the parser
156 | self.locations = []
157 | self.highest = 0
158 |
159 | self.id_store: Dict[int, Frame] = {self.root._id: self.root}
160 |
161 | def idgenerator(self):
162 | i = self.next_id
163 | self.next_id += 1
164 |
165 | return i
166 |
167 | def s(self, index):
168 | return self._t[index]
169 |
170 | def parse_internal_data(self, pbdata):
171 | self._t = pbdata.string_table
172 | self.functions = self.parse_functions(pbdata.function)
173 | self.mappings = self.parse_mapping(pbdata.mapping)
174 | self.locations = self.parse_location(pbdata.location)
175 |
176 | def parse(self, binary_data):
177 | pbdata = unmarshal(binary_data)
178 | self.parse_internal_data(pbdata)
179 |
180 | sample_types = self.parse_sample_types(pbdata.sample_type)
181 |
182 | root = self.root
183 | root.values = [0] * len(sample_types)
184 | for pbsample in pbdata.sample:
185 | child_frame = self.parse_sample(pbsample)
186 | if not child_frame:
187 | continue
188 | root.values = list(map(sum, zip(root.values, child_frame.values)))
189 | root.pile_up(child_frame)
190 |
191 | pprof_profile = Profile(
192 | filename=self.filename,
193 | root_stack=root,
194 | highest_lines=self.highest,
195 | total_sample=len(pbdata.sample),
196 | sample_types=sample_types,
197 | id_store=self.id_store,
198 | )
199 |
200 | # default is 0, by the doc, default should be the last one
201 | if pbdata.default_sample_type:
202 | pprof_profile.default_sample_type_index = (
203 | pbdata.default_sample_type
204 | )
205 |
206 | pprof_profile.created_at = self.parse_created_at(pbdata.time_nanos)
207 | pprof_profile.period = pbdata.period
208 | pprof_profile.period_type = self.to_smaple_type(pbdata.period_type)
209 |
210 | return pprof_profile
211 |
212 | def parse_sample(self, sample) -> PprofFrame | None:
213 | values = sample.value
214 | locations = list(
215 | reversed([self.locations[i] for i in sample.location_id])
216 | )
217 |
218 | my_depth = sum(len(loc.lines) for loc in locations)
219 | self.highest = max(my_depth, self.highest)
220 |
221 | current_parent = None
222 | head = None
223 | for location in locations:
224 | for line in location.lines:
225 | frame = self.line2frame(location, line, values)
226 |
227 | if current_parent:
228 | frame.parent = current_parent
229 | current_parent.children = [frame]
230 | if not head:
231 | head = frame
232 |
233 | current_parent = frame
234 |
235 | return head
236 |
237 | def line2frame(self, location: Location, line: Line, values) -> PprofFrame:
238 | frame = PprofFrame(
239 | name=line.function.name,
240 | _id=self.idgenerator(),
241 | values=values,
242 | root=self.root,
243 | mapping=location.mapping,
244 | )
245 | frame.line = line
246 | self.id_store[frame._id] = frame
247 | return frame
248 |
249 | def parse_location(self, pblocations):
250 | parsed_locations = {}
251 | for pl in pblocations:
252 | loc = Location()
253 | loc.id = pl.id
254 | loc.mapping = self.mappings[pl.mapping_id]
255 | loc.address = pl.address
256 | loc.lines = self.parse_line(pl.line)
257 | loc.is_folded = pl.is_folded
258 | parsed_locations[loc.id] = loc
259 |
260 | return parsed_locations
261 |
262 | def parse_mapping(self, pbmappings):
263 | mappings = {}
264 | for pbm in pbmappings:
265 | m = Mapping()
266 | m.id = pbm.id
267 | m.memory_start = pbm.memory_start
268 | m.memory_limit = pbm.memory_limit
269 | m.file_offset = pbm.file_offset
270 | m.filename = self.s(pbm.filename)
271 | m.build_id = self.s(pbm.build_id)
272 | m.has_functions = pbm.has_functions
273 | m.has_filenames = pbm.has_filenames
274 | m.has_line_numbers = pbm.has_line_numbers
275 | m.has_inline_frames = pbm.has_inline_frames
276 |
277 | mappings[m.id] = m
278 |
279 | return mappings
280 |
281 | def parse_line(self, pblines) -> List[Line]:
282 | lines = []
283 | for pl in reversed(pblines):
284 | line = Line(
285 | line_no=pl.line,
286 | function=self.functions[pl.function_id],
287 | )
288 | lines.append(line)
289 | return lines
290 |
291 | def parse_functions(self, pfs):
292 | functions = {}
293 | for pf in pfs:
294 | functions[pf.id] = Function(
295 | id=pf.id,
296 | filename=self.s(pf.filename),
297 | name=self.s(pf.name),
298 | system_name=self.s(pf.system_name),
299 | start_line=pf.start_line,
300 | )
301 | return functions
302 |
303 | def parse_created_at(self, time_nanos):
304 | date = datetime.datetime.fromtimestamp(
305 | time_nanos / 1e9, tz=datetime.timezone.utc
306 | )
307 | return date
308 |
309 | def parse_sample_types(self, sample_types):
310 | result = []
311 | for st in sample_types:
312 | result.append(self.to_smaple_type(st))
313 |
314 | return result
315 |
316 | def to_smaple_type(self, st):
317 | return SampleType(self.s(st.type), self.s(st.unit))
318 |
319 | @classmethod
320 | def validate(cls, content: bytes) -> bool:
321 | try:
322 | unmarshal(content)
323 | except: # noqa E722
324 | logger.info("Error when parse content as Pprof")
325 | return False
326 | return True
327 |
328 |
329 | def get_frame_tree(root_frame):
330 | """
331 | only for testing and debugging
332 | """
333 |
334 | def _get_child(frame):
335 | return {c.name: _get_child(c) for c in frame.children}
336 |
337 | return {"root": _get_child(root_frame)}
338 |
339 |
340 | def parse_profile(binary_data, filename):
341 | parser = ProfileParser(filename)
342 | profile = parser.parse(binary_data)
343 |
344 | # import ipdb; ipdb.set_trace()
345 | return profile
346 |
347 |
348 | if __name__ == "__main__":
349 | with open("tests/pprof_data/goroutine.out", "rb") as f:
350 | content = f.read()
351 |
352 | parse_profile(content, "abc")
353 |
--------------------------------------------------------------------------------
/flameshow/pprof_parser/profile_pb2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by the protocol buffer compiler. DO NOT EDIT!
3 | # source: profile.proto
4 | """Generated protocol buffer code."""
5 | from google.protobuf.internal import builder as _builder
6 | from google.protobuf import descriptor as _descriptor
7 | from google.protobuf import descriptor_pool as _descriptor_pool
8 | from google.protobuf import symbol_database as _symbol_database
9 | # @@protoc_insertion_point(imports)
10 |
11 | _sym_db = _symbol_database.Default()
12 |
13 |
14 |
15 |
16 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rprofile.proto\x12\x12perftools.profiles\"\xd5\x03\n\x07Profile\x12\x32\n\x0bsample_type\x18\x01 \x03(\x0b\x32\x1d.perftools.profiles.ValueType\x12*\n\x06sample\x18\x02 \x03(\x0b\x32\x1a.perftools.profiles.Sample\x12,\n\x07mapping\x18\x03 \x03(\x0b\x32\x1b.perftools.profiles.Mapping\x12.\n\x08location\x18\x04 \x03(\x0b\x32\x1c.perftools.profiles.Location\x12.\n\x08\x66unction\x18\x05 \x03(\x0b\x32\x1c.perftools.profiles.Function\x12\x14\n\x0cstring_table\x18\x06 \x03(\t\x12\x13\n\x0b\x64rop_frames\x18\x07 \x01(\x03\x12\x13\n\x0bkeep_frames\x18\x08 \x01(\x03\x12\x12\n\ntime_nanos\x18\t \x01(\x03\x12\x16\n\x0e\x64uration_nanos\x18\n \x01(\x03\x12\x32\n\x0bperiod_type\x18\x0b \x01(\x0b\x32\x1d.perftools.profiles.ValueType\x12\x0e\n\x06period\x18\x0c \x01(\x03\x12\x0f\n\x07\x63omment\x18\r \x03(\x03\x12\x1b\n\x13\x64\x65\x66\x61ult_sample_type\x18\x0e \x01(\x03\"\'\n\tValueType\x12\x0c\n\x04type\x18\x01 \x01(\x03\x12\x0c\n\x04unit\x18\x02 \x01(\x03\"V\n\x06Sample\x12\x13\n\x0blocation_id\x18\x01 \x03(\x04\x12\r\n\x05value\x18\x02 \x03(\x03\x12(\n\x05label\x18\x03 \x03(\x0b\x32\x19.perftools.profiles.Label\"@\n\x05Label\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\x0b\n\x03str\x18\x02 \x01(\x03\x12\x0b\n\x03num\x18\x03 \x01(\x03\x12\x10\n\x08num_unit\x18\x04 \x01(\x03\"\xdd\x01\n\x07Mapping\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x14\n\x0cmemory_start\x18\x02 \x01(\x04\x12\x14\n\x0cmemory_limit\x18\x03 \x01(\x04\x12\x13\n\x0b\x66ile_offset\x18\x04 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x05 \x01(\x03\x12\x10\n\x08\x62uild_id\x18\x06 \x01(\x03\x12\x15\n\rhas_functions\x18\x07 \x01(\x08\x12\x15\n\rhas_filenames\x18\x08 \x01(\x08\x12\x18\n\x10has_line_numbers\x18\t \x01(\x08\x12\x19\n\x11has_inline_frames\x18\n \x01(\x08\"v\n\x08Location\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x12\n\nmapping_id\x18\x02 \x01(\x04\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\x04\x12&\n\x04line\x18\x04 \x03(\x0b\x32\x18.perftools.profiles.Line\x12\x11\n\tis_folded\x18\x05 \x01(\x08\")\n\x04Line\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\x04\x12\x0c\n\x04line\x18\x02 \x01(\x03\"_\n\x08\x46unction\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\x03\x12\x13\n\x0bsystem_name\x18\x03 \x01(\x03\x12\x10\n\x08\x66ilename\x18\x04 \x01(\x03\x12\x12\n\nstart_line\x18\x05 \x01(\x03\x42-\n\x1d\x63om.google.perftools.profilesB\x0cProfileProtob\x06proto3')
17 |
18 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
19 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'profile_pb2', globals())
20 | if _descriptor._USE_C_DESCRIPTORS == False:
21 |
22 | DESCRIPTOR._options = None
23 | DESCRIPTOR._serialized_options = b'\n\035com.google.perftools.profilesB\014ProfileProto'
24 | _PROFILE._serialized_start=38
25 | _PROFILE._serialized_end=507
26 | _VALUETYPE._serialized_start=509
27 | _VALUETYPE._serialized_end=548
28 | _SAMPLE._serialized_start=550
29 | _SAMPLE._serialized_end=636
30 | _LABEL._serialized_start=638
31 | _LABEL._serialized_end=702
32 | _MAPPING._serialized_start=705
33 | _MAPPING._serialized_end=926
34 | _LOCATION._serialized_start=928
35 | _LOCATION._serialized_end=1046
36 | _LINE._serialized_start=1048
37 | _LINE._serialized_end=1089
38 | _FUNCTION._serialized_start=1091
39 | _FUNCTION._serialized_end=1186
40 | # @@protoc_insertion_point(module_scope)
41 |
--------------------------------------------------------------------------------
/flameshow/render/__init__.py:
--------------------------------------------------------------------------------
1 | from .app import FlameshowApp
2 |
3 | __all__ = ["FlameshowApp"]
4 |
--------------------------------------------------------------------------------
/flameshow/render/app.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import logging
3 | from typing import ClassVar
4 |
5 | from textual import on
6 | from textual.app import App, ComposeResult
7 | from textual.binding import Binding, BindingType
8 | from textual.containers import VerticalScroll
9 | from textual.css.query import NoMatches
10 | from textual.reactive import reactive
11 | from textual.widgets import Footer, Static, Tabs, Tab
12 |
13 | from flameshow import __version__
14 | from flameshow.render.framedetail import FrameDetail, InformaionScreen
15 | from flameshow.render.header import FlameshowHeader
16 | from flameshow.render.tabs import SampleTabs
17 |
18 | from .flamegraph import FlameGraph
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | class FlameGraphScroll(
24 | VerticalScroll, inherit_bindings=False, can_focus=False
25 | ):
26 | BINDINGS: ClassVar[list[BindingType]] = [
27 | Binding("b", "page_up", "Scroll Page Up", show=True, key_display="B"),
28 | Binding(
29 | "f,space",
30 | "page_down",
31 | "Scroll Page Down",
32 | show=True,
33 | key_display="F",
34 | ),
35 | Binding("home", "scroll_home", "Scroll Home", show=False),
36 | Binding("end", "scroll_end", "Scroll End", show=False),
37 | Binding("pageup", "page_up", "Page Up", show=False),
38 | Binding("pagedown", "page_down", "Page Down", show=False),
39 | ]
40 |
41 | def scroll_to_make_line_center(self, line_no):
42 | height = self.size.height
43 | start_line = max(0, line_no - round(height / 2))
44 | self.scroll_to(y=start_line)
45 | return start_line
46 |
47 |
48 | class FlameshowApp(App):
49 | BINDINGS = [
50 | Binding("d", "toggle_dark", "Toggle dark mode", show=False),
51 | Binding(
52 | "tab,n",
53 | "switch_sample_type",
54 | "Switch Sample Type",
55 | priority=True,
56 | show=True,
57 | key_display="tab",
58 | ),
59 | Binding("ctrl+c,q", "quit", "Quit", show=True, key_display="Q"),
60 | Binding("o", "debug"),
61 | Binding("i", "information_screen", "Toggle view stack", show=True),
62 | ]
63 |
64 | DEFAULT_CSS = """
65 | #sample-type-radio {
66 | width: 20%;
67 | height: 1fr;
68 | }
69 |
70 | #profile-detail-info {
71 | text-align: right;
72 | color: grey;
73 | }
74 |
75 | Tabs {
76 | margin-bottom: 0;
77 | }
78 | """
79 |
80 | focused_stack_id = reactive(0)
81 | sample_index = reactive(None, init=False)
82 | view_frame = reactive(None, init=False)
83 |
84 | def __init__(
85 | self,
86 | profile,
87 | _debug_exit_after_rednder=False,
88 | *args,
89 | **kwargs,
90 | ):
91 | super().__init__(*args, **kwargs)
92 | self.profile = profile
93 | self.root_stack = profile.root_stack
94 |
95 | self._debug_exit_after_rednder = _debug_exit_after_rednder
96 |
97 | self.parents_that_only_one_child = []
98 |
99 | self.filename = self.profile.filename
100 |
101 | if profile.default_sample_type_index < 0:
102 | self.sample_index = (
103 | len(profile.sample_types) + profile.default_sample_type_index
104 | )
105 | else:
106 | self.sample_index = profile.default_sample_type_index
107 |
108 | fg = FlameGraph(
109 | self.profile,
110 | self.focused_stack_id,
111 | self.sample_index,
112 | self.root_stack,
113 | )
114 | fg.styles.height = self.profile.highest_lines + 1
115 | self.flamegraph_widget = fg
116 |
117 | tabs = [
118 | Tab(f"{s.sample_type}, {s.sample_unit}", id=f"sample-{index}")
119 | for index, s in enumerate(profile.sample_types)
120 | ]
121 | active_tab = tabs[self.sample_index].id
122 | self.tabs_widget = SampleTabs(*tabs, active=active_tab)
123 |
124 | self.frame_detail = FrameDetail(
125 | profile=profile,
126 | frame=self.root_stack,
127 | sample_index=self.sample_index,
128 | )
129 | self.show_information_screen = False
130 |
131 | def on_mount(self):
132 | logger.info("mounted")
133 | self.title = "flameshow"
134 | self.sub_title = f"v{__version__}"
135 |
136 | self.view_frame = self.root_stack
137 |
138 | def compose(self) -> ComposeResult:
139 | """Create child widgets for the app."""
140 |
141 | center_text = self._center_header_text(self.sample_index)
142 | yield FlameshowHeader(center_text)
143 |
144 | yield self.tabs_widget
145 |
146 | yield FlameGraphScroll(
147 | self.flamegraph_widget,
148 | id="flamegraph-out-container",
149 | )
150 |
151 | yield self.frame_detail
152 |
153 | yield self._profile_info(self.profile.created_at)
154 | yield Footer()
155 |
156 | def _center_header_text(self, sample_index):
157 | chosen_sample_type = self.profile.sample_types[sample_index]
158 | center_header = (
159 | f"{self.filename}: ({chosen_sample_type.sample_type},"
160 | f" {chosen_sample_type.sample_unit})"
161 | )
162 | return center_header
163 |
164 | def _profile_info(self, created_at: datetime):
165 | if not created_at:
166 | return Static("")
167 |
168 | datetime_str = created_at.astimezone().strftime(
169 | "Dumped at %Y %b %d(%A) %H:%M:%S %Z"
170 | )
171 | return Static(datetime_str, id="profile-detail-info")
172 |
173 | @on(FlameGraph.ViewFrameChanged)
174 | async def handle_view_frame_changed(self, e):
175 | logger.debug("app handle_view_frame_changed...")
176 | new_frame = e.frame
177 | by_mouse = e.by_mouse
178 | self.view_frame = new_frame
179 |
180 | self.flamegraph_widget.view_frame = new_frame
181 |
182 | if not by_mouse:
183 | frame_line_no = self.profile.frameid_to_lineno[new_frame._id]
184 | container = self.query_one("#flamegraph-out-container")
185 | container.scroll_to_make_line_center(line_no=frame_line_no)
186 |
187 | async def watch_sample_index(self, sample_index):
188 | logger.info("sample index changed to %d", sample_index)
189 |
190 | center_text = self._center_header_text(self.sample_index)
191 | try:
192 | header = self.query_one("FlameshowHeader")
193 | except NoMatches:
194 | logger.warning(
195 | "FlameshowHeader not found, might be not composed yet."
196 | )
197 | return
198 | header.center_text = center_text
199 |
200 | self.flamegraph_widget.sample_index = sample_index
201 | self.frame_detail.sample_index = sample_index
202 |
203 | if self.show_information_screen:
204 | information_screen = self.query_one("InformaionScreen")
205 | information_screen.sample_index = sample_index
206 |
207 | async def watch_view_frame(self, old, new_frame):
208 | logger.debug(
209 | "view info stack changed: old: %s, new: %s",
210 | old,
211 | new_frame,
212 | )
213 | self.frame_detail.frame = new_frame
214 |
215 | async def watch_focused_stack_id(
216 | self,
217 | focused_stack_id,
218 | ):
219 | logger.info(f"{focused_stack_id=} changed")
220 | self.flamegraph_widget.focused_stack_id = focused_stack_id
221 |
222 | def action_switch_sample_type(self):
223 | self.tabs_widget.action_next_tab()
224 |
225 | @property
226 | def sample_unit(self):
227 | return self.profile.sample_types[self.sample_index].sample_unit
228 |
229 | def action_debug(self):
230 | logger.info("currently focused on: %s", self.focused)
231 |
232 | @on(Tabs.TabActivated)
233 | def handle_sample_type_changed(self, event: Tabs.TabActivated):
234 | logger.info("Tab changed: %s", event)
235 | chosen_index = event.tab.id.split("-")[1]
236 | self.sample_index = int(chosen_index)
237 |
238 | def action_information_screen(self):
239 | if self.show_information_screen:
240 | self.pop_screen()
241 | else:
242 | self.push_screen(
243 | InformaionScreen(
244 | self.view_frame,
245 | self.sample_index,
246 | self.sample_unit,
247 | self.profile,
248 | )
249 | )
250 | self.show_information_screen = not self.show_information_screen
251 |
252 | @on(InformaionScreen.InformaionScreenPopped)
253 | def handle_inforamtion_screen_pop(self, event):
254 | logger.info("Information screen popped, event=%s", event)
255 | if self.show_information_screen:
256 | self.pop_screen()
257 | self.show_information_screen = False
258 |
--------------------------------------------------------------------------------
/flameshow/render/flamegraph.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | from functools import lru_cache
3 | import logging
4 | import time
5 | from typing import Dict, List, Union
6 |
7 | import iteround
8 | from rich.segment import Segment
9 | from rich.style import Style
10 | from textual import on
11 | from textual.binding import Binding
12 | from textual.color import Color
13 | from textual.events import Click, MouseEvent, MouseMove
14 | from textual.message import Message
15 | from textual.reactive import reactive
16 | from textual.strip import Strip
17 | from textual.widget import Widget
18 |
19 | from flameshow.const import (
20 | SELECTED_PARENTS_BG_COLOR_BLEND_FACTOR,
21 | SELECTED_PARENTS_BG_COLOR_BLEND_TO,
22 | VIEW_INFO_COLOR,
23 | VIEW_INFO_OTHER_COLOR,
24 | )
25 | from flameshow.exceptions import RenderException
26 | from flameshow.models import Frame
27 |
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 | FrameMap = namedtuple("FrameMap", "offset width")
32 |
33 |
34 | def add_array(arr1, arr2):
35 | return list(map(sum, zip(arr1, arr2)))
36 |
37 |
38 | class FlameGraph(Widget, can_focus=True):
39 | BINDINGS = [
40 | Binding("j,down", "move_down", "Down", key_display="↓"),
41 | Binding("k,up", "move_up", "Up", key_display="↑"),
42 | Binding("l,right", "move_right", "Right", key_display="→"),
43 | Binding("h,left", "move_left", "Left", key_display="←"),
44 | Binding("enter", "zoom_in", "Zoom In"),
45 | Binding("escape", "zoom_out", "Zoom Out", show=False),
46 | ]
47 |
48 | focused_stack_id = reactive(0)
49 | sample_index = reactive(0, init=False)
50 | view_frame = reactive(None, init=False)
51 |
52 | class ViewFrameChanged(Message):
53 | """View Frame changed"""
54 |
55 | def __init__(self, frame, by_mouse=False) -> None:
56 | super().__init__()
57 | self.frame = frame
58 | self.by_mouse = by_mouse
59 |
60 | def __repr__(self) -> str:
61 | return f"ViewFrameChanged({self.frame=})"
62 |
63 | def __init__(
64 | self,
65 | profile,
66 | focused_stack_id,
67 | sample_index,
68 | view_frame: Frame,
69 | *args,
70 | **kwargs,
71 | ):
72 | super().__init__(*args, **kwargs)
73 | self.profile = profile
74 | # +1 is extra "root" node
75 | self.focused_stack_id = focused_stack_id
76 | self.sample_index = sample_index
77 | self.view_frame = view_frame
78 |
79 | # pre-render
80 | self.frame_maps = None
81 |
82 | def render_lines(self, crop):
83 | my_width = crop.size.width
84 | self.frame_maps = self.generate_frame_maps(
85 | my_width, self.focused_stack_id
86 | )
87 |
88 | logger.info("render crop: %s", crop)
89 |
90 | return super().render_lines(crop)
91 |
92 | @lru_cache
93 | def generate_frame_maps(self, width, focused_stack_id):
94 | """
95 | compute attributes for render for every frame
96 |
97 | only re-computes with width, focused_stack changing
98 | """
99 | t1 = time.time()
100 | logger.info(
101 | "lru cache miss, Generates frame map, for width=%d,"
102 | " focused_stack_id=%s",
103 | width,
104 | focused_stack_id,
105 | )
106 | frame_maps: Dict[int, List[FrameMap]] = {}
107 | current_focused_stack = self.profile.id_store[focused_stack_id]
108 | st_count = len(current_focused_stack.values)
109 | logger.debug("values count: %s", st_count)
110 |
111 | # set me to 100% and siblins to 0
112 | me = current_focused_stack
113 | while me:
114 | frame_maps[me._id] = [FrameMap(0, width) for _ in range(st_count)]
115 | me = me.parent
116 |
117 | logger.info("frame maps: %s", frame_maps)
118 |
119 | def _generate_for_children(frame):
120 | # generate for children
121 | my_maps = frame_maps[frame._id]
122 | for sample_i, my_map in enumerate(my_maps):
123 | parent_width = my_map.width
124 | if frame.values[sample_i] <= 0:
125 | child_widthes = [0.0 for _ in frame.children]
126 | else:
127 | child_widthes = [
128 | child.values[sample_i]
129 | / frame.values[sample_i]
130 | * parent_width
131 | for child in frame.children
132 | ]
133 |
134 | # the tail_spaces here only for iteround, in the case that
135 | # child total is not 100% of parent, so tail need to be here
136 | # to take some spaces
137 | tail_spaces = float(parent_width - sum(child_widthes))
138 | if tail_spaces > 0:
139 | child_widthes.append(tail_spaces)
140 | rounded_child_widthes = iteround.saferound(
141 | child_widthes, 0, topline=parent_width
142 | )
143 |
144 | offset = my_map.offset
145 | for index, child in enumerate(frame.children):
146 | child_width = int(rounded_child_widthes[index])
147 | frame_maps.setdefault(child._id, []).append(
148 | FrameMap(
149 | offset=offset,
150 | width=child_width,
151 | )
152 | )
153 | offset += child_width
154 |
155 | for child in frame.children:
156 | _generate_for_children(child)
157 |
158 | _generate_for_children(current_focused_stack)
159 | t2 = time.time()
160 | logger.info("Generates frame maps done, took %.4f seconds", t2 - t1)
161 | return frame_maps
162 |
163 | def render_line(self, y: int) -> Strip:
164 | # logger.info("container_size: %s", self.container_size)
165 | line = self.profile.lines[y]
166 |
167 | if not self.frame_maps:
168 | # never should happen
169 | # pragma: no cover
170 | raise RenderException("frame_maps is not init yet!")
171 |
172 | segments = []
173 | cursor = 0
174 | for frame in line:
175 | frame_maps = self.frame_maps.get(frame._id)
176 | if not frame_maps:
177 | # never should happen
178 | continue # pragma: no cover
179 | frame_map = frame_maps[self.sample_index]
180 | my_width = frame_map.width
181 | if not my_width:
182 | continue
183 |
184 | text = "▏" + frame.display_name
185 | offset = frame_map.offset
186 | pre_pad = offset - cursor
187 |
188 | if pre_pad > 0:
189 | segments.append(Segment(" " * pre_pad))
190 | cursor += pre_pad
191 | elif pre_pad < 0:
192 | # never should happen
193 | raise Exception(
194 | "Prepad is negative! {}".format(pre_pad)
195 | ) # pragma: no cover
196 |
197 | if len(text) < my_width:
198 | text += " " * (my_width - len(text))
199 | if len(text) > my_width:
200 | text = text[:my_width]
201 |
202 | display_color = frame.display_color
203 | bold = False
204 |
205 | expand_before_line = self.profile.frameid_to_lineno[
206 | self.focused_stack_id
207 | ]
208 | if y <= expand_before_line:
209 | display_color = display_color.blend(
210 | Color.parse(SELECTED_PARENTS_BG_COLOR_BLEND_TO),
211 | SELECTED_PARENTS_BG_COLOR_BLEND_FACTOR,
212 | )
213 |
214 | if frame is self.view_frame:
215 | display_color = Color.parse(VIEW_INFO_COLOR)
216 | bold = True
217 | elif frame.name == self.view_frame.name:
218 | display_color = Color.parse(VIEW_INFO_OTHER_COLOR)
219 |
220 | if my_width > 0:
221 | # | is always default color
222 | segments.append(
223 | Segment(
224 | text[0],
225 | Style(
226 | bgcolor=display_color.rich_color,
227 | ),
228 | )
229 | )
230 | if my_width > 1:
231 | segments.append(
232 | Segment(
233 | text[1:],
234 | Style(
235 | color=display_color.get_contrast_text().rich_color,
236 | bgcolor=display_color.rich_color,
237 | bold=bold,
238 | ),
239 | )
240 | )
241 | cursor += my_width
242 |
243 | strip = Strip(segments)
244 | return strip
245 |
246 | def action_zoom_in(self):
247 | logger.info("Zoom in!")
248 | self.focused_stack_id = self.view_frame._id
249 |
250 | def action_zoom_out(self):
251 | logger.info("Zoom out!")
252 | self.focused_stack_id = self.profile.root_stack._id
253 |
254 | def action_move_down(self):
255 | logger.debug("move down")
256 | view_frame = self.view_frame
257 | children = view_frame.children
258 |
259 | if not children:
260 | logger.debug("no more children")
261 | return
262 |
263 | # go to the biggest value
264 | new_view_info_frame = self._get_biggest_exist_child(children)
265 | self.post_message(self.ViewFrameChanged(new_view_info_frame))
266 |
267 | def _get_biggest_exist_child(self, stacks):
268 | biggest = max(stacks, key=lambda s: s.values[self.sample_index])
269 | return biggest
270 |
271 | def action_move_up(self):
272 | logger.debug("move up")
273 | parent = self.view_frame.parent
274 |
275 | if not parent:
276 | logger.debug("no more children")
277 | return
278 |
279 | self.post_message(self.ViewFrameChanged(parent))
280 |
281 | def action_move_right(self):
282 | logger.debug("move right")
283 |
284 | right = self._find_right_sibling(self.view_frame)
285 |
286 | logger.debug("found right sibling: %s", right)
287 | if not right:
288 | logger.debug("Got no right sibling")
289 | return
290 |
291 | self.post_message(self.ViewFrameChanged(right))
292 |
293 | def _find_right_sibling(self, me):
294 | my_parent = me.parent
295 | while my_parent:
296 | siblings = my_parent.children
297 | if len(siblings) >= 2:
298 | choose_index = siblings.index(me)
299 | while choose_index < len(siblings):
300 | choose_index = choose_index + 1
301 | if (
302 | choose_index < len(siblings)
303 | and siblings[choose_index].values[self.sample_index]
304 | > 0
305 | ):
306 | return siblings[choose_index]
307 |
308 | me = my_parent
309 | my_parent = my_parent.parent
310 |
311 | def action_move_left(self):
312 | logger.debug("move left")
313 |
314 | left = self._find_left_sibling(self.view_frame)
315 |
316 | if not left:
317 | logger.debug("Got no left sibling")
318 | return
319 |
320 | self.post_message(self.ViewFrameChanged(left))
321 |
322 | def _find_left_sibling(self, me):
323 | """
324 | Find left.
325 | Even not currently displayed, still can be viewed on detail.
326 | No need to check if the fgid is currently rendered.
327 | """
328 | my_parent = me.parent
329 | while my_parent:
330 | siblings = my_parent.children
331 | if len(siblings) >= 2:
332 | choose_index = siblings.index(me)
333 | # move left, until:
334 | # got a sibling while value is not 0 (0 won't render)
335 | # and index >= 0
336 | while choose_index >= 0:
337 | choose_index = choose_index - 1
338 | if (
339 | choose_index >= 0
340 | and siblings[choose_index].values[self.sample_index]
341 | > 0
342 | ):
343 | return siblings[choose_index]
344 |
345 | me = my_parent
346 | my_parent = my_parent.parent
347 |
348 | def on_mouse_move(self, event: MouseMove) -> None:
349 | hover_frame = self.get_frame_under_mouse(event)
350 | if hover_frame:
351 | logger.info("mouse hover on %s", hover_frame)
352 | self.post_message(
353 | self.ViewFrameChanged(hover_frame, by_mouse=True)
354 | )
355 |
356 | @on(Click)
357 | def handle_click_frame(self, event: Click):
358 | frame = self.get_frame_under_mouse(event)
359 | if frame:
360 | self.focused_stack_id = frame._id
361 |
362 | def get_frame_under_mouse(self, event: MouseEvent) -> Union[None, Frame]:
363 | line_no = event.y
364 | x = event.x
365 |
366 | if line_no >= len(self.profile.lines):
367 | return
368 |
369 | line = self.profile.lines[line_no]
370 |
371 | for frame in line:
372 | frame_maps = self.frame_maps.get(frame._id)
373 | if not frame_maps:
374 | # this frame not exist in current render
375 | continue
376 | frame_map = frame_maps[self.sample_index]
377 | offset = frame_map.offset
378 | width = frame_map.width
379 |
380 | if offset <= x < offset + width: # find it!
381 | return frame
382 |
--------------------------------------------------------------------------------
/flameshow/render/framedetail.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from textual.binding import Binding
4 | from textual.containers import Vertical, VerticalScroll
5 | from textual.css.query import NoMatches
6 | from textual.message import Message
7 | from textual.reactive import reactive
8 | from textual.screen import Screen
9 | from textual.widget import Widget
10 | from textual.widgets import Footer, Static
11 |
12 | from flameshow.models import Frame
13 | from flameshow.render.header import FlameshowHeader
14 | from flameshow.utils import sizeof
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | def humanize(sample_unit, value):
20 | display_value = value
21 | if sample_unit == "bytes":
22 | display_value = sizeof(value)
23 |
24 | return str(display_value)
25 |
26 |
27 | class FrameStatThis(Widget):
28 | frame = reactive(None, init=False)
29 | sample_index = reactive(None, init=False)
30 |
31 | # width = 998.1MiB|998.1MiB
32 | DEFAULT_CSS = """
33 | FrameStatThis {
34 | height: 5;
35 | padding: 0 1;
36 | border: round $secondary;
37 | border-title-align: center;
38 |
39 | layout: grid;
40 | grid-size: 2 3;
41 | grid-rows: 1fr;
42 | grid-columns: 1fr;
43 | grid-gutter: 0 1;
44 | }
45 |
46 | #stat-this-total-label {
47 | background: $primary-background;
48 | }
49 | #stat-this-self-label {
50 | background: $secondary-background;
51 | }
52 | """
53 |
54 | def __init__(self, frame, profile, sample_index, *args, **kwargs):
55 | self.composed = False
56 | super().__init__(*args, **kwargs)
57 |
58 | self.profile = profile
59 | self.border_title = "This Instance"
60 | self.frame = frame
61 | self.sample_index = sample_index
62 |
63 | def compose(self):
64 | yield Static("Total", id="stat-this-total-label")
65 | yield Static("Self", id="stat-this-self-label")
66 | yield Static(
67 | self.frame_this_total_value_humanize, id="stat-this-total-value"
68 | )
69 | yield Static(self.frame_self_value_humanize, id="stat-this-self-value")
70 | yield Static(self.frame_total_percent, id="stat-this-total-percent")
71 | yield Static(self.frame_self_percent, id="stat-this-self-percent")
72 | self.composed = True
73 |
74 | def watch_frame(self, _: Frame):
75 | self._rerender()
76 |
77 | def watch_sample_index(self, _: int):
78 | self._rerender()
79 |
80 | def _rerender(self):
81 | if not self.composed:
82 | return
83 | logger.info(f"rerender --> {self.frame=} {self.sample_index=}")
84 |
85 | total_value_widget = self.query_one("#stat-this-total-value")
86 | total_value_widget.update(self.frame_this_total_value_humanize)
87 |
88 | total_percent_widget = self.query_one("#stat-this-total-percent")
89 | total_percent_widget.update(self.frame_total_percent)
90 |
91 | self_value_widget = self.query_one("#stat-this-self-value")
92 | self_value_widget.update(self.frame_self_value_humanize)
93 |
94 | self_percent_widget = self.query_one("#stat-this-self-percent")
95 | self_percent_widget.update(self.frame_self_percent)
96 |
97 | # TODO value should be rendered as different color based on total value
98 | @property
99 | def frame_this_total_value_humanize(self):
100 | logger.info("this instance: %s", self.frame)
101 |
102 | logger.info(
103 | "this instance name: %s, values=%s",
104 | self.frame.display_name,
105 | self.frame.values,
106 | )
107 | value = self.frame.values[self.sample_index]
108 | value_display = humanize(self.sample_unit, value)
109 | return value_display
110 |
111 | @property
112 | def frame_self_value(self):
113 | value = self.frame.values[self.sample_index]
114 | self_value = value
115 | child_value = 0
116 | if self.frame.children:
117 | for child in self.frame.children:
118 | child_value += child.values[self.sample_index]
119 |
120 | self_value -= child_value
121 | return self_value
122 |
123 | @property
124 | def frame_self_value_humanize(self):
125 | value_display = humanize(self.sample_unit, self.frame_self_value)
126 | return value_display
127 |
128 | @property
129 | def frame_self_percent(self):
130 | frame = self.frame
131 | sample_index = self.sample_index
132 |
133 | if not frame.root.values[sample_index]:
134 | p_root = 0
135 | else:
136 | p_root = (
137 | self.frame_self_value / frame.root.values[sample_index] * 100
138 | )
139 |
140 | return f"{p_root:.2f}%"
141 |
142 | @property
143 | def frame_total_percent(self):
144 | frame = self.frame
145 | sample_index = self.sample_index
146 |
147 | if not frame.root.values[sample_index]:
148 | p_root = 0
149 | else:
150 | p_root = (
151 | frame.values[sample_index]
152 | / frame.root.values[sample_index]
153 | * 100
154 | )
155 |
156 | return f"{p_root:.2f}%"
157 |
158 | @property
159 | def sample_unit(self):
160 | return self.profile.sample_types[self.sample_index].sample_unit
161 |
162 |
163 | class FrameStatAll(Widget):
164 | frame = reactive(None)
165 | sample_index = reactive(None)
166 |
167 | # width = 998.1MiB|998.1MiB
168 | DEFAULT_CSS = """
169 | FrameStatAll {
170 | height: 5;
171 | padding: 0 1;
172 | border: round $secondary;
173 | border-title-align: center;
174 |
175 | layout: grid;
176 | grid-size: 2 3;
177 | grid-rows: 1fr;
178 | grid-columns: 1fr;
179 | grid-gutter: 0 1;
180 | }
181 |
182 | #stat-all-total-label {
183 | background: $primary-background;
184 | }
185 | #stat-all-self-label {
186 | background: $secondary-background;
187 | }
188 | """
189 |
190 | def __init__(self, frame, profile, sample_index, *args, **kwargs):
191 | self.composed = False
192 | super().__init__(*args, **kwargs)
193 | self.frame = frame
194 | self.sample_index = sample_index
195 | self.profile = profile
196 | self.border_title = "All Instances"
197 | self.name_to_frame = profile.name_aggr
198 |
199 | def compose(self):
200 | yield Static("Total", id="stat-all-total-label")
201 | yield Static("Self", id="stat-all-self-label")
202 | yield Static(
203 | self.frame_all_total_value_humanize, id="stat-all-total-value"
204 | )
205 | yield Static(
206 | self.frame_all_self_value_humanize, id="stat-all-self-value"
207 | )
208 | yield Static(self.frame_all_total_percent, id="stat-all-total-percent")
209 | yield Static(self.frame_all_self_percent, id="stat-all-self-percent")
210 | self.composed = True
211 |
212 | def watch_frame(self, _: Frame):
213 | self._rerender()
214 |
215 | def watch_sample_index(self, _: int):
216 | self._rerender()
217 |
218 | def _rerender(self):
219 | if not self.composed:
220 | return
221 | logger.info(f"rerender --> {self.frame=} {self.sample_index=}")
222 |
223 | total_value_widget = self.query_one("#stat-all-total-value")
224 | total_value_widget.update(self.frame_all_total_value_humanize)
225 |
226 | total_percent_widget = self.query_one("#stat-all-total-percent")
227 | total_percent_widget.update(self.frame_all_total_percent)
228 |
229 | self_value_widget = self.query_one("#stat-all-self-value")
230 | self_value_widget.update(self.frame_all_self_value_humanize)
231 |
232 | self_percent_widget = self.query_one("#stat-all-self-percent")
233 | self_percent_widget.update(self.frame_all_self_percent)
234 |
235 | @property
236 | def frame_all_self_value(self):
237 | frames_same_name = self.name_to_frame[self.frame.name]
238 | total_value = 0
239 | i = self.sample_index
240 | for instance in frames_same_name:
241 | self_value = instance.values[i]
242 |
243 | if instance.children:
244 | child_total = sum(
245 | child.values[i] for child in instance.children
246 | )
247 | self_value -= child_total
248 |
249 | total_value += self_value
250 |
251 | return total_value
252 |
253 | @property
254 | def frame_all_self_value_humanize(self):
255 | value_display = humanize(self.sample_unit, self.frame_all_self_value)
256 | return value_display
257 |
258 | @property
259 | def frame_all_total_value(self):
260 | frames_same_name = self.name_to_frame[self.frame.name]
261 | total_value = sum(
262 | f.values[self.sample_index] for f in frames_same_name
263 | )
264 | return total_value
265 |
266 | @property
267 | def frame_all_total_value_humanize(self):
268 | value_display = humanize(self.sample_unit, self.frame_all_total_value)
269 | return value_display
270 |
271 | @property
272 | def frame_all_self_percent(self):
273 | frame = self.frame
274 | sample_index = self.sample_index
275 |
276 | if not frame.root.values[sample_index]:
277 | p_root = 0
278 | else:
279 | p_root = (
280 | self.frame_all_self_value
281 | / frame.root.values[sample_index]
282 | * 100
283 | )
284 |
285 | return f"{p_root:.2f}%"
286 |
287 | @property
288 | def frame_all_total_percent(self):
289 | frame = self.frame
290 | sample_index = self.sample_index
291 |
292 | if not frame.root.values[sample_index]:
293 | p_root = 0
294 | else:
295 | p_root = (
296 | self.frame_all_total_value
297 | / frame.root.values[sample_index]
298 | * 100
299 | )
300 |
301 | return f"{p_root:.2f}%"
302 |
303 | @property
304 | def sample_unit(self):
305 | return self.profile.sample_types[self.sample_index].sample_unit
306 |
307 |
308 | class FrameDetail(Widget):
309 | DEFAULT_CSS = """
310 | FrameDetail {
311 | layout: horizontal;
312 | height: 10;
313 | }
314 |
315 | #span-stack-container {
316 | width: 1fr;
317 | height: 1fr;
318 | padding-left: 1;
319 | padding-right: 0;
320 | border: round $secondary;
321 | content-align-vertical: middle;
322 | }
323 |
324 | #stat-container {
325 | width: 20%;
326 | max-width: 25;
327 | }
328 | """
329 | frame = reactive(None, init=False)
330 | sample_index = reactive(None, init=False)
331 |
332 | def __init__(self, frame, profile, sample_index, *args, **kwargs):
333 | self.composed = False
334 | super().__init__(*args, **kwargs)
335 | self.frame = frame
336 | self.sample_index = sample_index
337 | self.profile = profile
338 |
339 | def compose(self):
340 | yield Vertical(
341 | FrameStatThis(self.frame, self.profile, self.sample_index),
342 | FrameStatAll(self.frame, self.profile, self.sample_index),
343 | id="stat-container",
344 | )
345 | content = self.frame.render_detail(self.sample_index, self.sample_unit)
346 | span_detail = Static(
347 | content,
348 | id="span-detail",
349 | )
350 | span_stack_container = VerticalScroll(
351 | span_detail, id="span-stack-container"
352 | )
353 | span_stack_container.border_title = self.frame.title
354 | yield span_stack_container
355 | self.composed = True
356 |
357 | def _rerender(self):
358 | if not self.composed:
359 | return
360 | try:
361 | span_detail = self.query_one("#span-detail")
362 | span_stack_container = self.query_one("#span-stack-container")
363 | except NoMatches:
364 | return
365 | span_stack_container.border_title = self.frame.title
366 | content = self.frame.render_detail(self.sample_index, self.sample_unit)
367 | span_detail.update(content)
368 |
369 | try:
370 | frame_this_widget = self.query_one("FrameStatThis")
371 | except NoMatches:
372 | logger.warning("Can not find FrameStatThis when _rerender detail")
373 | else:
374 | frame_this_widget.frame = self.frame
375 | frame_this_widget.sample_index = self.sample_index
376 |
377 | try:
378 | frame_all_widget = self.query_one("FrameStatAll")
379 | except NoMatches:
380 | logger.warning("Can not find FrameStatAll when _rerender detail")
381 | else:
382 | frame_all_widget.frame = self.frame
383 | frame_all_widget.sample_index = self.sample_index
384 |
385 | @property
386 | def sample_unit(self):
387 | return self.profile.sample_types[self.sample_index].sample_unit
388 |
389 | def watch_frame(self, new_frame):
390 | logger.info("detailed frame changed to: %s", new_frame)
391 | self._rerender()
392 |
393 | def watch_sample_index(self, new_sample_index):
394 | logger.info("sample index changed to: %s", new_sample_index)
395 | self._rerender()
396 |
397 |
398 | class InformaionScreen(Screen):
399 | BINDINGS = [
400 | Binding("escape", "exit_screen", "Close detail screen", show=True),
401 | ]
402 | sample_index = reactive(None, init=False)
403 |
404 | class InformaionScreenPopped(Message):
405 | pass
406 |
407 | def __init__(
408 | self, frame, sample_index, sample_unit, profile, *args, **kwargs
409 | ) -> None:
410 | self.composed = False
411 | super().__init__(*args, **kwargs)
412 | self.frame = frame
413 | self.sample_index = sample_index
414 | self.profile = profile
415 |
416 | def compose(self):
417 | center_text = "Stack detail information"
418 | yield FlameshowHeader(center_text)
419 | content = self.frame.render_detail(self.sample_index, self.sample_unit)
420 | span_detail = Static(
421 | content,
422 | id="span-detail",
423 | )
424 | span_stack_container = VerticalScroll(
425 | span_detail, id="span-stack-container"
426 | )
427 | span_stack_container.border_title = self.frame.title
428 | yield span_stack_container
429 | yield Footer()
430 | self.composed = True
431 |
432 | def action_exit_screen(self):
433 | self.post_message(self.InformaionScreenPopped())
434 |
435 | def watch_sample_index(self, new_sample_index):
436 | logger.info("sample index change: %s", new_sample_index)
437 | self._rerender()
438 |
439 | def _rerender(self):
440 | if not self.composed:
441 | return
442 | content = self.frame.render_detail(self.sample_index, self.sample_unit)
443 | try:
444 | span_detail = self.query_one("#span-detail")
445 | except NoMatches:
446 | logger.warning("Didn't found #span-detail")
447 | return
448 | span_detail.update(content)
449 |
450 | @property
451 | def sample_unit(self):
452 | return self.profile.sample_types[self.sample_index].sample_unit
453 |
--------------------------------------------------------------------------------
/flameshow/render/header.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from rich.text import Text
4 | from textual.app import RenderResult
5 | from textual.containers import Horizontal
6 | from textual.css.query import NoMatches
7 | from textual.reactive import Reactive, reactive
8 | from textual.widget import Widget
9 | from textual.widgets import Header
10 |
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class HeaderIcon(Widget):
16 | """Display an 'icon' on the left of the header."""
17 |
18 | DEFAULT_CSS = """
19 | HeaderIcon {
20 | dock: left;
21 | padding-right: 1;
22 | width: 3;
23 | content-align: left middle;
24 | }
25 | """
26 |
27 | icon = reactive("🔥")
28 | """The character to use as the icon within the header."""
29 |
30 | def render(self) -> RenderResult:
31 | """Render the header icon.
32 |
33 | Returns:
34 | The rendered icon.
35 | """
36 | return self.icon
37 |
38 |
39 | class HeaderTitle(Widget):
40 | """Display the title / subtitle in the header."""
41 |
42 | DEFAULT_CSS = """
43 | HeaderTitle {
44 | content-align: left middle;
45 | width: 20;
46 | }
47 | """
48 |
49 | text: Reactive[str] = Reactive("")
50 | """The main title text."""
51 |
52 | sub_text = Reactive("")
53 | """The sub-title text."""
54 |
55 | def render(self) -> RenderResult:
56 | """Render the title and sub-title.
57 |
58 | Returns:
59 | The value to render.
60 | """
61 | text = Text(self.text, no_wrap=True, overflow="ellipsis")
62 | if self.sub_text:
63 | text.append(" — ")
64 | text.append(self.sub_text, "dim")
65 | return text
66 |
67 |
68 | class HeaderOpenedFilename(Widget):
69 | DEFAULT_CSS = """
70 | HeaderOpenedFilename {
71 | content-align: center middle;
72 | width: 70%;
73 | }
74 | """
75 |
76 | filename = reactive("")
77 |
78 | def __init__(self, filename, *args, **kwargs):
79 | super().__init__(*args, **kwargs)
80 | self.filename = filename
81 |
82 | def render(self) -> RenderResult:
83 | text = Text(self.filename, no_wrap=True, overflow="ellipsis")
84 | logger.info("header filename: %s", self.filename)
85 | return text
86 |
87 |
88 | class FlameshowHeader(Header):
89 | center_text = reactive("", init=False)
90 |
91 | def __init__(self, filename, *args, **kwargs):
92 | super().__init__(*args, **kwargs)
93 | self.center_text = filename
94 |
95 | def compose(self):
96 | yield HeaderIcon()
97 | yield Horizontal(
98 | HeaderTitle(),
99 | HeaderOpenedFilename(self.center_text, id="header-center-text"),
100 | )
101 |
102 | def watch_center_text(self, newtext):
103 | try:
104 | headero = self.query_one("#header-center-text")
105 | except NoMatches:
106 | pass
107 | else:
108 | headero.filename = newtext
109 |
--------------------------------------------------------------------------------
/flameshow/render/tabs.py:
--------------------------------------------------------------------------------
1 | from textual.widgets import Tabs
2 |
3 |
4 | class SampleTabs(Tabs, can_focus=False):
5 | pass
6 |
--------------------------------------------------------------------------------
/flameshow/runtime.py:
--------------------------------------------------------------------------------
1 | """
2 | Holds the run time configs.
3 | Can be changed dynamically.
4 | """
5 |
6 | from dataclasses import dataclass
7 | import logging
8 |
9 | from .colors import flamegraph_random_color_platte
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | @dataclass
15 | class Runtime:
16 | color_platte = flamegraph_random_color_platte
17 |
18 | def get_color(self, key):
19 | return self.color_platte.get_color(key)
20 |
21 |
22 | r = Runtime()
23 |
--------------------------------------------------------------------------------
/flameshow/utils.py:
--------------------------------------------------------------------------------
1 | def sizeof(num, suffix="B"):
2 | """
3 | credit: Fred Cirera
4 | - https://stackoverflow.com/questions/1094841/get-human-readable-version-of-file-size
5 | - https://web.archive.org/web/20111010015624/http://blogmag.net/blog/read/38/Print_human_readable_file_size
6 | """ # noqa: E501
7 | f = "{num:.1f}{unit}{suffix}"
8 | for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
9 | if abs(num) < 1024.0:
10 | return f.format(num=num, unit=unit, suffix=suffix)
11 | num /= 1024.0
12 | return f.format(num=num, unit="Yi", suffix=suffix)
13 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: proto run-test
2 |
3 | proto:
4 | docker run --rm -v${PWD}:${PWD} -w${PWD} namely/protoc-all --proto_path=${PWD}/proto \
5 | --python_out=${PWD}/flameshow/pprof_parser/ ${PWD}/proto/profile.proto
6 | bump_patch:
7 | bumpversion patch
8 |
9 | bump_minor:
10 | bumpversion minor
11 |
12 | patch: bump_patch
13 | rm -rf dist
14 | poetry build
15 | poetry publish
16 |
17 | _perf_startup:
18 | sudo py-spy record python _perf_main.py
19 |
20 | run-test:
21 | rm -rf htmlcov && pytest --cov-report html --cov=flameshow -vv --disable-warnings
22 | flake8 .
23 | black .
24 | open htmlcov/index.html
25 |
--------------------------------------------------------------------------------
/proto/profile.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Google Inc. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Profile is a common stacktrace profile format.
16 | //
17 | // Measurements represented with this format should follow the
18 | // following conventions:
19 | //
20 | // - Consumers should treat unset optional fields as if they had been
21 | // set with their default value.
22 | //
23 | // - When possible, measurements should be stored in "unsampled" form
24 | // that is most useful to humans. There should be enough
25 | // information present to determine the original sampled values.
26 | //
27 | // - On-disk, the serialized proto must be gzip-compressed.
28 | //
29 | // - The profile is represented as a set of samples, where each sample
30 | // references a sequence of locations, and where each location belongs
31 | // to a mapping.
32 | // - There is a N->1 relationship from sample.location_id entries to
33 | // locations. For every sample.location_id entry there must be a
34 | // unique Location with that id.
35 | // - There is an optional N->1 relationship from locations to
36 | // mappings. For every nonzero Location.mapping_id there must be a
37 | // unique Mapping with that id.
38 |
39 | syntax = "proto3";
40 |
41 | package perftools.profiles;
42 |
43 | option java_package = "com.google.perftools.profiles";
44 | option java_outer_classname = "ProfileProto";
45 |
46 | message Profile {
47 | // A description of the samples associated with each Sample.value.
48 | // For a cpu profile this might be:
49 | // [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]]
50 | // For a heap profile, this might be:
51 | // [["allocations","count"], ["space","bytes"]],
52 | // If one of the values represents the number of events represented
53 | // by the sample, by convention it should be at index 0 and use
54 | // sample_type.unit == "count".
55 | repeated ValueType sample_type = 1;
56 | // The set of samples recorded in this profile.
57 | repeated Sample sample = 2;
58 | // Mapping from address ranges to the image/binary/library mapped
59 | // into that address range. mapping[0] will be the main binary.
60 | repeated Mapping mapping = 3;
61 | // Locations referenced by samples.
62 | repeated Location location = 4;
63 | // Functions referenced by locations.
64 | repeated Function function = 5;
65 | // A common table for strings referenced by various messages.
66 | // string_table[0] must always be "".
67 | repeated string string_table = 6;
68 | // frames with Function.function_name fully matching the following
69 | // regexp will be dropped from the samples, along with their successors.
70 | int64 drop_frames = 7; // Index into string table.
71 | // frames with Function.function_name fully matching the following
72 | // regexp will be kept, even if it matches drop_frames.
73 | int64 keep_frames = 8; // Index into string table.
74 |
75 | // The following fields are informational, do not affect
76 | // interpretation of results.
77 |
78 | // Time of collection (UTC) represented as nanoseconds past the epoch.
79 | int64 time_nanos = 9;
80 | // Duration of the profile, if a duration makes sense.
81 | int64 duration_nanos = 10;
82 | // The kind of events between sampled occurrences.
83 | // e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
84 | ValueType period_type = 11;
85 | // The number of events between sampled occurrences.
86 | int64 period = 12;
87 | // Free-form text associated with the profile. The text is displayed as is
88 | // to the user by the tools that read profiles (e.g. by pprof). This field
89 | // should not be used to store any machine-readable information, it is only
90 | // for human-friendly content. The profile must stay functional if this field
91 | // is cleaned.
92 | repeated int64 comment = 13; // Indices into string table.
93 | // Index into the string table of the type of the preferred sample
94 | // value. If unset, clients should default to the last sample value.
95 | int64 default_sample_type = 14;
96 | }
97 |
98 | // ValueType describes the semantics and measurement units of a value.
99 | message ValueType {
100 | int64 type = 1; // Index into string table.
101 | int64 unit = 2; // Index into string table.
102 | }
103 |
104 | // Each Sample records values encountered in some program
105 | // context. The program context is typically a stack trace, perhaps
106 | // augmented with auxiliary information like the thread-id, some
107 | // indicator of a higher level request being handled etc.
108 | message Sample {
109 | // The ids recorded here correspond to a Profile.location.id.
110 | // The leaf is at location_id[0].
111 | repeated uint64 location_id = 1;
112 | // The type and unit of each value is defined by the corresponding
113 | // entry in Profile.sample_type. All samples must have the same
114 | // number of values, the same as the length of Profile.sample_type.
115 | // When aggregating multiple samples into a single sample, the
116 | // result has a list of values that is the element-wise sum of the
117 | // lists of the originals.
118 | repeated int64 value = 2;
119 | // label includes additional context for this sample. It can include
120 | // things like a thread id, allocation size, etc.
121 | //
122 | // NOTE: While possible, having multiple values for the same label key is
123 | // strongly discouraged and should never be used. Most tools (e.g. pprof) do
124 | // not have good (or any) support for multi-value labels. And an even more
125 | // discouraged case is having a string label and a numeric label of the same
126 | // name on a sample. Again, possible to express, but should not be used.
127 | repeated Label label = 3;
128 | }
129 |
130 | message Label {
131 | int64 key = 1; // Index into string table
132 |
133 | // At most one of the following must be present
134 | int64 str = 2; // Index into string table
135 | int64 num = 3;
136 |
137 | // Should only be present when num is present.
138 | // Specifies the units of num.
139 | // Use arbitrary string (for example, "requests") as a custom count unit.
140 | // If no unit is specified, consumer may apply heuristic to deduce the unit.
141 | // Consumers may also interpret units like "bytes" and "kilobytes" as memory
142 | // units and units like "seconds" and "nanoseconds" as time units,
143 | // and apply appropriate unit conversions to these.
144 | int64 num_unit = 4; // Index into string table
145 | }
146 |
147 | message Mapping {
148 | // Unique nonzero id for the mapping.
149 | uint64 id = 1;
150 | // Address at which the binary (or DLL) is loaded into memory.
151 | uint64 memory_start = 2;
152 | // The limit of the address range occupied by this mapping.
153 | uint64 memory_limit = 3;
154 | // Offset in the binary that corresponds to the first mapped address.
155 | uint64 file_offset = 4;
156 | // The object this entry is loaded from. This can be a filename on
157 | // disk for the main binary and shared libraries, or virtual
158 | // abstractions like "[vdso]".
159 | int64 filename = 5; // Index into string table
160 | // A string that uniquely identifies a particular program version
161 | // with high probability. E.g., for binaries generated by GNU tools,
162 | // it could be the contents of the .note.gnu.build-id field.
163 | int64 build_id = 6; // Index into string table
164 |
165 | // The following fields indicate the resolution of symbolic info.
166 | bool has_functions = 7;
167 | bool has_filenames = 8;
168 | bool has_line_numbers = 9;
169 | bool has_inline_frames = 10;
170 | }
171 |
172 | // Describes function and line table debug information.
173 | message Location {
174 | // Unique nonzero id for the location. A profile could use
175 | // instruction addresses or any integer sequence as ids.
176 | uint64 id = 1;
177 | // The id of the corresponding profile.Mapping for this location.
178 | // It can be unset if the mapping is unknown or not applicable for
179 | // this profile type.
180 | uint64 mapping_id = 2;
181 | // The instruction address for this location, if available. It
182 | // should be within [Mapping.memory_start...Mapping.memory_limit]
183 | // for the corresponding mapping. A non-leaf address may be in the
184 | // middle of a call instruction. It is up to display tools to find
185 | // the beginning of the instruction if necessary.
186 | uint64 address = 3;
187 | // Multiple line indicates this location has inlined functions,
188 | // where the last entry represents the caller into which the
189 | // preceding entries were inlined.
190 | //
191 | // E.g., if memcpy() is inlined into printf:
192 | // line[0].function_name == "memcpy"
193 | // line[1].function_name == "printf"
194 | repeated Line line = 4;
195 | // Provides an indication that multiple symbols map to this location's
196 | // address, for example due to identical code folding by the linker. In that
197 | // case the line information above represents one of the multiple
198 | // symbols. This field must be recomputed when the symbolization state of the
199 | // profile changes.
200 | bool is_folded = 5;
201 | }
202 |
203 | message Line {
204 | // The id of the corresponding profile.Function for this line.
205 | uint64 function_id = 1;
206 | // Line number in source code.
207 | int64 line = 2;
208 | }
209 |
210 | message Function {
211 | // Unique nonzero id for the function.
212 | uint64 id = 1;
213 | // Name of the function, in human-readable form if available.
214 | int64 name = 2; // Index into string table
215 | // Name of the function, as identified by the system.
216 | // For instance, it can be a C++ mangled name.
217 | int64 system_name = 3; // Index into string table
218 | // Source file containing the function.
219 | int64 filename = 4; // Index into string table
220 | // Line number in source file.
221 | int64 start_line = 5;
222 | }
223 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "flameshow"
3 | version = "1.1.4"
4 | homepage = "https://github.com/laixintao/flameshow"
5 | description = "A Terminal Flamegraph Viewer"
6 | authors = ["laixintao "]
7 | maintainers = ["laixintao "]
8 | readme = "README.md"
9 | license = "GPLv3"
10 | keywords = ["terminal", "golang", "pprof", "flamegraph"]
11 | packages = [{include = "flameshow"}]
12 | include = [
13 | { path = "docs", format = "sdist" },
14 | { path = "tests", format = "sdist" },
15 | { path = "proto", format = "sdist" },
16 | ]
17 |
18 | classifiers = [
19 | "Development Status :: 5 - Production/Stable",
20 | "Environment :: Console",
21 | "Environment :: Console :: Curses",
22 | "Intended Audience :: Developers",
23 | "Operating System :: Microsoft :: Windows :: Windows 10",
24 | "Operating System :: Microsoft :: Windows :: Windows 11",
25 | "Operating System :: MacOS",
26 | "Operating System :: POSIX :: Linux",
27 | # "Programming Language :: Python :: 3.7",
28 | # "Programming Language :: Python :: 3.8",
29 | # "Programming Language :: Python :: 3.9",
30 | "Programming Language :: Python :: 3.10",
31 | "Programming Language :: Python :: 3.11",
32 | "Programming Language :: Python :: 3.12",
33 | "Typing :: Typed",
34 | ]
35 |
36 | [tool.poetry.urls]
37 | "Bug Tracker" = "https://github.com/laixintao/flameshow/issues"
38 |
39 | [tool.poetry.dependencies]
40 | python = "^3.10"
41 | typing-extensions = "^4.7.1"
42 | textual = "^0.37.1"
43 | click = "^8.1.7"
44 | protobuf = "^4.25"
45 | iteround = "^1.0.4"
46 |
47 | [tool.poetry.scripts]
48 | flameshow = 'flameshow.main:main'
49 |
50 | [tool.poetry.group.dev.dependencies]
51 | pytest = "^7.4.2"
52 | pytest-asyncio = "^0.21.1"
53 | ipdb = "^0.13.13"
54 | flake8 = "^6.1.0"
55 | pytest-cov = "^2.12.1"
56 |
57 | [build-system]
58 | requires = ["poetry-core"]
59 | build-backend = "poetry.core.masonry.api"
60 |
61 | [tool.black]
62 | line-length = 79
63 | target-version = ['py37']
64 | preview = true
65 | extend-exclude = '''
66 | # A regex preceded with ^/ will apply only to files and directories
67 | # in the root of the project.
68 | (
69 | .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project
70 | )
71 | '''
72 |
73 | [tool.pytest.ini_options]
74 | asyncio_mode = "auto"
75 |
76 | [tool.coverage.run]
77 | omit = [
78 | "flameshow/pprof_parser/profile_pb2.py",
79 | "flameshow/exceptions.py",
80 | ]
81 |
82 | [tool.coverage.report]
83 | exclude_lines = [
84 | 'pragma: no cover',
85 | 'if TYPE_CHECKING:',
86 | 'if __name__ == "__main__":',
87 | '@overload',
88 | '__rich_repr__',
89 | '@abstractmethod',
90 | 'NotImplementedError',
91 | ]
92 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pathlib
3 | from flameshow.pprof_parser import parse_profile
4 |
5 |
6 | pytest_plugins = ("pytest_asyncio",)
7 |
8 |
9 | @pytest.fixture(scope="session")
10 | def data_dir():
11 | return pathlib.Path(__file__).parent / "pprof_data"
12 |
13 |
14 | @pytest.fixture(scope="session")
15 | def goroutine_pprof():
16 | with open(
17 | pathlib.Path(__file__).parent / "pprof_data/goroutine.out", "rb"
18 | ) as f:
19 | return f.read()
20 |
21 |
22 | @pytest.fixture(scope="session")
23 | def profile10s():
24 | with open(
25 | pathlib.Path(__file__).parent / "pprof_data/profile-10seconds.out",
26 | "rb",
27 | ) as f:
28 | return f.read()
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def profile10s_profile():
33 | with open(
34 | pathlib.Path(__file__).parent / "pprof_data/profile-10seconds.out",
35 | "rb",
36 | ) as f:
37 | return parse_profile(f.read(), "pprof_data/profile-10seconds.out")
38 |
39 |
40 | @pytest.fixture(scope="session")
41 | def simple_collapse_data():
42 | with open(
43 | pathlib.Path(__file__).parent / "stackcollapse_data/simple.txt",
44 | "rb",
45 | ) as f:
46 | return f.read()
47 |
48 |
49 | @pytest.fixture(scope="session")
50 | def collapse_data_with_comment():
51 | with open(
52 | pathlib.Path(__file__).parent / "stackcollapse_data/with_comment.txt",
53 | "rb",
54 | ) as f:
55 | return f.read()
56 |
--------------------------------------------------------------------------------
/tests/pprof_data/goroutine.out:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/pprof_data/goroutine.out
--------------------------------------------------------------------------------
/tests/pprof_data/goroutine_frametree.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": {
3 | "net/http.(*conn).serve": {
4 | "net/http.(*conn).readRequest": {
5 | "net/http.readRequest": {
6 | "net/textproto.(*Reader).ReadLine": {
7 | "net/textproto.(*Reader).readLineSlice": {
8 | "bufio.(*Reader).ReadLine": {
9 | "bufio.(*Reader).ReadSlice": {
10 | "bufio.(*Reader).fill": {
11 | "net/http.(*connReader).Read": {
12 | "net.(*conn).Read": {
13 | "net.(*netFD).Read": {
14 | "internal/poll.(*FD).Read": {
15 | "internal/poll.(*pollDesc).waitRead": {
16 | "internal/poll.(*pollDesc).wait": {
17 | "internal/poll.runtime_pollWait": {
18 | "runtime.netpollblock": {
19 | "runtime.gopark": {}
20 | }
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | },
35 | "net/http.serverHandler.ServeHTTP": {
36 | "net/http.(*ServeMux).ServeHTTP": {
37 | "main.(*handler).ServeHTTP": {
38 | "net/http.HandlerFunc.ServeHTTP": {
39 | "github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerCounter.func1": {
40 | "net/http.HandlerFunc.ServeHTTP": {
41 | "github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerInFlight.func1": {
42 | "net/http.HandlerFunc.ServeHTTP": {
43 | "github.com/prometheus/client_golang/prometheus/promhttp.HandlerFor.func1": {
44 | "github.com/prometheus/client_golang/prometheus.Gatherers.Gather": {
45 | "github.com/prometheus/client_golang/prometheus.(*Registry).Gather": {
46 | "runtime.selectgo": { "runtime.gopark": {} }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 | },
56 | "net/http.HandlerFunc.ServeHTTP": {
57 | "net/http/pprof.Index": {
58 | "net/http/pprof.handler.ServeHTTP": {
59 | "runtime/pprof.(*Profile).WriteTo": {
60 | "runtime/pprof.writeGoroutine": {
61 | "runtime/pprof.writeRuntimeProfile": {
62 | "runtime/pprof.runtime_goroutineProfileWithLabels": {}
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | },
72 | "github.com/godbus/dbus.(*Conn).inWorker": {
73 | "github.com/godbus/dbus.(*unixTransport).ReadMessage": {
74 | "io.ReadFull": {
75 | "io.ReadAtLeast": {
76 | "github.com/godbus/dbus.(*oobReader).Read": {
77 | "net.(*UnixConn).ReadMsgUnix": {
78 | "net.(*UnixConn).readMsg": {
79 | "net.(*netFD).readMsg": {
80 | "internal/poll.(*FD).ReadMsg": {
81 | "internal/poll.(*pollDesc).waitRead": {
82 | "internal/poll.(*pollDesc).wait": {
83 | "internal/poll.runtime_pollWait": {
84 | "runtime.netpollblock": { "runtime.gopark": {} }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
96 | },
97 | "github.com/prometheus/client_golang/prometheus.(*Registry).Gather.func2": {
98 | "sync.(*WaitGroup).Wait": {
99 | "sync.runtime_Semacquire": {
100 | "runtime.semacquire1": {
101 | "runtime.goparkunlock": { "runtime.gopark": {} }
102 | }
103 | }
104 | }
105 | },
106 | "github.com/prometheus/client_golang/prometheus.(*Registry).Gather.func1": {
107 | "github.com/prometheus/node_exporter/collector.NodeCollector.Collect": {
108 | "sync.(*WaitGroup).Wait": {
109 | "sync.runtime_Semacquire": {
110 | "runtime.semacquire1": {
111 | "runtime.goparkunlock": { "runtime.gopark": {} }
112 | }
113 | }
114 | }
115 | }
116 | },
117 | "github.com/prometheus/node_exporter/collector.NodeCollector.Collect.func1": {
118 | "github.com/prometheus/node_exporter/collector.execute": {
119 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).Update": {
120 | "github.com/prometheus/node_exporter/collector.newSystemdDbusConn": {
121 | "github.com/coreos/go-systemd/dbus.New": {
122 | "github.com/coreos/go-systemd/dbus.NewSystemdConnection": {
123 | "github.com/coreos/go-systemd/dbus.NewConnection": {
124 | "github.com/coreos/go-systemd/dbus.NewSystemdConnection.func1": {
125 | "github.com/coreos/go-systemd/dbus.dbusAuthConnection": {
126 | "github.com/godbus/dbus.(*Conn).Auth": {
127 | "github.com/godbus/dbus.(*Conn).tryAuth": {
128 | "github.com/godbus/dbus.authReadLine": {
129 | "bufio.(*Reader).ReadBytes": {
130 | "bufio.(*Reader).collectFragments": {
131 | "bufio.(*Reader).ReadSlice": {
132 | "bufio.(*Reader).fill": {
133 | "net.(*conn).Read": {
134 | "net.(*netFD).Read": {
135 | "internal/poll.(*FD).Read": {
136 | "internal/poll.(*pollDesc).waitRead": {
137 | "internal/poll.(*pollDesc).wait": {
138 | "internal/poll.runtime_pollWait": {
139 | "runtime.netpollblock": {
140 | "runtime.gopark": {}
141 | }
142 | }
143 | }
144 | }
145 | }
146 | }
147 | }
148 | }
149 | }
150 | }
151 | }
152 | }
153 | },
154 | "github.com/godbus/dbus.authReadLine": {
155 | "bufio.(*Reader).ReadBytes": {
156 | "bufio.(*Reader).collectFragments": {
157 | "bufio.(*Reader).ReadSlice": {
158 | "bufio.(*Reader).fill": {
159 | "net.(*conn).Read": {
160 | "net.(*netFD).Read": {
161 | "internal/poll.(*FD).Read": {
162 | "internal/poll.(*pollDesc).waitRead": {
163 | "internal/poll.(*pollDesc).wait": {
164 | "internal/poll.runtime_pollWait": {
165 | "runtime.netpollblock": {
166 | "runtime.gopark": {}
167 | }
168 | }
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 | }
178 | }
179 | }
180 | }
181 | }
182 | }
183 | }
184 | }
185 | },
186 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).getAllUnits": {
187 | "github.com/coreos/go-systemd/dbus.(*Conn).ListUnits": {
188 | "github.com/godbus/dbus.(*Object).Call": {
189 | "runtime.chanrecv1": {
190 | "runtime.chanrecv": { "runtime.gopark": {} }
191 | }
192 | }
193 | }
194 | }
195 | }
196 | }
197 | },
198 | "github.com/godbus/dbus.(*Object).createCall.func2": {
199 | "runtime.chanrecv1": { "runtime.chanrecv": { "runtime.gopark": {} } }
200 | },
201 | "github.com/coreos/go-systemd/dbus.(*Conn).dispatch.func1": {
202 | "runtime.chanrecv2": { "runtime.chanrecv": { "runtime.gopark": {} } }
203 | },
204 | "net/http.(*connReader).backgroundRead": {
205 | "net.(*conn).Read": {
206 | "net.(*netFD).Read": {
207 | "internal/poll.(*FD).Read": {
208 | "internal/poll.(*pollDesc).waitRead": {
209 | "internal/poll.(*pollDesc).wait": {
210 | "internal/poll.runtime_pollWait": {
211 | "runtime.netpollblock": { "runtime.gopark": {} }
212 | }
213 | }
214 | }
215 | }
216 | }
217 | }
218 | },
219 | "runtime.main": {
220 | "main.main": {
221 | "github.com/prometheus/exporter-toolkit/web.ListenAndServe": {
222 | "github.com/prometheus/exporter-toolkit/web.Serve": {
223 | "net/http.(*Server).Serve": {
224 | "net.(*TCPListener).Accept": {
225 | "net.(*TCPListener).accept": {
226 | "net.(*netFD).accept": {
227 | "internal/poll.(*FD).Accept": {
228 | "internal/poll.(*pollDesc).waitRead": {
229 | "internal/poll.(*pollDesc).wait": {
230 | "internal/poll.runtime_pollWait": {
231 | "runtime.netpollblock": { "runtime.gopark": {} }
232 | }
233 | }
234 | }
235 | }
236 | }
237 | }
238 | }
239 | }
240 | }
241 | }
242 | }
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/tests/pprof_data/heap.out:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/pprof_data/heap.out
--------------------------------------------------------------------------------
/tests/pprof_data/profile-10seconds.out:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/pprof_data/profile-10seconds.out
--------------------------------------------------------------------------------
/tests/pprof_data/profile10s_frametree.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": {
3 | "github.com/prometheus/node_exporter/collector.NodeCollector.Collect.func1": {
4 | "github.com/prometheus/node_exporter/collector.execute": {
5 | "github.com/prometheus/node_exporter/collector.(*meminfoCollector).Update": {
6 | "github.com/prometheus/node_exporter/collector.(*meminfoCollector).getMemInfo": {
7 | "github.com/prometheus/node_exporter/collector.procFilePath": {
8 | "path/filepath.Join": {
9 | "path/filepath.join": {
10 | "strings.Join": {
11 | "strings.(*Builder).Grow": {
12 | "strings.(*Builder).grow": {
13 | "runtime.makeslice": {
14 | "runtime.newstack": {
15 | "runtime.copystack": { "runtime.gentraceback": {} }
16 | }
17 | }
18 | }
19 | }
20 | }
21 | }
22 | }
23 | }
24 | }
25 | },
26 | "github.com/prometheus/node_exporter/collector.(*netClassCollector).Update": {
27 | "github.com/prometheus/node_exporter/collector.(*netClassCollector).getNetClassInfo": {
28 | "github.com/prometheus/procfs/sysfs.FS.NetClassByIface": {
29 | "github.com/prometheus/procfs/sysfs.parseNetClassIface": {
30 | "io/ioutil.ReadDir": {
31 | "os.(*File).Readdir": {
32 | "os.(*File).readdir": {
33 | "os.Lstat": {
34 | "os.lstatNolog": {
35 | "os.ignoringEINTR": {
36 | "os.lstatNolog.func1": {
37 | "syscall.Lstat": {
38 | "syscall.fstatat": { "syscall.Syscall6": {} }
39 | }
40 | }
41 | }
42 | }
43 | },
44 | "os.direntReclen": {
45 | "os.readInt": { "os.readIntLE": {} }
46 | }
47 | }
48 | }
49 | },
50 | "github.com/prometheus/procfs/internal/util.SysReadFile": {
51 | "os.Open": {
52 | "os.OpenFile": {
53 | "os.openFileNolog": {
54 | "os.newFile": {
55 | "syscall.SetNonblock": {
56 | "syscall.fcntl": {
57 | "syscall.Syscall": { "runtime.exitsyscall": {} }
58 | }
59 | },
60 | "internal/poll.(*FD).Init": {
61 | "internal/poll.(*pollDesc).init": {
62 | "internal/poll.runtime_pollOpen": {
63 | "runtime.netpollopen": {
64 | "runtime.epollctl": {}
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | },
73 | "os.(*File).Fd": {
74 | "internal/poll.(*FD).SetBlocking": {
75 | "syscall.SetNonblock": {
76 | "syscall.fcntl": { "syscall.Syscall": {} }
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 | },
85 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).Update": {
86 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).getAllUnits": {
87 | "github.com/coreos/go-systemd/dbus.(*Conn).ListUnits": {
88 | "github.com/coreos/go-systemd/dbus.(*Conn).listUnitsInternal": {
89 | "github.com/godbus/dbus.(*Call).Store": {
90 | "github.com/godbus/dbus.Store": {
91 | "github.com/godbus/dbus.storeInterfaces": {
92 | "github.com/godbus/dbus.store": {
93 | "github.com/godbus/dbus.store": {
94 | "github.com/godbus/dbus.storeSlice": {
95 | "github.com/godbus/dbus.storeSliceIntoSlice": {
96 | "github.com/godbus/dbus.store": {
97 | "github.com/godbus/dbus.storeSlice": {
98 | "github.com/godbus/dbus.storeSliceIntoSlice": {
99 | "github.com/godbus/dbus.getVariantValue": {
100 | "github.com/godbus/dbus.isVariant": {
101 | "runtime.ifaceeq": {}
102 | }
103 | }
104 | }
105 | }
106 | }
107 | }
108 | }
109 | }
110 | }
111 | }
112 | }
113 | },
114 | "github.com/godbus/dbus.Store": {
115 | "github.com/godbus/dbus.storeInterfaces": {
116 | "github.com/godbus/dbus.store": {
117 | "github.com/godbus/dbus.store": {
118 | "github.com/godbus/dbus.storeSlice": {
119 | "github.com/godbus/dbus.storeStruct": {
120 | "github.com/godbus/dbus.Store": {
121 | "github.com/godbus/dbus.storeInterfaces": {
122 | "github.com/godbus/dbus.store": {
123 | "github.com/godbus/dbus.store": {
124 | "github.com/godbus/dbus.storeBase": {
125 | "github.com/godbus/dbus.setDest": {
126 | "github.com/godbus/dbus.isVariant": {
127 | "runtime.ifaceeq": {}
128 | }
129 | }
130 | }
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 | }
141 | }
142 | }
143 | }
144 | },
145 | "github.com/prometheus/node_exporter/collector.(*udpQueuesCollector).Update": {
146 | "github.com/prometheus/procfs.FS.NetUDPSummary": {
147 | "github.com/prometheus/procfs.newNetUDPSummary": {
148 | "github.com/prometheus/procfs.newNetIPSocketSummary": {
149 | "bufio.(*Scanner).Scan": {
150 | "io.(*LimitedReader).Read": {
151 | "os.(*File).Read": {
152 | "os.(*File).read": {
153 | "internal/poll.(*FD).Read": {
154 | "internal/poll.ignoringEINTRIO": {
155 | "syscall.Read": {
156 | "syscall.read": { "syscall.Syscall": {} }
157 | }
158 | }
159 | }
160 | }
161 | }
162 | }
163 | }
164 | }
165 | }
166 | }
167 | }
168 | }
169 | },
170 | "runtime.gcBgMarkWorker": {
171 | "runtime.gcMarkDone": {
172 | "runtime.systemstack": {
173 | "runtime.gcMarkDone.func1": {
174 | "runtime.forEachP": {
175 | "runtime.preemptall": {
176 | "runtime.preemptone": {
177 | "runtime.preemptM": {
178 | "runtime.signalM": { "runtime.tgkill": {} }
179 | }
180 | }
181 | },
182 | "runtime.notetsleep": {
183 | "runtime.notetsleep_internal": {
184 | "runtime.futexsleep": { "runtime.futex": {} }
185 | }
186 | }
187 | }
188 | }
189 | }
190 | },
191 | "runtime.systemstack": {
192 | "runtime.gcBgMarkWorker.func2": {
193 | "runtime.gcDrain": {
194 | "runtime.scanobject": {
195 | "runtime.greyobject": { "runtime.pageIndexOf": {} }
196 | }
197 | }
198 | }
199 | }
200 | },
201 | "runtime.mcall": {
202 | "runtime.gosched_m": {
203 | "runtime.goschedImpl": {
204 | "runtime.schedule": {
205 | "runtime.findrunnable": { "runtime.(*randomEnum).next": {} }
206 | }
207 | }
208 | },
209 | "runtime.park_m": {
210 | "runtime.schedule": {
211 | "runtime.findrunnable": {
212 | "runtime.(*randomEnum).next": {},
213 | "runtime.netpoll": { "runtime.epollwait": {} },
214 | "runtime.runqget": {},
215 | "runtime.checkTimers": {},
216 | "runtime.pMask.read": {},
217 | "runtime.stopm": {
218 | "runtime.mPark": {
219 | "runtime.notesleep": {
220 | "runtime.futexsleep": { "runtime.futex": {} }
221 | }
222 | }
223 | },
224 | "runtime.(*randomOrder).start": {},
225 | "runtime.(*randomEnum).position": {},
226 | "runtime.(*randomEnum).done": {}
227 | },
228 | "runtime.resetspinning": {
229 | "runtime.wakep": {
230 | "runtime.startm": {
231 | "runtime.notewakeup": {
232 | "runtime.futexwakeup": { "runtime.futex": {} }
233 | }
234 | }
235 | }
236 | }
237 | }
238 | },
239 | "runtime.goexit0": {
240 | "runtime.schedule": {
241 | "runtime.findrunnable": {
242 | "runtime.netpoll": { "runtime.epollwait": {} }
243 | }
244 | }
245 | }
246 | },
247 | "github.com/godbus/dbus.(*Conn).inWorker": {
248 | "github.com/godbus/dbus.(*unixTransport).ReadMessage": {
249 | "github.com/godbus/dbus.DecodeMessage": {
250 | "github.com/godbus/dbus.(*decoder).Decode": {
251 | "github.com/godbus/dbus.(*decoder).decode": {
252 | "github.com/godbus/dbus.(*decoder).decode": {
253 | "github.com/godbus/dbus.(*decoder).decode": {
254 | "github.com/godbus/dbus.alignment": { "runtime.ifaceeq": {} },
255 | "github.com/godbus/dbus.(*decoder).decode": {
256 | "github.com/godbus/dbus.(*decoder).binread": {
257 | "encoding/binary.Read": {}
258 | }
259 | }
260 | }
261 | }
262 | }
263 | },
264 | "runtime.mapassign": {
265 | "runtime.newobject": {
266 | "runtime.mallocgc": { "runtime.heapBitsSetType": {} }
267 | }
268 | }
269 | },
270 | "io.ReadFull": {
271 | "io.ReadAtLeast": {
272 | "github.com/godbus/dbus.(*oobReader).Read": {
273 | "net.(*UnixConn).ReadMsgUnix": {
274 | "net.(*UnixConn).readMsg": {
275 | "net.(*netFD).readMsg": {
276 | "internal/poll.(*FD).ReadMsg": {
277 | "syscall.Recvmsg": {
278 | "syscall.recvmsg": { "syscall.Syscall": {} }
279 | }
280 | }
281 | }
282 | }
283 | }
284 | }
285 | }
286 | }
287 | }
288 | },
289 | "net/http.(*conn).serve": {
290 | "net/http.serverHandler.ServeHTTP": {
291 | "net/http.(*ServeMux).ServeHTTP": {
292 | "main.(*handler).ServeHTTP": {
293 | "net/http.HandlerFunc.ServeHTTP": {
294 | "github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerCounter.func1": {
295 | "net/http.HandlerFunc.ServeHTTP": {
296 | "github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerInFlight.func1": {
297 | "net/http.HandlerFunc.ServeHTTP": {
298 | "github.com/prometheus/client_golang/prometheus/promhttp.HandlerFor.func1": {
299 | "github.com/prometheus/client_golang/prometheus.Gatherers.Gather": {
300 | "github.com/prometheus/client_golang/prometheus/internal.NormalizeMetricFamilies": {
301 | "sort.Sort": {
302 | "sort.quickSort": {
303 | "sort.quickSort": { "sort.doPivot": {} }
304 | }
305 | }
306 | },
307 | "github.com/prometheus/client_golang/prometheus.(*Registry).Gather": {
308 | "github.com/prometheus/client_golang/prometheus.processMetric": {
309 | "github.com/prometheus/client_golang/prometheus.checkMetricConsistency": {
310 | "runtime.mapassign_fast64": {
311 | "runtime.growWork_fast64": {
312 | "runtime.evacuate_fast64": {}
313 | }
314 | }
315 | },
316 | "runtime.mapaccess2_faststr": { "memeqbody": {} }
317 | },
318 | "github.com/prometheus/client_golang/prometheus/internal.NormalizeMetricFamilies": {
319 | "sort.Sort": {
320 | "sort.quickSort": {
321 | "sort.doPivot": {
322 | "github.com/prometheus/client_golang/prometheus/internal.metricSorter.Less": {
323 | "runtime.memequal": {}
324 | }
325 | }
326 | }
327 | }
328 | }
329 | }
330 | },
331 | "github.com/prometheus/common/expfmt.encoderCloser.Encode": {
332 | "github.com/prometheus/common/expfmt.NewEncoder.func7": {
333 | "github.com/prometheus/common/expfmt.MetricFamilyToText": {
334 | "github.com/prometheus/common/expfmt.MetricFamilyToText.func1": {
335 | "bufio.(*Writer).Flush": {
336 | "compress/gzip.(*Writer).Write": {
337 | "compress/flate.(*Writer).Write": {
338 | "compress/flate.(*compressor).write": {
339 | "compress/flate.(*compressor).deflate": {
340 | "compress/flate.(*compressor).findMatch": {
341 | "compress/flate.matchLen": {}
342 | },
343 | "runtime.asyncPreempt": {}
344 | }
345 | }
346 | }
347 | }
348 | }
349 | },
350 | "github.com/prometheus/common/expfmt.writeSample": {
351 | "github.com/prometheus/common/expfmt.writeLabelPairs": {
352 | "github.com/prometheus/common/expfmt.writeEscapedString": {
353 | "strings.(*Replacer).WriteString": {
354 | "strings.(*byteStringReplacer).WriteString": {
355 | "bufio.(*Writer).WriteString": {}
356 | }
357 | }
358 | }
359 | }
360 | }
361 | }
362 | }
363 | }
364 | }
365 | }
366 | }
367 | }
368 | }
369 | }
370 | }
371 | }
372 | },
373 | "net/http.(*conn).readRequest": {
374 | "net/http.readRequest": {
375 | "net/textproto.(*Reader).ReadLine": {
376 | "net/textproto.(*Reader).readLineSlice": {
377 | "bufio.(*Reader).ReadLine": {
378 | "bufio.(*Reader).ReadSlice": {
379 | "bufio.(*Reader).fill": {
380 | "net/http.(*connReader).Read": {
381 | "net.(*conn).Read": {
382 | "net.(*netFD).Read": {
383 | "internal/poll.(*FD).Read": {
384 | "internal/poll.ignoringEINTRIO": {
385 | "syscall.Read": {
386 | "syscall.read": { "syscall.Syscall": {} }
387 | }
388 | }
389 | }
390 | }
391 | }
392 | }
393 | }
394 | }
395 | }
396 | }
397 | }
398 | }
399 | }
400 | },
401 | "runtime.morestack": {
402 | "runtime.newstack": {
403 | "runtime.gopreempt_m": {
404 | "runtime.goschedImpl": {
405 | "runtime.lock": {
406 | "runtime.lockWithRank": {
407 | "runtime.lock2": { "runtime.procyield": {} }
408 | }
409 | }
410 | }
411 | }
412 | }
413 | },
414 | "golang.org/x/sync/errgroup.(*Group).Go.func1": {
415 | "github.com/prometheus/procfs/sysfs.FS.SystemCpufreq.func1": {
416 | "github.com/prometheus/procfs/sysfs.parseCpufreqCpuinfo": {
417 | "github.com/prometheus/procfs/internal/util.SysReadFile": {
418 | "os.(*File).Close": {
419 | "os.(*file).close": {
420 | "internal/poll.(*FD).Close": {
421 | "internal/poll.(*FD).decref": {
422 | "internal/poll.(*FD).destroy": {
423 | "syscall.Close": { "syscall.Syscall": {} }
424 | }
425 | }
426 | }
427 | }
428 | }
429 | },
430 | "runtime.newobject": {
431 | "runtime.mallocgc": { "runtime.heapBitsSetType": {} }
432 | },
433 | "github.com/prometheus/procfs/internal/util.ReadUintFromFile": {
434 | "io/ioutil.ReadFile": {
435 | "os.ReadFile": {
436 | "os.(*File).Close": {
437 | "os.(*file).close": {
438 | "runtime.SetFinalizer": {
439 | "runtime.systemstack": {
440 | "runtime.SetFinalizer.func1": {
441 | "runtime.removefinalizer": {
442 | "runtime.removespecial": {
443 | "runtime.(*mspan).ensureSwept": {
444 | "runtime.osyield": {}
445 | }
446 | }
447 | }
448 | }
449 | }
450 | }
451 | }
452 | },
453 | "os.Open": {
454 | "os.OpenFile": {
455 | "os.openFileNolog": {
456 | "syscall.Open": {
457 | "syscall.openat": { "syscall.Syscall6": {} }
458 | }
459 | }
460 | }
461 | }
462 | }
463 | }
464 | }
465 | }
466 | }
467 | },
468 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).Update.func1": {
469 | "github.com/prometheus/node_exporter/collector.(*systemdCollector).collectUnitStatusMetrics": {
470 | "github.com/prometheus/client_golang/prometheus.MustNewConstMetric": {
471 | "github.com/prometheus/client_golang/prometheus.NewConstMetric": {
472 | "github.com/prometheus/client_golang/prometheus.validateLabelValues": {
473 | "unicode/utf8.ValidString": {}
474 | }
475 | }
476 | },
477 | "github.com/coreos/go-systemd/dbus.(*Conn).GetUnitTypeProperty": {
478 | "github.com/coreos/go-systemd/dbus.(*Conn).getProperty": {
479 | "github.com/godbus/dbus.(*Object).Call": {
480 | "github.com/godbus/dbus.(*Object).createCall": {
481 | "github.com/godbus/dbus.(*Conn).sendMessageAndIfClosed": {
482 | "github.com/godbus/dbus.(*outputHandler).sendAndIfClosed": {
483 | "github.com/godbus/dbus.(*unixTransport).SendMessage": {
484 | "github.com/godbus/dbus.(*Message).EncodeTo": {
485 | "github.com/godbus/dbus.(*encoder).Encode": {
486 | "github.com/godbus/dbus.(*encoder).encode": {
487 | "github.com/godbus/dbus.(*encoder).encode": {
488 | "github.com/godbus/dbus.(*encoder).encode": {
489 | "github.com/godbus/dbus.(*encoder).encode": {
490 | "bytes.(*Buffer).Write": {
491 | "bytes.(*Buffer).grow": {
492 | "bytes.makeSlice": {
493 | "runtime.makeslice": {
494 | "runtime.mallocgc": {
495 | "runtime.(*mcache).nextFree": {
496 | "runtime.(*mcache).refill": {
497 | "runtime.(*mcentral).cacheSpan": {
498 | "runtime.(*mcentral).grow": {
499 | "runtime.(*mheap).alloc": {
500 | "runtime.memclrNoHeapPointers": {}
501 | }
502 | }
503 | }
504 | }
505 | },
506 | "runtime.memclrNoHeapPointers": {}
507 | }
508 | }
509 | }
510 | }
511 | }
512 | }
513 | }
514 | }
515 | }
516 | }
517 | }
518 | }
519 | }
520 | }
521 | }
522 | }
523 | }
524 | }
525 | }
526 | },
527 | "runtime.bgsweep": {
528 | "runtime.sweepone": {
529 | "runtime.(*mspan).sweep": {
530 | "runtime.newMarkBits": { "runtime.(*gcBitsArena).tryAlloc": {} }
531 | }
532 | }
533 | }
534 | }
535 | }
536 |
--------------------------------------------------------------------------------
/tests/pprof_data/sample_location_line_multiple.json:
--------------------------------------------------------------------------------
1 | {
2 | "SampleType": [
3 | {
4 | "Type": "goroutine",
5 | "Unit": "count"
6 | }
7 | ],
8 | "Sample": [
9 | {
10 | "Label": null,
11 | "Location": [
12 | {
13 | "Address": 4582655,
14 | "ID": 1,
15 | "IsFolded": false,
16 | "Line": [
17 | {
18 | "Function": {
19 | "Filename": "/usr/local/go/src/runtime/traceback.go",
20 | "ID": 1,
21 | "Name": "runtime.gentraceback",
22 | "StartLine": 0,
23 | "SystemName": "runtime.gentraceback"
24 | },
25 | "Line": 177
26 | }
27 | ],
28 | "Mapping": {
29 | "BuildID": "",
30 | "File": "/usr/bin/node-exporter",
31 | "HasFilenames": false,
32 | "HasFunctions": true,
33 | "HasInlineFrames": false,
34 | "HasLineNumbers": false,
35 | "ID": 1,
36 | "KernelRelocationSymbol": "",
37 | "Limit": 11280384,
38 | "Offset": 0,
39 | "Start": 4194304
40 | }
41 | },
42 | {
43 | "Address": 4532272,
44 | "ID": 2,
45 | "IsFolded": false,
46 | "Line": [
47 | {
48 | "Function": {
49 | "Filename": "/usr/local/go/src/runtime/stack.go",
50 | "ID": 2,
51 | "Name": "runtime.copystack",
52 | "StartLine": 0,
53 | "SystemName": "runtime.copystack"
54 | },
55 | "Line": 908
56 | }
57 | ],
58 | "Mapping": {
59 | "BuildID": "",
60 | "File": "/usr/bin/node-exporter",
61 | "HasFilenames": false,
62 | "HasFunctions": true,
63 | "HasInlineFrames": false,
64 | "HasLineNumbers": false,
65 | "ID": 1,
66 | "KernelRelocationSymbol": "",
67 | "Limit": 11280384,
68 | "Offset": 0,
69 | "Start": 4194304
70 | }
71 | },
72 | {
73 | "Address": 4533174,
74 | "ID": 3,
75 | "IsFolded": false,
76 | "Line": [
77 | {
78 | "Function": {
79 | "Filename": "/usr/local/go/src/runtime/stack.go",
80 | "ID": 3,
81 | "Name": "runtime.newstack",
82 | "StartLine": 0,
83 | "SystemName": "runtime.newstack"
84 | },
85 | "Line": 1078
86 | }
87 | ],
88 | "Mapping": {
89 | "BuildID": "",
90 | "File": "/usr/bin/node-exporter",
91 | "HasFilenames": false,
92 | "HasFunctions": true,
93 | "HasInlineFrames": false,
94 | "HasLineNumbers": false,
95 | "ID": 1,
96 | "KernelRelocationSymbol": "",
97 | "Limit": 11280384,
98 | "Offset": 0,
99 | "Start": 4194304
100 | }
101 | },
102 | {
103 | "Address": 4523594,
104 | "ID": 4,
105 | "IsFolded": false,
106 | "Line": [
107 | {
108 | "Function": {
109 | "Filename": "/usr/local/go/src/runtime/slice.go",
110 | "ID": 4,
111 | "Name": "runtime.makeslice",
112 | "StartLine": 0,
113 | "SystemName": "runtime.makeslice"
114 | },
115 | "Line": 83
116 | }
117 | ],
118 | "Mapping": {
119 | "BuildID": "",
120 | "File": "/usr/bin/node-exporter",
121 | "HasFilenames": false,
122 | "HasFunctions": true,
123 | "HasInlineFrames": false,
124 | "HasLineNumbers": false,
125 | "ID": 1,
126 | "KernelRelocationSymbol": "",
127 | "Limit": 11280384,
128 | "Offset": 0,
129 | "Start": 4194304
130 | }
131 | },
132 | {
133 | "Address": 5331654,
134 | "ID": 5,
135 | "IsFolded": false,
136 | "Line": [
137 | {
138 | "Function": {
139 | "Filename": "/usr/local/go/src/strings/builder.go",
140 | "ID": 5,
141 | "Name": "strings.(*Builder).grow",
142 | "StartLine": 0,
143 | "SystemName": "strings.(*Builder).grow"
144 | },
145 | "Line": 68
146 | },
147 | {
148 | "Function": {
149 | "Filename": "/usr/local/go/src/strings/builder.go",
150 | "ID": 6,
151 | "Name": "strings.(*Builder).Grow",
152 | "StartLine": 0,
153 | "SystemName": "strings.(*Builder).Grow"
154 | },
155 | "Line": 82
156 | },
157 | {
158 | "Function": {
159 | "Filename": "/usr/local/go/src/strings/strings.go",
160 | "ID": 7,
161 | "Name": "strings.Join",
162 | "StartLine": 0,
163 | "SystemName": "strings.Join"
164 | },
165 | "Line": 434
166 | }
167 | ],
168 | "Mapping": {
169 | "BuildID": "",
170 | "File": "/usr/bin/node-exporter",
171 | "HasFilenames": false,
172 | "HasFunctions": true,
173 | "HasInlineFrames": false,
174 | "HasLineNumbers": false,
175 | "ID": 1,
176 | "KernelRelocationSymbol": "",
177 | "Limit": 11280384,
178 | "Offset": 0,
179 | "Start": 4194304
180 | }
181 | },
182 | {
183 | "Address": 5810086,
184 | "ID": 6,
185 | "IsFolded": false,
186 | "Line": [
187 | {
188 | "Function": {
189 | "Filename": "/usr/local/go/src/path/filepath/path_unix.go",
190 | "ID": 8,
191 | "Name": "path/filepath.join",
192 | "StartLine": 0,
193 | "SystemName": "path/filepath.join"
194 | },
195 | "Line": 45
196 | }
197 | ],
198 | "Mapping": {
199 | "BuildID": "",
200 | "File": "/usr/bin/node-exporter",
201 | "HasFilenames": false,
202 | "HasFunctions": true,
203 | "HasInlineFrames": false,
204 | "HasLineNumbers": false,
205 | "ID": 1,
206 | "KernelRelocationSymbol": "",
207 | "Limit": 11280384,
208 | "Offset": 0,
209 | "Start": 4194304
210 | }
211 | },
212 | {
213 | "Address": 10874332,
214 | "ID": 7,
215 | "IsFolded": false,
216 | "Line": [
217 | {
218 | "Function": {
219 | "Filename": "/usr/local/go/src/path/filepath/path.go",
220 | "ID": 9,
221 | "Name": "path/filepath.Join",
222 | "StartLine": 0,
223 | "SystemName": "path/filepath.Join"
224 | },
225 | "Line": 213
226 | },
227 | {
228 | "Function": {
229 | "Filename": "/go/src/node_exporter/collector/paths.go",
230 | "ID": 10,
231 | "Name": "github.com/prometheus/node_exporter/collector.procFilePath",
232 | "StartLine": 0,
233 | "SystemName": "github.com/prometheus/node_exporter/collector.procFilePath"
234 | },
235 | "Line": 32
236 | },
237 | {
238 | "Function": {
239 | "Filename": "/go/src/node_exporter/collector/meminfo_linux.go",
240 | "ID": 11,
241 | "Name": "github.com/prometheus/node_exporter/collector.(*meminfoCollector).getMemInfo",
242 | "StartLine": 0,
243 | "SystemName": "github.com/prometheus/node_exporter/collector.(*meminfoCollector).getMemInfo"
244 | },
245 | "Line": 34
246 | }
247 | ],
248 | "Mapping": {
249 | "BuildID": "",
250 | "File": "/usr/bin/node-exporter",
251 | "HasFilenames": false,
252 | "HasFunctions": true,
253 | "HasInlineFrames": false,
254 | "HasLineNumbers": false,
255 | "ID": 1,
256 | "KernelRelocationSymbol": "",
257 | "Limit": 11280384,
258 | "Offset": 0,
259 | "Start": 4194304
260 | }
261 | },
262 | {
263 | "Address": 10872900,
264 | "ID": 8,
265 | "IsFolded": false,
266 | "Line": [
267 | {
268 | "Function": {
269 | "Filename": "/go/src/node_exporter/collector/meminfo.go",
270 | "ID": 12,
271 | "Name": "github.com/prometheus/node_exporter/collector.(*meminfoCollector).Update",
272 | "StartLine": 0,
273 | "SystemName": "github.com/prometheus/node_exporter/collector.(*meminfoCollector).Update"
274 | },
275 | "Line": 50
276 | }
277 | ],
278 | "Mapping": {
279 | "BuildID": "",
280 | "File": "/usr/bin/node-exporter",
281 | "HasFilenames": false,
282 | "HasFunctions": true,
283 | "HasInlineFrames": false,
284 | "HasLineNumbers": false,
285 | "ID": 1,
286 | "KernelRelocationSymbol": "",
287 | "Limit": 11280384,
288 | "Offset": 0,
289 | "Start": 4194304
290 | }
291 | },
292 | {
293 | "Address": 10682787,
294 | "ID": 9,
295 | "IsFolded": false,
296 | "Line": [
297 | {
298 | "Function": {
299 | "Filename": "/go/src/node_exporter/collector/collector.go",
300 | "ID": 13,
301 | "Name": "github.com/prometheus/node_exporter/collector.execute",
302 | "StartLine": 0,
303 | "SystemName": "github.com/prometheus/node_exporter/collector.execute"
304 | },
305 | "Line": 161
306 | }
307 | ],
308 | "Mapping": {
309 | "BuildID": "",
310 | "File": "/usr/bin/node-exporter",
311 | "HasFilenames": false,
312 | "HasFunctions": true,
313 | "HasInlineFrames": false,
314 | "HasLineNumbers": false,
315 | "ID": 1,
316 | "KernelRelocationSymbol": "",
317 | "Limit": 11280384,
318 | "Offset": 0,
319 | "Start": 4194304
320 | }
321 | },
322 | {
323 | "Address": 11206640,
324 | "ID": 10,
325 | "IsFolded": false,
326 | "Line": [
327 | {
328 | "Function": {
329 | "Filename": "/go/src/node_exporter/collector/collector.go",
330 | "ID": 14,
331 | "Name": "github.com/prometheus/node_exporter/collector.NodeCollector.Collect.func1",
332 | "StartLine": 0,
333 | "SystemName": "github.com/prometheus/node_exporter/collector.NodeCollector.Collect.func1"
334 | },
335 | "Line": 152
336 | }
337 | ],
338 | "Mapping": {
339 | "BuildID": "",
340 | "File": "/usr/bin/node-exporter",
341 | "HasFilenames": false,
342 | "HasFunctions": true,
343 | "HasInlineFrames": false,
344 | "HasLineNumbers": false,
345 | "ID": 1,
346 | "KernelRelocationSymbol": "",
347 | "Limit": 11280384,
348 | "Offset": 0,
349 | "Start": 4194304
350 | }
351 | }
352 | ],
353 | "NumLabel": null,
354 | "NumUnit": null,
355 | "Value": [1, 10000000]
356 | }
357 | ]
358 | }
359 |
--------------------------------------------------------------------------------
/tests/stackcollapse_data/simple.txt:
--------------------------------------------------------------------------------
1 | a;b;c 1
2 | a;b;c 1
3 | a;b;d 4
4 | a;b;c 3
5 | a;b 5
6 |
--------------------------------------------------------------------------------
/tests/stackcollapse_data/simple_square.txt:
--------------------------------------------------------------------------------
1 | a;b;[c] 1
2 | a;[b];d 4
3 |
--------------------------------------------------------------------------------
/tests/stackcollapse_data/with_comment.txt:
--------------------------------------------------------------------------------
1 | # austin: 3.7.0
2 | # interval: 100
3 | # mode: wall
4 | # python: 3.12.7
5 | # duration: 828637
6 | # sampling: 3,35,6057
7 | # saturation: 39/5305
8 | # errors: 820/5305
9 |
10 |
11 | P245906;T0:245906 16905
12 | P245906;T0:245906;::1546 158
13 |
--------------------------------------------------------------------------------
/tests/test_cli_click.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest.mock import MagicMock, patch
3 |
4 | from click.testing import CliRunner
5 |
6 | from flameshow.main import ensure_tty, main
7 |
8 |
9 | def test_print_version():
10 | runner = CliRunner()
11 | with patch("flameshow.main.__version__", "1.2.3"):
12 | result = runner.invoke(main, ["--version"])
13 | assert result.exit_code == 0
14 | assert result.output == "1.2.3\n"
15 |
16 |
17 | @patch("flameshow.main.os")
18 | def test_run_app(mock_os, data_dir):
19 | mock_os.isatty.return_value = True
20 | runner = CliRunner()
21 | result = runner.invoke(
22 | main, [str(data_dir / "profile-10seconds.out")], input="q"
23 | )
24 |
25 | assert result.exit_code == 0
26 |
27 |
28 | @patch("flameshow.main.sys")
29 | @patch("flameshow.main.os")
30 | def test_ensure_tty_when_its_not(mock_os, mock_sys):
31 | mock_os.isatty.return_value = False
32 | opened_fd = object()
33 | mock_os.fdopen.return_value = opened_fd
34 |
35 | fake_stdin = MagicMock()
36 | mock_sys.stdin = fake_stdin
37 |
38 | ensure_tty()
39 |
40 | fake_stdin.close.assert_called()
41 | assert hasattr(mock_sys, "stdin")
42 | assert mock_sys.stdin is opened_fd
43 |
--------------------------------------------------------------------------------
/tests/test_colors.py:
--------------------------------------------------------------------------------
1 | from flameshow.colors import LinaerColorPlatte, flamegraph_random_color_platte
2 | from textual.color import Color
3 |
4 |
5 | def test_linaer_color_platte():
6 | platte = LinaerColorPlatte()
7 |
8 | color1 = platte.get_color("1")
9 | color3 = platte.get_color("1")
10 |
11 | color2 = platte.get_color("2")
12 | assert color1 is color3
13 |
14 | assert isinstance(color2, Color)
15 |
16 | for key in range(999):
17 | platte.get_color(key)
18 |
19 |
20 | def test_flamegraph_random_color_platte():
21 | platte = flamegraph_random_color_platte
22 |
23 | color1 = platte.get_color("1")
24 | color3 = platte.get_color("1")
25 |
26 | color2 = platte.get_color("2")
27 | assert color1 is color3
28 |
29 | assert isinstance(color2, Color)
30 |
--------------------------------------------------------------------------------
/tests/test_integration/test_app.py:
--------------------------------------------------------------------------------
1 | from flameshow.render.app import FlameshowApp
2 |
3 |
4 | async def test_app_startup(profile10s_profile):
5 | app = FlameshowApp(profile10s_profile)
6 | async with app.run_test() as pilot:
7 | center_text = app.query_one("HeaderOpenedFilename")
8 | assert (
9 | center_text.filename
10 | == "pprof_data/profile-10seconds.out: (cpu, nanoseconds)"
11 | )
12 |
13 | # test moving around
14 | await pilot.press("j")
15 | flamegraph_widget = app.query_one("FlameGraph")
16 | assert flamegraph_widget.view_frame._id == 34
17 | assert flamegraph_widget.view_frame.name == "runtime.mcall"
18 |
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flameshow.main import setup_log
3 | import os
4 |
5 | from unittest.mock import patch
6 |
7 |
8 | def test_run_app_with_verbose_logging(data_dir):
9 | # cleanup logfile first
10 | path = data_dir / "._pytest_flameshow.log"
11 | try:
12 | os.remove(path)
13 | except: # noqa
14 | pass
15 |
16 | with patch.object(logging, "basicConfig") as mock_config:
17 | setup_log(True, logging.DEBUG, path)
18 |
19 | mock_config.assert_called_once()
20 |
--------------------------------------------------------------------------------
/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 |
4 | from flameshow.models import Profile, SampleType
5 | from flameshow.pprof_parser.parser import Frame, PprofFrame, ProfileParser
6 |
7 |
8 | def test_parse_max_depth_when_have_multiple_lines(profile10s):
9 | parser = ProfileParser("abc")
10 |
11 | profile = parser.parse(profile10s)
12 | assert profile.highest_lines == 26
13 |
14 |
15 | def test_pile_up():
16 | root = Frame("root", 0, values=[5])
17 | s1 = Frame("s1", 1, values=[4], parent=root)
18 | s2 = Frame("s2", 2, values=[4], parent=s1)
19 |
20 | root.children = [s1]
21 | s1.children = [s2]
22 |
23 | s1 = Frame("s1", 3, values=[3], parent=None)
24 | s2 = Frame("s2", 4, values=[2], parent=s1)
25 | s1.children = [s2]
26 |
27 | root.pile_up(s1)
28 | assert root.children[0].values == [7]
29 | assert root.children[0].children[0].values == [6]
30 |
31 |
32 | def test_profile_creataion():
33 | root = Frame("root", 0, values=[5])
34 | s1 = Frame("s1", 1, values=[4], parent=root)
35 | s2 = Frame("s2", 2, values=[1], parent=s1)
36 | s3 = Frame("s3", 3, values=[2], parent=s1)
37 |
38 | root.children = [s1]
39 | s1.children = [s2, s3]
40 |
41 | p = Profile(
42 | filename="abc",
43 | root_stack=root,
44 | highest_lines=1,
45 | total_sample=2,
46 | sample_types=[SampleType("goroutine", "count")],
47 | id_store={
48 | 0: root,
49 | 1: s1,
50 | 2: s2,
51 | 3: s3,
52 | },
53 | )
54 | assert p.lines == [[root], [s1], [s2, s3]]
55 |
56 |
57 | def test_frame():
58 | f1 = Frame("foo", 12)
59 | f2 = Frame("bar", 12)
60 |
61 | assert f1 == f2
62 | assert f1 != Frame("a", 13)
63 | assert f1 != 123
64 |
65 | f4 = Frame("has_child", 14, [f1])
66 | assert f4.children == [f1]
67 |
68 | assert str(f1) == ""
69 |
70 |
71 | def test_frame_get_color():
72 | with patch("flameshow.models.r") as mock_r:
73 | mock_r.get_color.return_value = "#1122ff"
74 | f1 = Frame("foo", 12)
75 | assert f1.display_color == "#1122ff"
76 | mock_r.get_color.assert_called_with("foo")
77 |
78 |
79 | def test_frame_get_color_full_model_path():
80 | with patch("flameshow.models.r") as mock_r:
81 | mock_r.get_color.return_value = "#1122ff"
82 | f1 = PprofFrame(
83 | "github.com/prometheus/common/expfmt.MetricFamilyToText.func1", 12
84 | )
85 | assert f1.display_color == "#1122ff"
86 | mock_r.get_color.assert_called_with("expfmt")
87 |
--------------------------------------------------------------------------------
/tests/test_pprof_parse/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/test_pprof_parse/__init__.py
--------------------------------------------------------------------------------
/tests/test_pprof_parse/test_golang_pprof.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | from flameshow.models import Frame, Profile, SampleType
4 |
5 | from flameshow.pprof_parser.parser import ProfileParser
6 | from flameshow.pprof_parser.parser import (
7 | Function,
8 | Line,
9 | Location,
10 | Mapping,
11 | ProfileParser,
12 | get_frame_tree,
13 | parse_profile,
14 | unmarshal,
15 | )
16 |
17 | from ..utils import create_frame
18 |
19 |
20 | def test_python_protobuf_goroutine_check_frame_tree(goroutine_pprof, data_dir):
21 | profile = parse_profile(goroutine_pprof, "profile10s.out")
22 | frame_tree = get_frame_tree(profile.root_stack)
23 |
24 | with open(data_dir / "goroutine_frametree.json") as f:
25 | expected = json.load(f)
26 |
27 | assert frame_tree == expected
28 |
29 |
30 | def test_golang_goroutine_parse_using_protobuf(goroutine_pprof):
31 | profile = parse_profile(goroutine_pprof, "goroutine.out")
32 | assert len(profile.sample_types) == 1
33 |
34 | st = profile.sample_types[0]
35 | assert st.sample_type == "goroutine"
36 | assert st.sample_unit == "count"
37 |
38 | assert profile.created_at == datetime.datetime(
39 | 2023, 9, 9, 7, 8, 29, 664363, tzinfo=datetime.timezone.utc
40 | )
41 |
42 | assert profile.period_type.sample_type == "goroutine"
43 | assert profile.period_type.sample_unit == "count"
44 | assert profile.period == 1
45 | assert profile.default_sample_type_index == -1
46 | assert profile.highest_lines == 24
47 |
48 |
49 | def test_golang_profile10s_parse_using_protobuf(profile10s):
50 | profile = parse_profile(profile10s, "profile10s.out")
51 | assert len(profile.sample_types) == 2
52 |
53 | st = profile.sample_types[0]
54 | assert st.sample_type == "samples"
55 | assert st.sample_unit == "count"
56 | st = profile.sample_types[1]
57 | assert st.sample_type == "cpu"
58 | assert st.sample_unit == "nanoseconds"
59 |
60 | assert profile.created_at == datetime.datetime(
61 | 2023, 9, 9, 7, 8, 29, 866118, tzinfo=datetime.timezone.utc
62 | )
63 |
64 | assert profile.period_type.sample_type == "cpu"
65 | assert profile.period_type.sample_unit == "nanoseconds"
66 | assert profile.period == 10000000
67 |
68 | assert profile.default_sample_type_index == -1
69 |
70 |
71 | def test_protobuf_parse_gorouting_mapping(goroutine_pprof):
72 | pb_binary = unmarshal(goroutine_pprof)
73 | parser = ProfileParser("goroutine.out")
74 | parser.parse_internal_data(pb_binary)
75 | assert parser.mappings == {
76 | 1: Mapping(
77 | id=1,
78 | memory_start=4194304,
79 | memory_limit=11280384,
80 | file_offset=0,
81 | filename="/usr/bin/node-exporter",
82 | build_id="",
83 | has_functions=True,
84 | has_filenames=False,
85 | has_line_numbers=False,
86 | has_inline_frames=False,
87 | ),
88 | 2: Mapping(
89 | id=2,
90 | memory_start=140721318682624,
91 | memory_limit=140721318690816,
92 | file_offset=0,
93 | filename="[vdso]",
94 | build_id="",
95 | has_functions=False,
96 | has_filenames=False,
97 | has_line_numbers=False,
98 | has_inline_frames=False,
99 | ),
100 | 3: Mapping(
101 | id=3,
102 | memory_start=18446744073699065856,
103 | memory_limit=18446744073699069952,
104 | file_offset=0,
105 | filename="[vsyscall]",
106 | build_id="",
107 | has_functions=False,
108 | has_filenames=False,
109 | has_line_numbers=False,
110 | has_inline_frames=False,
111 | ),
112 | }
113 | assert parser.locations[1] == Location(
114 | id=1,
115 | mapping=Mapping(
116 | id=1,
117 | memory_start=4194304,
118 | memory_limit=11280384,
119 | file_offset=0,
120 | filename="/usr/bin/node-exporter",
121 | build_id="",
122 | has_functions=True,
123 | has_filenames=False,
124 | has_line_numbers=False,
125 | has_inline_frames=False,
126 | ),
127 | address=4435364,
128 | lines=[
129 | Line(
130 | line_no=336,
131 | function=Function(
132 | id=1,
133 | filename="/usr/local/go/src/runtime/proc.go",
134 | name="runtime.gopark",
135 | start_line=0,
136 | system_name="runtime.gopark",
137 | ),
138 | )
139 | ],
140 | is_folded=False,
141 | )
142 | assert parser.functions[1] == Function(
143 | id=1,
144 | filename="/usr/local/go/src/runtime/proc.go",
145 | name="runtime.gopark",
146 | start_line=0,
147 | system_name="runtime.gopark",
148 | )
149 |
150 |
151 | def test_parser_get_name_aggr():
152 | root = create_frame(
153 | {
154 | "id": 0,
155 | "values": [10],
156 | "children": [
157 | {"id": 1, "values": [3], "children": []},
158 | {"id": 2, "values": [4], "children": []},
159 | ],
160 | }
161 | )
162 | p = Profile(
163 | filename="abc",
164 | root_stack=root,
165 | highest_lines=1,
166 | total_sample=2,
167 | sample_types=[SampleType("goroutine", "count")],
168 | id_store={},
169 | )
170 | name_aggr = p.name_aggr
171 | assert name_aggr["node-0"] == [Frame("", 0)]
172 | assert name_aggr["node-1"] == [Frame("", 1)]
173 | assert name_aggr["node-2"] == [Frame("", 2)]
174 |
175 |
176 | def test_parser_get_name_aggr_with_nested():
177 | root = create_frame(
178 | {
179 | "id": 0,
180 | "values": [10],
181 | "children": [
182 | {
183 | "id": 1,
184 | "name": "foo",
185 | "values": [3],
186 | "children": [
187 | {
188 | "id": 10,
189 | "values": [3],
190 | "children": [
191 | {
192 | "id": 11,
193 | "values": [3],
194 | "children": [
195 | {
196 | "id": 21,
197 | "values": [3],
198 | "children": [],
199 | "name": "bar",
200 | },
201 | ],
202 | "name": "foo",
203 | },
204 | ],
205 | },
206 | ],
207 | },
208 | {"id": 2, "values": [4], "children": [], "name": "foo"},
209 | ],
210 | }
211 | )
212 |
213 | p = Profile(
214 | filename="abc",
215 | root_stack=root,
216 | highest_lines=1,
217 | total_sample=2,
218 | sample_types=[SampleType("goroutine", "count")],
219 | id_store={},
220 | )
221 |
222 | name_aggr = p.name_aggr
223 | assert name_aggr["node-0"] == [Frame("", 0)]
224 | assert name_aggr["foo"] == [Frame("", 1), Frame("", 2)]
225 | assert name_aggr["bar"] == [Frame("", 21)]
226 |
227 |
228 | def test_parser_get_name_aggr_with_previous_occrance():
229 | root = create_frame(
230 | {
231 | "id": 0,
232 | "values": [10],
233 | "children": [
234 | {
235 | "id": 1,
236 | "name": "foo",
237 | "values": [3],
238 | "children": [],
239 | },
240 | {
241 | "id": 2,
242 | "values": [4],
243 | "children": [
244 | {
245 | "id": 3,
246 | "name": "foo",
247 | "values": [2],
248 | "children": [],
249 | },
250 | ],
251 | "name": "bar",
252 | },
253 | ],
254 | }
255 | )
256 |
257 | p = Profile(
258 | filename="abc",
259 | root_stack=root,
260 | highest_lines=1,
261 | total_sample=2,
262 | sample_types=[SampleType("goroutine", "count")],
263 | id_store={},
264 | )
265 |
266 | name_aggr = p.name_aggr
267 | assert name_aggr["foo"] == [Frame("", 1), Frame("", 3)]
268 |
--------------------------------------------------------------------------------
/tests/test_profile_parser.py:
--------------------------------------------------------------------------------
1 | from flameshow.pprof_parser.parser import Line, PprofFrame, Frame
2 |
3 |
4 | def test_pile_up():
5 | root = Frame("root", 0, values=[5])
6 | s1 = Frame("s1", 1, values=[4], parent=root)
7 | s2 = Frame("s2", 2, values=[4], parent=s1)
8 |
9 | root.children = [s1]
10 | s1.children = [s2]
11 |
12 | s1 = Frame("s1", 3, values=[3], parent=None)
13 | s2 = Frame("s2", 4, values=[2], parent=s1)
14 | s1.children = [s2]
15 |
16 | root.pile_up(s1)
17 | assert root.children[0].values == [7]
18 | assert root.children[0].children[0].values == [6]
19 |
20 |
21 | def test_render_detail_when_parent_zero():
22 | root = PprofFrame("root", 0, values=[0])
23 | s1 = PprofFrame("s1", 1, values=[0], parent=root, root=root)
24 | s1.line = Line()
25 | s1.line.function.name = "asdf"
26 |
27 | detail = s1.render_detail(0, "bytes")
28 | assert "asdf: 0.0B" in str(detail)
29 |
--------------------------------------------------------------------------------
/tests/test_render/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/test_render/__init__.py
--------------------------------------------------------------------------------
/tests/test_render/test_app.py:
--------------------------------------------------------------------------------
1 | from textual.geometry import Size
2 | from flameshow.models import Frame, Profile, SampleType
3 | from flameshow.render.app import FlameGraphScroll, FlameshowApp
4 | from unittest.mock import MagicMock, patch, PropertyMock
5 |
6 |
7 | def test_flamegraph_container_scroll():
8 | def _test_scroll(line_no, height, expected_center):
9 | fc = FlameGraphScroll()
10 | size = MagicMock()
11 | size.height = height
12 |
13 | with patch(
14 | "flameshow.render.app.FlameGraphScroll.size",
15 | new_callable=PropertyMock,
16 | ) as mock_size:
17 | mock_size.return_value = Size(0, height)
18 | to_line = fc.scroll_to_make_line_center(line_no)
19 |
20 | assert to_line == expected_center
21 |
22 | _test_scroll(line_no=0, height=10, expected_center=0)
23 | _test_scroll(line_no=3, height=10, expected_center=0)
24 | _test_scroll(line_no=4, height=10, expected_center=0)
25 | _test_scroll(line_no=5, height=10, expected_center=0)
26 | _test_scroll(line_no=6, height=10, expected_center=1)
27 | _test_scroll(line_no=10, height=10, expected_center=5)
28 | _test_scroll(line_no=20, height=10, expected_center=15)
29 |
30 |
31 | def test_app_set_title_after_mount():
32 | r = Frame("root", 0)
33 | p = Profile(
34 | filename="abc",
35 | root_stack=r,
36 | highest_lines=1,
37 | total_sample=2,
38 | sample_types=[SampleType("goroutine", "count")],
39 | id_store={},
40 | )
41 | app = FlameshowApp(p)
42 | app.on_mount()
43 | assert app.title == "flameshow"
44 | assert app.sub_title.startswith("v")
45 | assert app.view_frame == r
46 | assert app.sample_unit == "count"
47 |
--------------------------------------------------------------------------------
/tests/test_render/test_flamegraph.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 |
3 | import pytest
4 | from textual.events import MouseMove
5 |
6 | from flameshow.exceptions import RenderException
7 | from flameshow.models import Frame
8 | from flameshow.pprof_parser.parser import Profile, SampleType
9 | from flameshow.render.flamegraph import FlameGraph, FrameMap, add_array
10 |
11 | from ..utils import create_frame
12 |
13 |
14 | def test_flamegraph_generate_frame_maps_parents_with_only_child():
15 | root = Frame("root", 0, values=[5])
16 | s1 = Frame("s1", 1, values=[4], parent=root)
17 | s2 = Frame("s2", 2, values=[2], parent=s1)
18 |
19 | root.children = [s1]
20 | s1.children = [s2]
21 |
22 | p = Profile(
23 | filename="abc",
24 | root_stack=root,
25 | highest_lines=1,
26 | total_sample=2,
27 | sample_types=[SampleType("goroutine", "count")],
28 | id_store={
29 | 0: root,
30 | 1: s1,
31 | 2: s2,
32 | },
33 | )
34 | flamegraph_widget = FlameGraph(p, 0, -1, 0)
35 |
36 | # focus on 0 root
37 | frame_maps = flamegraph_widget.generate_frame_maps(20, 0)
38 |
39 | assert frame_maps == {
40 | 0: [FrameMap(offset=0, width=20)],
41 | 1: [FrameMap(offset=0, width=16)],
42 | 2: [FrameMap(offset=0, width=8)],
43 | }
44 |
45 |
46 | def test_add_array():
47 | assert add_array([1, 2, 3], [4, 5, 6]) == [5, 7, 9]
48 |
49 |
50 | def test_flamegraph_generate_frame_maps():
51 | root = Frame("root", 0, values=[5])
52 | s1 = Frame("s1", 1, values=[4], parent=root)
53 | s2 = Frame("s2", 2, values=[1], parent=s1)
54 | s3 = Frame("s3", 3, values=[2], parent=s1)
55 |
56 | root.children = [s1]
57 | s1.children = [s2, s3]
58 |
59 | p = Profile(
60 | filename="abc",
61 | root_stack=root,
62 | highest_lines=1,
63 | total_sample=2,
64 | sample_types=[SampleType("goroutine", "count")],
65 | id_store={
66 | 0: root,
67 | 1: s1,
68 | 2: s2,
69 | 3: s3,
70 | },
71 | )
72 | flamegraph_widget = FlameGraph(p, 0, -1, 0)
73 |
74 | # focus on 0 root
75 | frame_maps = flamegraph_widget.generate_frame_maps(20, 1)
76 |
77 | assert frame_maps == {
78 | 0: [FrameMap(offset=0, width=20)],
79 | 1: [FrameMap(offset=0, width=20)],
80 | 2: [FrameMap(offset=0, width=5)],
81 | 3: [FrameMap(offset=5, width=10)],
82 | }
83 |
84 | # focus on 1
85 | frame_maps = flamegraph_widget.generate_frame_maps(20, 1)
86 |
87 | assert frame_maps == {
88 | 0: [FrameMap(offset=0, width=20)],
89 | 1: [FrameMap(offset=0, width=20)],
90 | 2: [FrameMap(offset=0, width=5)],
91 | 3: [FrameMap(offset=5, width=10)],
92 | }
93 |
94 |
95 | def test_flamegraph_generate_frame_maps_child_width_0():
96 | root = Frame("root", 0, values=[5])
97 | s1 = Frame("s1", 1, values=[4], parent=root)
98 | s2 = Frame("s2", 2, values=[0], parent=s1)
99 |
100 | root.children = [s1]
101 | s1.children = [s2]
102 |
103 | p = Profile(
104 | filename="abc",
105 | root_stack=root,
106 | highest_lines=1,
107 | total_sample=2,
108 | sample_types=[SampleType("goroutine", "count")],
109 | id_store={
110 | 0: root,
111 | 1: s1,
112 | 2: s2,
113 | },
114 | )
115 | flamegraph_widget = FlameGraph(p, 0, -1, 0)
116 |
117 | # focus on 0 root
118 | frame_maps = flamegraph_widget.generate_frame_maps(20, 1)
119 |
120 | assert frame_maps == {
121 | 1: [FrameMap(offset=0, width=20)],
122 | 0: [FrameMap(offset=0, width=20)],
123 | 2: [FrameMap(offset=0, width=00)],
124 | }
125 |
126 |
127 | def test_flamegraph_render_line():
128 | root = Frame("root", 0, values=[10])
129 | s1 = Frame("s1", 1, values=[4], parent=root)
130 | s2 = Frame("s2", 2, values=[3], parent=root)
131 |
132 | root.children = [s1, s2]
133 |
134 | p = Profile(
135 | filename="abc",
136 | root_stack=root,
137 | highest_lines=1,
138 | total_sample=2,
139 | sample_types=[SampleType("goroutine", "count")],
140 | id_store={
141 | 0: root,
142 | 1: s1,
143 | 2: s2,
144 | },
145 | )
146 | flamegraph_widget = FlameGraph(p, 0, -1, root)
147 | flamegraph_widget.frame_maps = flamegraph_widget.generate_frame_maps(
148 | 10, focused_stack_id=0
149 | )
150 |
151 | strip = flamegraph_widget.render_line(
152 | 1,
153 | )
154 |
155 | line_strings = [seg.text for seg in strip._segments]
156 |
157 | assert line_strings == ["▏", "s1 ", "▏", "s2"]
158 |
159 |
160 | def test_flamegraph_render_line_without_init():
161 | root = Frame("root", 0, values=[10])
162 | s1 = Frame("s1", 1, values=[4], parent=root)
163 | s2 = Frame("s2", 2, values=[3], parent=root)
164 |
165 | root.children = [s1, s2]
166 |
167 | p = Profile(
168 | filename="abc",
169 | root_stack=root,
170 | highest_lines=1,
171 | total_sample=2,
172 | sample_types=[SampleType("goroutine", "count")],
173 | id_store={
174 | 0: root,
175 | 1: s1,
176 | 2: s2,
177 | },
178 | )
179 | flamegraph_widget = FlameGraph(p, 0, -1, 0)
180 |
181 | with pytest.raises(RenderException):
182 | flamegraph_widget.render_line(
183 | 1,
184 | )
185 |
186 |
187 | def test_flamegraph_action_zoom_in_zoom_out():
188 | root = Frame("root", 123, values=[5])
189 | s1 = Frame("s1", 42, values=[1])
190 |
191 | p = Profile(
192 | filename="abc",
193 | root_stack=root,
194 | highest_lines=1,
195 | total_sample=2,
196 | sample_types=[SampleType("goroutine", "count")],
197 | id_store={},
198 | )
199 | flamegraph_widget = FlameGraph(p, 0, -1, 0)
200 | flamegraph_widget.focused_stack_id = 333
201 |
202 | flamegraph_widget.action_zoom_out()
203 |
204 | assert flamegraph_widget.focused_stack_id == 123
205 |
206 | flamegraph_widget.view_frame = s1
207 | flamegraph_widget.action_zoom_in()
208 | assert flamegraph_widget.focused_stack_id == 42
209 |
210 |
211 | def test_flamegraph_action_move_down():
212 | root = create_frame(
213 | {
214 | "id": 0,
215 | "values": [10],
216 | "children": [
217 | {"id": 1, "values": [3], "children": []},
218 | {"id": 2, "values": [4], "children": []},
219 | ],
220 | }
221 | )
222 |
223 | p = Profile(
224 | filename="abc",
225 | root_stack=root,
226 | highest_lines=10,
227 | total_sample=10,
228 | sample_types=[SampleType("goroutine", "count")],
229 | id_store={},
230 | )
231 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=root)
232 | flamegraph_widget.post_message = MagicMock()
233 | flamegraph_widget.action_move_down()
234 |
235 | flamegraph_widget.post_message.assert_called_once()
236 | args = flamegraph_widget.post_message.call_args[0]
237 | message = args[0]
238 | assert message.by_mouse == False
239 | assert message.frame._id == 2
240 |
241 | assert str(message) == "ViewFrameChanged(self.frame=)"
242 |
243 |
244 | def test_flamegraph_action_move_down_no_more_children():
245 | root = create_frame(
246 | {
247 | "id": 0,
248 | "values": [10],
249 | "children": [],
250 | }
251 | )
252 |
253 | p = Profile(
254 | filename="abc",
255 | root_stack=root,
256 | highest_lines=10,
257 | total_sample=10,
258 | sample_types=[SampleType("goroutine", "count")],
259 | id_store={},
260 | )
261 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=root)
262 | flamegraph_widget.post_message = MagicMock()
263 | flamegraph_widget.action_move_down()
264 |
265 | flamegraph_widget.post_message.assert_not_called()
266 |
267 |
268 | def test_flamegraph_action_move_down_children_is_zero():
269 | root = create_frame(
270 | {
271 | "id": 0,
272 | "values": [10],
273 | "children": [
274 | {"id": 1, "values": [0], "children": []},
275 | ],
276 | }
277 | )
278 |
279 | p = Profile(
280 | filename="abc",
281 | root_stack=root,
282 | highest_lines=10,
283 | total_sample=10,
284 | sample_types=[SampleType("goroutine", "count")],
285 | id_store={},
286 | )
287 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=root)
288 | flamegraph_widget.post_message = MagicMock()
289 | flamegraph_widget.action_move_down()
290 |
291 | flamegraph_widget.post_message.assert_called_once()
292 | args = flamegraph_widget.post_message.call_args[0]
293 | message = args[0]
294 | assert message.by_mouse == False
295 | assert message.frame._id == 1
296 |
297 |
298 | def test_flamegraph_action_move_up():
299 | id_store = {}
300 | root = create_frame(
301 | {
302 | "id": 0,
303 | "values": [10],
304 | "children": [
305 | {
306 | "id": 1,
307 | "values": [2],
308 | "children": [
309 | {"id": 3, "values": [1], "children": []},
310 | ],
311 | },
312 | {"id": 2, "values": [3], "children": []},
313 | ],
314 | },
315 | id_store,
316 | )
317 |
318 | p = Profile(
319 | filename="abc",
320 | root_stack=root,
321 | highest_lines=10,
322 | total_sample=10,
323 | sample_types=[SampleType("goroutine", "count")],
324 | id_store=id_store,
325 | )
326 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[3])
327 | flamegraph_widget.post_message = MagicMock()
328 | flamegraph_widget.action_move_up()
329 |
330 | flamegraph_widget.post_message.assert_called_once()
331 | args = flamegraph_widget.post_message.call_args[0]
332 | message = args[0]
333 | assert message.by_mouse == False
334 | assert message.frame._id == 1
335 |
336 | # move up but no more parents
337 | flamegraph_widget.post_message = MagicMock()
338 | flamegraph_widget.view_frame = root
339 | flamegraph_widget.action_move_up()
340 | flamegraph_widget.post_message.assert_not_called()
341 |
342 |
343 | def test_flamegraph_action_move_right_sibling_just_here():
344 | id_store = {}
345 | root = create_frame(
346 | {
347 | "id": 0,
348 | "values": [10],
349 | "children": [
350 | {
351 | "id": 1,
352 | "values": [2],
353 | "children": [],
354 | },
355 | {"id": 2, "values": [3], "children": []},
356 | ],
357 | },
358 | id_store,
359 | )
360 |
361 | p = Profile(
362 | filename="abc",
363 | root_stack=root,
364 | highest_lines=10,
365 | total_sample=10,
366 | sample_types=[SampleType("goroutine", "count")],
367 | id_store=id_store,
368 | )
369 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[1])
370 | flamegraph_widget.post_message = MagicMock()
371 | flamegraph_widget.action_move_right()
372 |
373 | flamegraph_widget.post_message.assert_called_once()
374 | args = flamegraph_widget.post_message.call_args[0]
375 | message = args[0]
376 | assert message.by_mouse == False
377 | assert message.frame._id == 2
378 |
379 | # no more sibling
380 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[2])
381 | flamegraph_widget.post_message = MagicMock()
382 | flamegraph_widget.action_move_right()
383 |
384 | flamegraph_widget.post_message.assert_not_called()
385 |
386 |
387 | def test_flamegraph_action_move_right_sibling_goes_to_parent():
388 | id_store = {}
389 | root = create_frame(
390 | {
391 | "id": 0,
392 | "values": [10],
393 | "children": [
394 | {
395 | "id": 1,
396 | "values": [2],
397 | "children": [
398 | {"id": 3, "values": [1], "children": []},
399 | ],
400 | },
401 | {"id": 2, "values": [3], "children": []},
402 | ],
403 | },
404 | id_store,
405 | )
406 |
407 | p = Profile(
408 | filename="abc",
409 | root_stack=root,
410 | highest_lines=10,
411 | total_sample=10,
412 | sample_types=[SampleType("goroutine", "count")],
413 | id_store=id_store,
414 | )
415 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[3])
416 | flamegraph_widget.post_message = MagicMock()
417 | flamegraph_widget.action_move_right()
418 |
419 | flamegraph_widget.post_message.assert_called_once()
420 | args = flamegraph_widget.post_message.call_args[0]
421 | message = args[0]
422 | assert message.by_mouse == False
423 | assert message.frame._id == 2
424 |
425 |
426 | def test_flamegraph_action_move_right_on_root():
427 | id_store = {}
428 | root = create_frame(
429 | {
430 | "id": 0,
431 | "values": [10],
432 | "children": [
433 | {
434 | "id": 1,
435 | "values": [2],
436 | "children": [
437 | {"id": 3, "values": [1], "children": []},
438 | ],
439 | },
440 | {"id": 2, "values": [3], "children": []},
441 | ],
442 | },
443 | id_store,
444 | )
445 |
446 | p = Profile(
447 | filename="abc",
448 | root_stack=root,
449 | highest_lines=10,
450 | total_sample=10,
451 | sample_types=[SampleType("goroutine", "count")],
452 | id_store=id_store,
453 | )
454 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[0])
455 | flamegraph_widget.post_message = MagicMock()
456 | flamegraph_widget.action_move_right()
457 |
458 | flamegraph_widget.post_message.assert_not_called()
459 |
460 |
461 | def test_flamegraph_action_move_left_sibling_just_here():
462 | id_store = {}
463 | root = create_frame(
464 | {
465 | "id": 0,
466 | "values": [10],
467 | "children": [
468 | {"id": 2, "values": [3], "children": []},
469 | {
470 | "id": 1,
471 | "values": [2],
472 | "children": [],
473 | },
474 | ],
475 | },
476 | id_store,
477 | )
478 |
479 | p = Profile(
480 | filename="abc",
481 | root_stack=root,
482 | highest_lines=10,
483 | total_sample=10,
484 | sample_types=[SampleType("goroutine", "count")],
485 | id_store=id_store,
486 | )
487 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[1])
488 | flamegraph_widget.post_message = MagicMock()
489 | flamegraph_widget.action_move_left()
490 |
491 | flamegraph_widget.post_message.assert_called_once()
492 | args = flamegraph_widget.post_message.call_args[0]
493 | message = args[0]
494 | assert message.by_mouse == False
495 | assert message.frame._id == 2
496 |
497 | # no more sibling
498 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[2])
499 | flamegraph_widget.post_message = MagicMock()
500 | flamegraph_widget.action_move_left()
501 |
502 | flamegraph_widget.post_message.assert_not_called()
503 |
504 |
505 | def test_flamegraph_action_move_left_sibling_goes_to_parent():
506 | id_store = {}
507 | root = create_frame(
508 | {
509 | "id": 0,
510 | "values": [10],
511 | "children": [
512 | {"id": 2, "values": [3], "children": []},
513 | {
514 | "id": 1,
515 | "values": [2],
516 | "children": [
517 | {"id": 3, "values": [1], "children": []},
518 | ],
519 | },
520 | ],
521 | },
522 | id_store,
523 | )
524 |
525 | p = Profile(
526 | filename="abc",
527 | root_stack=root,
528 | highest_lines=10,
529 | total_sample=10,
530 | sample_types=[SampleType("goroutine", "count")],
531 | id_store=id_store,
532 | )
533 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[3])
534 | flamegraph_widget.post_message = MagicMock()
535 | flamegraph_widget.action_move_left()
536 |
537 | flamegraph_widget.post_message.assert_called_once()
538 | args = flamegraph_widget.post_message.call_args[0]
539 | message = args[0]
540 | assert message.by_mouse == False
541 | assert message.frame._id == 2
542 |
543 |
544 | def test_flamegraph_action_move_left_on_root():
545 | id_store = {}
546 | root = create_frame(
547 | {
548 | "id": 0,
549 | "values": [10],
550 | "children": [
551 | {
552 | "id": 1,
553 | "values": [2],
554 | "children": [
555 | {"id": 3, "values": [1], "children": []},
556 | ],
557 | },
558 | {"id": 2, "values": [3], "children": []},
559 | ],
560 | },
561 | id_store,
562 | )
563 |
564 | p = Profile(
565 | filename="abc",
566 | root_stack=root,
567 | highest_lines=10,
568 | total_sample=10,
569 | sample_types=[SampleType("goroutine", "count")],
570 | id_store=id_store,
571 | )
572 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[0])
573 | flamegraph_widget.post_message = MagicMock()
574 | flamegraph_widget.action_move_left()
575 |
576 | flamegraph_widget.post_message.assert_not_called()
577 |
578 |
579 | def test_flamegraph_render_on_mouse_move():
580 | id_store = {}
581 | # 10
582 | # 3, 2
583 | # , 1
584 | root = create_frame(
585 | {
586 | "id": 0,
587 | "values": [10],
588 | "children": [
589 | {"id": 2, "values": [3], "children": []},
590 | {
591 | "id": 1,
592 | "values": [2],
593 | "children": [
594 | {"id": 3, "values": [1], "children": []},
595 | ],
596 | },
597 | ],
598 | },
599 | id_store,
600 | )
601 |
602 | p = Profile(
603 | filename="abc",
604 | root_stack=root,
605 | highest_lines=10,
606 | total_sample=10,
607 | sample_types=[SampleType("goroutine", "count")],
608 | id_store=id_store,
609 | )
610 | flamegraph_widget = FlameGraph(p, 0, -1, view_frame=id_store[3])
611 | flamegraph_widget.frame_maps = flamegraph_widget.generate_frame_maps(10, 0)
612 | flamegraph_widget.post_message = MagicMock()
613 |
614 | mouse_event = MouseMove(
615 | x=2,
616 | y=1,
617 | delta_x=0,
618 | delta_y=0,
619 | button=False,
620 | shift=False,
621 | meta=False,
622 | ctrl=False,
623 | )
624 | flamegraph_widget.on_mouse_move(mouse_event)
625 |
626 | flamegraph_widget.post_message.assert_called_once()
627 | args = flamegraph_widget.post_message.call_args[0]
628 | message = args[0]
629 | assert message.by_mouse == True
630 | assert message.frame._id == 2
631 |
632 | assert flamegraph_widget.focused_stack_id == 0
633 | flamegraph_widget.handle_click_frame(mouse_event)
634 | assert flamegraph_widget.focused_stack_id == 2
635 |
636 | # move to lines that empty
637 | flamegraph_widget.post_message = MagicMock()
638 | flamegraph_widget.on_mouse_move(
639 | MouseMove(
640 | x=1,
641 | y=2,
642 | delta_x=0,
643 | delta_y=0,
644 | button=False,
645 | shift=False,
646 | meta=False,
647 | ctrl=False,
648 | )
649 | )
650 | args = flamegraph_widget.post_message.assert_not_called()
651 |
652 | # just to move the the exact offset, should still work
653 | # should be hover on next span instead of last
654 | flamegraph_widget.post_message = MagicMock()
655 | flamegraph_widget.on_mouse_move(
656 | MouseMove(
657 | x=3,
658 | y=1,
659 | delta_x=0,
660 | delta_y=0,
661 | button=False,
662 | shift=False,
663 | meta=False,
664 | ctrl=False,
665 | )
666 | )
667 | flamegraph_widget.post_message.assert_called_once()
668 | args = flamegraph_widget.post_message.call_args[0]
669 | message = args[0]
670 | assert message.by_mouse == True
671 | assert message.frame._id == 1
672 |
673 | # move down, not hover on any lines
674 | flamegraph_widget.post_message = MagicMock()
675 | flamegraph_widget.on_mouse_move(
676 | MouseMove(
677 | x=0,
678 | y=3,
679 | delta_x=0,
680 | delta_y=0,
681 | button=False,
682 | shift=False,
683 | meta=False,
684 | ctrl=False,
685 | )
686 | )
687 | args = flamegraph_widget.post_message.assert_not_called()
688 |
689 |
690 | def test_flamegraph_render_line_with_some_width_is_0():
691 | id_store = {}
692 | root = create_frame(
693 | {
694 | "id": 0,
695 | "values": [10],
696 | "children": [
697 | {"id": 2, "values": [3], "children": []},
698 | {
699 | "id": 1,
700 | "values": [2],
701 | "children": [
702 | {"id": 3, "values": [1], "children": []},
703 | ],
704 | },
705 | {"id": 4, "values": [0.1], "children": []},
706 | ],
707 | },
708 | id_store,
709 | )
710 |
711 | p = Profile(
712 | filename="abc",
713 | root_stack=root,
714 | highest_lines=1,
715 | total_sample=2,
716 | sample_types=[SampleType("samples", "count")],
717 | id_store=id_store,
718 | )
719 | flamegraph_widget = FlameGraph(p, 0, -1, root)
720 | flamegraph_widget.frame_maps = flamegraph_widget.generate_frame_maps(
721 | 10, focused_stack_id=0
722 | )
723 |
724 | strip = flamegraph_widget.render_line(
725 | 1,
726 | )
727 |
728 | line_strings = [seg.text for seg in strip._segments]
729 |
730 | assert line_strings == ["▏", "no", "▏", "n"]
731 |
732 |
733 | def test_flamegraph_render_line_with_focused_frame():
734 | id_store = {}
735 | root = create_frame(
736 | {
737 | "id": 0,
738 | "values": [10],
739 | "children": [
740 | {"id": 1, "values": [3], "children": []},
741 | {"id": 4, "values": [1], "children": []},
742 | {
743 | "id": 2,
744 | "values": [6],
745 | "children": [
746 | {"id": 3, "values": [1], "children": []},
747 | ],
748 | },
749 | ],
750 | },
751 | id_store,
752 | )
753 |
754 | p = Profile(
755 | filename="abc",
756 | root_stack=root,
757 | highest_lines=1,
758 | total_sample=2,
759 | sample_types=[SampleType("samples", "count")],
760 | id_store=id_store,
761 | )
762 | flamegraph_widget = FlameGraph(p, 2, -1, root)
763 | flamegraph_widget.frame_maps = flamegraph_widget.generate_frame_maps(
764 | 10, focused_stack_id=2
765 | )
766 |
767 | strip = flamegraph_widget.render_line(
768 | 1,
769 | )
770 |
771 | line_strings = [seg.text for seg in strip._segments]
772 |
773 | assert line_strings == ["▏", "node-2 "]
774 |
775 | flamegraph_widget.post_message = MagicMock()
776 |
777 | flamegraph_widget.on_mouse_move(
778 | MouseMove(
779 | x=0,
780 | y=1,
781 | delta_x=0,
782 | delta_y=0,
783 | button=False,
784 | shift=False,
785 | meta=False,
786 | ctrl=False,
787 | )
788 | )
789 |
790 | flamegraph_widget.post_message.assert_called_once()
791 | args = flamegraph_widget.post_message.call_args[0]
792 | message = args[0]
793 | assert message.by_mouse == True
794 | assert message.frame._id == 2
795 |
--------------------------------------------------------------------------------
/tests/test_render/test_render_detail.py:
--------------------------------------------------------------------------------
1 | from flameshow.models import Profile
2 | from flameshow.render.framedetail import FrameStatAll
3 | from ..utils import create_frame
4 |
5 |
6 | def test_render_self_value_all_instance():
7 | root = create_frame(
8 | {
9 | "id": 0,
10 | "values": [10],
11 | "children": [
12 | {"id": 1, "values": [4], "children": []},
13 | ],
14 | }
15 | )
16 | profile = Profile("asdf", root, 0, 0, [], {})
17 | widget = FrameStatAll(root, profile, 0)
18 | value = widget.frame_all_self_value
19 | assert value == 6
20 |
21 | child_frame = root.children[0]
22 |
23 | widget = FrameStatAll(child_frame, profile, 0)
24 | value = widget.frame_all_self_value
25 | assert value == 4
26 |
--------------------------------------------------------------------------------
/tests/test_render/test_render_header.py:
--------------------------------------------------------------------------------
1 | from rich.text import Text, Span
2 | from flameshow.render.header import (
3 | HeaderIcon,
4 | HeaderTitle,
5 | HeaderOpenedFilename,
6 | FlameshowHeader,
7 | )
8 |
9 |
10 | def test_header_icon():
11 | hi = HeaderIcon()
12 |
13 | assert hi.render() == "🔥"
14 |
15 |
16 | def test_header_title():
17 | ht = HeaderTitle()
18 | ht.text = "abc"
19 | ht.sub_text = "foo"
20 |
21 | assert ht.render() == Text("abc — foo", spans=[Span(6, 9, "dim")])
22 |
23 |
24 | def test_header_opened_filename():
25 | hf = HeaderOpenedFilename("goro.out")
26 |
27 | assert hf.render() == Text("goro.out")
28 |
29 |
30 | def test_flameshow_header():
31 | fh = FlameshowHeader("foo.out")
32 | result = list(fh.compose())
33 |
34 | assert len(result) == 2
35 |
36 | header_center_text = result[1]._nodes[1]
37 |
38 | assert isinstance(header_center_text, HeaderOpenedFilename)
39 |
--------------------------------------------------------------------------------
/tests/test_stackcollapse_parse/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laixintao/flameshow/400c6eeccf0b6ff24b71978ba5d575fb5cdfe5a3/tests/test_stackcollapse_parse/__init__.py
--------------------------------------------------------------------------------
/tests/test_stackcollapse_parse/test_model.py:
--------------------------------------------------------------------------------
1 | from rich.text import Text
2 | from flameshow.parsers.stackcollapse_parser import StackCollapseFrame
3 |
4 |
5 | def test_frame_model_render():
6 | f = StackCollapseFrame(
7 | "java::abc", 1, parent=None, children=[], values=[13]
8 | )
9 | render_result = f.render_one_frame_detail(f, 0, "count")
10 | assert len(render_result) == 1
11 | item = render_result[0]
12 | assert item.markup == "java::abc\n"
13 |
14 |
15 | def test_frame_model_render_with_square():
16 | f = StackCollapseFrame("[abc]", 1, parent=None, children=[], values=[13])
17 | render_result = f.render_one_frame_detail(f, 0, "count")
18 | assert len(render_result) == 1
19 | item = render_result[0]
20 | assert item.markup == "\\[abc]\n"
21 |
22 | title = f.title
23 | assert isinstance(title, Text)
24 | assert title.plain == "[abc]"
25 |
--------------------------------------------------------------------------------
/tests/test_stackcollapse_parse/test_parser.py:
--------------------------------------------------------------------------------
1 | from flameshow.parsers.stackcollapse_parser import StackCollapseParser
2 |
3 | from ..utils import frame2json
4 |
5 |
6 | def test_parse_simple_text_data(simple_collapse_data):
7 | parser = StackCollapseParser("a.txt")
8 | profile = parser.parse(simple_collapse_data)
9 | assert profile.highest_lines == 3
10 |
11 | assert frame2json(profile.root_stack) == {
12 | "root": {
13 | "children": [
14 | {
15 | "a": {
16 | "children": [
17 | {
18 | "b": {
19 | "children": [
20 | {"c": {"children": [], "values": [5]}},
21 | {"d": {"children": [], "values": [4]}},
22 | ],
23 | "values": [14],
24 | }
25 | }
26 | ],
27 | "values": [14],
28 | }
29 | }
30 | ],
31 | "values": [14],
32 | },
33 | }
34 |
35 | assert [f.name for f in profile.id_store.values()] == [
36 | "root",
37 | "a",
38 | "b",
39 | "c",
40 | "a",
41 | "b",
42 | "c",
43 | "a",
44 | "b",
45 | "d",
46 | "a",
47 | "b",
48 | "c",
49 | "a",
50 | "b",
51 | ]
52 |
53 | assert profile.root_stack.children[0].parent.name == "root"
54 |
55 |
56 | def test_validate_simple_text_data(simple_collapse_data):
57 | assert StackCollapseParser.validate(simple_collapse_data)
58 |
59 |
60 | def test_validate_text_data_with_comment(collapse_data_with_comment):
61 | assert StackCollapseParser.validate(collapse_data_with_comment)
62 |
63 |
64 | def test_validate_simple_text_data_not_utf8(simple_collapse_data):
65 | data = (simple_collapse_data.decode() + "你好").encode("big5")
66 | assert not StackCollapseParser.validate(data)
67 |
68 |
69 | def test_validate_simple_text_contains_empty(simple_collapse_data):
70 | data = (simple_collapse_data.decode() + "\r\n\r\n").encode()
71 | assert StackCollapseParser.validate(data)
72 |
73 |
74 | def test_validate_simple_text_data_contains_numbers_somtimes():
75 | data = b"""
76 | a;b;c 10
77 | 5
78 | c 4
79 | 10
80 | """
81 | assert StackCollapseParser.validate(data)
82 |
83 |
84 | def test_validate_simple_text_data_notmatch():
85 | data = b"""
86 | a;b;c 10
87 | c
88 | """
89 | assert not StackCollapseParser.validate(data)
90 |
91 |
92 | def test_validate_simple_text_data_contains_numbers_somtimes_parsing():
93 | data = b"""
94 | a;b;c 10
95 | 5
96 | c 4
97 | 10
98 | """
99 | profile = StackCollapseParser("a.txt").parse(data)
100 | assert frame2json(profile.root_stack) == {
101 | "root": {
102 | "children": [
103 | {
104 | "a": {
105 | "children": [
106 | {
107 | "b": {
108 | "children": [
109 | {"c": {"children": [], "values": [10]}}
110 | ],
111 | "values": [10],
112 | }
113 | }
114 | ],
115 | "values": [10],
116 | }
117 | },
118 | {"c": {"children": [], "values": [4]}},
119 | ],
120 | "values": [14],
121 | }
122 | }
123 |
124 |
125 | def test_space_and_numbers_in_stackcollapse_symbols():
126 | data = b"""
127 | a 1;b 2;c 3 10
128 | """
129 | profile = StackCollapseParser("a.txt").parse(data)
130 | assert frame2json(profile.root_stack) == {
131 | "root": {
132 | "children": [
133 | {
134 | "a 1": {
135 | "children": [
136 | {
137 | "b 2": {
138 | "children": [
139 | {
140 | "c 3": {
141 | "children": [],
142 | "values": [10],
143 | }
144 | }
145 | ],
146 | "values": [10],
147 | }
148 | }
149 | ],
150 | "values": [10],
151 | }
152 | }
153 | ],
154 | "values": [10],
155 | },
156 | }
157 |
158 |
159 | def test_validate_simple_text_data_contains_comments():
160 | data = b"""
161 | # austin: 3.7.0
162 | # interval: 100
163 | # mode: wall
164 | a;b;c 10
165 | 5
166 | c 4
167 | 10
168 | """
169 | profile = StackCollapseParser("a.txt").parse(data)
170 | assert frame2json(profile.root_stack) == {
171 | "root": {
172 | "children": [
173 | {
174 | "a": {
175 | "children": [
176 | {
177 | "b": {
178 | "children": [
179 | {"c": {"children": [], "values": [10]}}
180 | ],
181 | "values": [10],
182 | }
183 | }
184 | ],
185 | "values": [10],
186 | }
187 | },
188 | {"c": {"children": [], "values": [4]}},
189 | ],
190 | "values": [14],
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from flameshow.utils import sizeof
2 |
3 |
4 | def test_sizeof():
5 | assert sizeof(1) == "1.0B"
6 | assert sizeof(2048) == "2.0KiB"
7 | assert (
8 | sizeof(5.83 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)
9 | == "5.8YiB"
10 | )
11 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | from flameshow.models import Frame
2 |
3 |
4 | def create_frame(data, id_store=None):
5 | name = "node-{}".format(data["id"])
6 | if "name" in data:
7 | name = data["name"]
8 | root = Frame(
9 | name=name,
10 | _id=data["id"],
11 | values=data["values"],
12 | )
13 | root.children = []
14 | for child in data["children"]:
15 | cf = create_frame(child, id_store)
16 | root.children.append(cf)
17 | cf.parent = root
18 |
19 | if id_store is not None:
20 | id_store[root._id] = root
21 | return root
22 |
23 |
24 | def frame2json(frame):
25 | data = {
26 | frame.name: {
27 | "values": frame.values,
28 | "children": [frame2json(c) for c in frame.children],
29 | }
30 | }
31 | return data
32 |
--------------------------------------------------------------------------------