flamegraph-textual 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flamegraph_textual-0.1.0/PKG-INFO +170 -0
- flamegraph_textual-0.1.0/README.md +148 -0
- flamegraph_textual-0.1.0/flamegraph_textual/__init__.py +25 -0
- flamegraph_textual-0.1.0/flamegraph_textual/colors.py +67 -0
- flamegraph_textual-0.1.0/flamegraph_textual/const.py +4 -0
- flamegraph_textual-0.1.0/flamegraph_textual/exceptions.py +14 -0
- flamegraph_textual-0.1.0/flamegraph_textual/models.py +204 -0
- flamegraph_textual-0.1.0/flamegraph_textual/parsers/__init__.py +25 -0
- flamegraph_textual-0.1.0/flamegraph_textual/parsers/pprof_parser.py +352 -0
- flamegraph_textual-0.1.0/flamegraph_textual/parsers/profile_pb2.py +40 -0
- flamegraph_textual-0.1.0/flamegraph_textual/parsers/stackcollapse_parser.py +134 -0
- flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/__init__.py +4 -0
- flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/parser.py +1 -0
- flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/profile_pb2.py +1 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/__init__.py +3 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/app.py +257 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/flamegraph.py +381 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/framedetail.py +452 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/header.py +108 -0
- flamegraph_textual-0.1.0/flamegraph_textual/render/tabs.py +5 -0
- flamegraph_textual-0.1.0/flamegraph_textual/runtime.py +22 -0
- flamegraph_textual-0.1.0/flamegraph_textual/utils.py +12 -0
- flamegraph_textual-0.1.0/flamegraph_textual/view.py +164 -0
- flamegraph_textual-0.1.0/pyproject.toml +24 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flamegraph-textual
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal flamegraph renderer built for Textual
|
|
5
|
+
License: GPLv3
|
|
6
|
+
Author: laixintao
|
|
7
|
+
Author-email: laixintaoo@gmail.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: iteround (>=1.0.4,<2.0.0)
|
|
16
|
+
Requires-Dist: protobuf (>=4.25,<5.0)
|
|
17
|
+
Requires-Dist: rich (>=13.6.0,<14.0.0)
|
|
18
|
+
Requires-Dist: textual (>=0.37.1,<0.38.0)
|
|
19
|
+
Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# flamegraph-textual
|
|
23
|
+
|
|
24
|
+
`flamegraph-textual` is an interactive flamegraph component for
|
|
25
|
+
[Textual](https://github.com/Textualize/textual).
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
It is the rendering library extracted from
|
|
30
|
+
[flameshow](https://github.com/laixintao/flameshow). Use it when you want to
|
|
31
|
+
embed a terminal flamegraph inside your own Textual app instead of launching a
|
|
32
|
+
standalone viewer.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
pip install flamegraph-textual
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## What It Does
|
|
41
|
+
|
|
42
|
+
- Renders flamegraphs as a Textual widget
|
|
43
|
+
- Parses profile input for you
|
|
44
|
+
- Supports keyboard and mouse navigation
|
|
45
|
+
- Supports multiple sample types when present in the profile
|
|
46
|
+
- Works with bundled demo data or your own files
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
`FlameGraphView` is the main entrypoint. Pass it raw profile data and a
|
|
51
|
+
filename. The library parses the content internally.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
from textual.app import App, ComposeResult
|
|
57
|
+
|
|
58
|
+
from flamegraph_textual import FlameGraphView
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Demo(App):
|
|
62
|
+
def compose(self) -> ComposeResult:
|
|
63
|
+
profile_bytes = Path("profile.out").read_bytes()
|
|
64
|
+
yield FlameGraphView(profile_bytes, filename="profile.out")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
Demo().run()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For stackcollapse text input, passing `str` also works:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
|
|
75
|
+
from flamegraph_textual import FlameGraphView
|
|
76
|
+
|
|
77
|
+
profile_text = Path("stacks.txt").read_text(encoding="utf-8")
|
|
78
|
+
widget = FlameGraphView(profile_text, filename="stacks.txt")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Supported Input Formats
|
|
82
|
+
|
|
83
|
+
- pprof protobuf profiles
|
|
84
|
+
- stackcollapse text
|
|
85
|
+
|
|
86
|
+
The parser selection is automatic through:
|
|
87
|
+
[parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
|
|
88
|
+
|
|
89
|
+
## Try It Immediately
|
|
90
|
+
|
|
91
|
+
This repo includes sample profiles under:
|
|
92
|
+
|
|
93
|
+
- [tests/pprof_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/pprof_data)
|
|
94
|
+
- [tests/stackcollapse_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/stackcollapse_data)
|
|
95
|
+
|
|
96
|
+
Run the bundled examples with no setup:
|
|
97
|
+
|
|
98
|
+
```shell
|
|
99
|
+
python examples/pprof_binary.py
|
|
100
|
+
python examples/pprof_binary.py --sample goroutine
|
|
101
|
+
python examples/pprof_binary.py --sample heap
|
|
102
|
+
|
|
103
|
+
python examples/stackcollapse_text.py
|
|
104
|
+
python examples/stackcollapse_text.py --sample simple
|
|
105
|
+
python examples/stackcollapse_text.py --sample perf
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
You can still pass your own file path:
|
|
109
|
+
|
|
110
|
+
```shell
|
|
111
|
+
python examples/pprof_binary.py /path/to/profile.out
|
|
112
|
+
python examples/stackcollapse_text.py /path/to/stacks.txt
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Main API
|
|
116
|
+
|
|
117
|
+
Most users only need:
|
|
118
|
+
|
|
119
|
+
- [FlameGraphView](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/view.py)
|
|
120
|
+
- [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
|
|
121
|
+
|
|
122
|
+
Other exports are available if you want lower-level control:
|
|
123
|
+
|
|
124
|
+
- `FlameGraph`
|
|
125
|
+
- `FlameGraphScroll`
|
|
126
|
+
- `Frame`
|
|
127
|
+
- `Profile`
|
|
128
|
+
- `SampleType`
|
|
129
|
+
|
|
130
|
+
See:
|
|
131
|
+
[__init__.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/__init__.py)
|
|
132
|
+
|
|
133
|
+
## Controls
|
|
134
|
+
|
|
135
|
+
Inside the widget:
|
|
136
|
+
|
|
137
|
+
- `j` / `k` / `h` / `l` or arrow keys move selection
|
|
138
|
+
- `Enter` zooms in
|
|
139
|
+
- `Esc` zooms out
|
|
140
|
+
- `Tab` switches sample type
|
|
141
|
+
- `i` opens the detail screen when mounted inside a Textual app
|
|
142
|
+
- Mouse hover updates frame details
|
|
143
|
+
- Mouse click zooms into a frame
|
|
144
|
+
|
|
145
|
+
## Regenerate Protobuf Bindings
|
|
146
|
+
|
|
147
|
+
The canonical pprof schema lives in:
|
|
148
|
+
[profile.proto](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/proto/profile.proto)
|
|
149
|
+
|
|
150
|
+
The generated Python module lives in:
|
|
151
|
+
[profile_pb2.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/profile_pb2.py)
|
|
152
|
+
|
|
153
|
+
Regenerate it with:
|
|
154
|
+
|
|
155
|
+
```shell
|
|
156
|
+
poetry add --group dev grpcio-tools
|
|
157
|
+
poetry run python -m grpc_tools.protoc \
|
|
158
|
+
-I proto \
|
|
159
|
+
--python_out=flamegraph_textual/parsers \
|
|
160
|
+
proto/profile.proto
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
Run tests with:
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
pytest -q
|
|
169
|
+
```
|
|
170
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# flamegraph-textual
|
|
2
|
+
|
|
3
|
+
`flamegraph-textual` is an interactive flamegraph component for
|
|
4
|
+
[Textual](https://github.com/Textualize/textual).
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
It is the rendering library extracted from
|
|
9
|
+
[flameshow](https://github.com/laixintao/flameshow). Use it when you want to
|
|
10
|
+
embed a terminal flamegraph inside your own Textual app instead of launching a
|
|
11
|
+
standalone viewer.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```shell
|
|
16
|
+
pip install flamegraph-textual
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What It Does
|
|
20
|
+
|
|
21
|
+
- Renders flamegraphs as a Textual widget
|
|
22
|
+
- Parses profile input for you
|
|
23
|
+
- Supports keyboard and mouse navigation
|
|
24
|
+
- Supports multiple sample types when present in the profile
|
|
25
|
+
- Works with bundled demo data or your own files
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
`FlameGraphView` is the main entrypoint. Pass it raw profile data and a
|
|
30
|
+
filename. The library parses the content internally.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from textual.app import App, ComposeResult
|
|
36
|
+
|
|
37
|
+
from flamegraph_textual import FlameGraphView
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Demo(App):
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
profile_bytes = Path("profile.out").read_bytes()
|
|
43
|
+
yield FlameGraphView(profile_bytes, filename="profile.out")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Demo().run()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For stackcollapse text input, passing `str` also works:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
from flamegraph_textual import FlameGraphView
|
|
55
|
+
|
|
56
|
+
profile_text = Path("stacks.txt").read_text(encoding="utf-8")
|
|
57
|
+
widget = FlameGraphView(profile_text, filename="stacks.txt")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Supported Input Formats
|
|
61
|
+
|
|
62
|
+
- pprof protobuf profiles
|
|
63
|
+
- stackcollapse text
|
|
64
|
+
|
|
65
|
+
The parser selection is automatic through:
|
|
66
|
+
[parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
|
|
67
|
+
|
|
68
|
+
## Try It Immediately
|
|
69
|
+
|
|
70
|
+
This repo includes sample profiles under:
|
|
71
|
+
|
|
72
|
+
- [tests/pprof_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/pprof_data)
|
|
73
|
+
- [tests/stackcollapse_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/stackcollapse_data)
|
|
74
|
+
|
|
75
|
+
Run the bundled examples with no setup:
|
|
76
|
+
|
|
77
|
+
```shell
|
|
78
|
+
python examples/pprof_binary.py
|
|
79
|
+
python examples/pprof_binary.py --sample goroutine
|
|
80
|
+
python examples/pprof_binary.py --sample heap
|
|
81
|
+
|
|
82
|
+
python examples/stackcollapse_text.py
|
|
83
|
+
python examples/stackcollapse_text.py --sample simple
|
|
84
|
+
python examples/stackcollapse_text.py --sample perf
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can still pass your own file path:
|
|
88
|
+
|
|
89
|
+
```shell
|
|
90
|
+
python examples/pprof_binary.py /path/to/profile.out
|
|
91
|
+
python examples/stackcollapse_text.py /path/to/stacks.txt
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Main API
|
|
95
|
+
|
|
96
|
+
Most users only need:
|
|
97
|
+
|
|
98
|
+
- [FlameGraphView](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/view.py)
|
|
99
|
+
- [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
|
|
100
|
+
|
|
101
|
+
Other exports are available if you want lower-level control:
|
|
102
|
+
|
|
103
|
+
- `FlameGraph`
|
|
104
|
+
- `FlameGraphScroll`
|
|
105
|
+
- `Frame`
|
|
106
|
+
- `Profile`
|
|
107
|
+
- `SampleType`
|
|
108
|
+
|
|
109
|
+
See:
|
|
110
|
+
[__init__.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/__init__.py)
|
|
111
|
+
|
|
112
|
+
## Controls
|
|
113
|
+
|
|
114
|
+
Inside the widget:
|
|
115
|
+
|
|
116
|
+
- `j` / `k` / `h` / `l` or arrow keys move selection
|
|
117
|
+
- `Enter` zooms in
|
|
118
|
+
- `Esc` zooms out
|
|
119
|
+
- `Tab` switches sample type
|
|
120
|
+
- `i` opens the detail screen when mounted inside a Textual app
|
|
121
|
+
- Mouse hover updates frame details
|
|
122
|
+
- Mouse click zooms into a frame
|
|
123
|
+
|
|
124
|
+
## Regenerate Protobuf Bindings
|
|
125
|
+
|
|
126
|
+
The canonical pprof schema lives in:
|
|
127
|
+
[profile.proto](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/proto/profile.proto)
|
|
128
|
+
|
|
129
|
+
The generated Python module lives in:
|
|
130
|
+
[profile_pb2.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/profile_pb2.py)
|
|
131
|
+
|
|
132
|
+
Regenerate it with:
|
|
133
|
+
|
|
134
|
+
```shell
|
|
135
|
+
poetry add --group dev grpcio-tools
|
|
136
|
+
poetry run python -m grpc_tools.protoc \
|
|
137
|
+
-I proto \
|
|
138
|
+
--python_out=flamegraph_textual/parsers \
|
|
139
|
+
proto/profile.proto
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
Run tests with:
|
|
145
|
+
|
|
146
|
+
```shell
|
|
147
|
+
pytest -q
|
|
148
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from flamegraph_textual.models import Frame, Profile, SampleType
|
|
4
|
+
from flamegraph_textual.parsers import parse
|
|
5
|
+
from flamegraph_textual.render.app import FlameGraphScroll, FlameshowApp
|
|
6
|
+
from flamegraph_textual.render.flamegraph import FlameGraph, FrameMap, add_array
|
|
7
|
+
from flamegraph_textual.view import FlameGraphView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FlameGraphApp(FlameshowApp):
|
|
11
|
+
"""Convenience alias for embedding a full flamegraph app."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FlameGraph",
|
|
16
|
+
"FlameGraphApp",
|
|
17
|
+
"FlameGraphScroll",
|
|
18
|
+
"FlameGraphView",
|
|
19
|
+
"Frame",
|
|
20
|
+
"FrameMap",
|
|
21
|
+
"Profile",
|
|
22
|
+
"SampleType",
|
|
23
|
+
"add_array",
|
|
24
|
+
"parse",
|
|
25
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
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()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class FlamegraphTextualException(Exception):
|
|
2
|
+
"""Base exception for flamegraph_textual."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ProfileParseException(FlamegraphTextualException):
|
|
6
|
+
"""Can not parse the profile."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UsageError(FlamegraphTextualException):
|
|
10
|
+
"""Usage Error."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RenderException(FlamegraphTextualException):
|
|
14
|
+
"""Error during flamegraph render."""
|
|
@@ -0,0 +1,204 @@
|
|
|
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 flamegraph_textual.utils import sizeof
|
|
12
|
+
|
|
13
|
+
from flamegraph_textual.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"<Frame #{self._id} {self.name}>"
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from flamegraph_textual.exceptions import ProfileParseException
|
|
4
|
+
from flamegraph_textual.parsers.pprof_parser import ProfileParser as PprofParser
|
|
5
|
+
from flamegraph_textual.parsers.stackcollapse_parser import StackCollapseParser
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ALL_PARSERS = [PprofParser, StackCollapseParser]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def choose_parser(content: bytes):
|
|
14
|
+
for p in ALL_PARSERS:
|
|
15
|
+
if p.validate(content):
|
|
16
|
+
return p
|
|
17
|
+
raise ProfileParseException("Can not match any parser")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse(filecontent: bytes, filename):
|
|
21
|
+
parser_cls = choose_parser(filecontent)
|
|
22
|
+
logger.info("Using %s...", parser_cls)
|
|
23
|
+
parser = parser_cls(filename)
|
|
24
|
+
profile = parser.parse(filecontent)
|
|
25
|
+
return profile
|