flamegraph-textual 0.1.0__py3-none-any.whl
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/__init__.py +25 -0
- flamegraph_textual/colors.py +67 -0
- flamegraph_textual/const.py +4 -0
- flamegraph_textual/exceptions.py +14 -0
- flamegraph_textual/models.py +204 -0
- flamegraph_textual/parsers/__init__.py +25 -0
- flamegraph_textual/parsers/pprof_parser.py +352 -0
- flamegraph_textual/parsers/profile_pb2.py +40 -0
- flamegraph_textual/parsers/stackcollapse_parser.py +134 -0
- flamegraph_textual/pprof_parser/__init__.py +4 -0
- flamegraph_textual/pprof_parser/parser.py +1 -0
- flamegraph_textual/pprof_parser/profile_pb2.py +1 -0
- flamegraph_textual/render/__init__.py +3 -0
- flamegraph_textual/render/app.py +257 -0
- flamegraph_textual/render/flamegraph.py +381 -0
- flamegraph_textual/render/framedetail.py +452 -0
- flamegraph_textual/render/header.py +108 -0
- flamegraph_textual/render/tabs.py +5 -0
- flamegraph_textual/runtime.py +22 -0
- flamegraph_textual/utils.py +12 -0
- flamegraph_textual/view.py +164 -0
- flamegraph_textual-0.1.0.dist-info/METADATA +170 -0
- flamegraph_textual-0.1.0.dist-info/RECORD +24 -0
- flamegraph_textual-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse golang's pprof format into flamegraph_textual 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 flamegraph_textual.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")
|