-
Notifications
You must be signed in to change notification settings - Fork 2
/
frame_filter.py
executable file
·370 lines (293 loc) · 12.6 KB
/
frame_filter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/usr/bin/env python3
"""
Usage:
- Install debug symbols for python. (e.g. `sudo apt install pythonX.Y-dbg`)
- Source this file in your `.gdbinit`
`source ~/path/to/the/tvm_packedfunc_filter.py`
"""
import ctypes
import enum
import itertools
import os
import re
from abc import abstractmethod
from typing import Union
from .utils import _unwrap_frame, StacktraceOnRaise
import gdb
from gdb.FrameDecorator import FrameDecorator
class FilterLevel(enum.Flag):
Disabled = 0
Interpreter = enum.auto()
Pytest = enum.auto()
Dispatch = enum.auto()
Default = Interpreter | Pytest | Dispatch
CommonBaseClass = enum.auto()
All = Default | CommonBaseClass
class FrameFilter:
@classmethod
def register(cls, filter_level):
for subclass in cls._filters:
filter = subclass()
filter.enabled = filter_level & subclass.filter_level
gdb.frame_filters[filter.name] = filter
_filters = []
def __init_subclass__(cls, /, filter_level, priority=100, **kwargs):
super().__init_subclass__(**kwargs)
cls.filter_level = filter_level
cls.priority = priority
FrameFilter._filters.append(cls)
def __init__(self):
self.name = "TVM_" + type(self).__name__
self.priority = type(self).priority
self.enabled = True
class PythonFrameFilter(FrameFilter, filter_level=FilterLevel.Interpreter):
def __init__(self):
self.name = "TVM_Python_Filter"
self.priority = 100
self.enabled = True
def filter(self, frame_iter):
with StacktraceOnRaise():
python_frames = []
prev_evalframe = None
for frame in frame_iter:
is_python = self.is_python_frame(frame)
is_evalframe = is_python and self.is_python_evalframe(frame)
if is_python and is_evalframe and prev_evalframe is None:
prev_evalframe = frame
python_frames.append(frame)
elif is_python and is_evalframe and prev_evalframe is not None:
yield PythonFrameDecorator(prev_evalframe, python_frames)
python_frames = [frame]
prev_evalframe = frame
elif is_python and not is_evalframe:
python_frames.append(frame)
elif not is_python and python_frames and prev_evalframe is not None:
yield PythonFrameDecorator(prev_evalframe, python_frames)
python_frames = []
prev_evalframe = None
yield frame
elif not is_python and python_frames and prev_evalframe is None:
yield from python_frames
python_frames = []
yield frame
elif not is_python and not python_frames:
yield frame
@staticmethod
def is_python_frame(frame):
"""Check if this stack frame is owned by CPython
Returns True if the stack frame is part of the python
executable or libpython.so. May erroneously return False if
the frame is a CPython frame included statically in another
executable using libpython.a
"""
frame = _unwrap_frame(frame)
# Find the file that contains the current instruction pointer.
shared_lib_name = gdb.solib_name(frame.pc())
prog_name = gdb.current_progspace().filename
if shared_lib_name:
obj_filepath = shared_lib_name
else:
obj_filepath = prog_name
obj_filename = os.path.basename(obj_filepath)
# Check for pythonX.Y, and debug versions pythonX.Yd
is_python_exe = bool(re.match(r"^python\d+\.\d+d?$", obj_filename))
# Check for libpythonX.Y.so, libpythonX.Yd.so, with optional versioning
is_libpython = bool(re.match(r"^libpython\d+\.\d+d?\.so(\.\d)*$", obj_filename))
# Check for cpython compiled modules (e.g. _ctypes.cpython-38-x86_64-linux-gnu.so)
is_cpython_module = "cpython" in obj_filename
# Check for libffi.so, with optional versioning
is_ffi = bool(re.match(r"^libffi.so(\.\d)*$", obj_filename))
is_python = is_python_exe or is_libpython or is_cpython_module or is_ffi
return is_python
@staticmethod
def is_python_evalframe(frame):
"""Check if this is a python stack frame
Returns True if the stack frame is a C++ frame that
corresponds to a Python stack frame
"""
# python3.8-gdb.py looks for "_PyEval_EvalFrameDefault", but
# that has arguments optimized out on ubuntu 20.04. Instead,
# let's use PyEval_EvalFrameEx instead. This is part of the
# CPython API, so it should be more stable to find.
return _unwrap_frame(frame).name() == "_PyEval_EvalFrameDefault"
class PythonFrameDecorator(FrameDecorator):
def __init__(self, evalframe, frames):
super().__init__(evalframe)
self.frame = evalframe
self.gdbframe = _unwrap_frame(evalframe)
self._elided = frames
@StacktraceOnRaise()
def elided(self):
return self._elided
@property
def pyop(self):
if hasattr(self, "_pyop"):
return self._pyop
# All gdb python extensions run in the same __main__
# namespace. This Frame object is defined by python*-gdb.py,
# and gives some utilities for interacting with python stack
# frames. Repeat the checks for it, in case the
# python*-gdb.py helpers were loaded since the last time.
PyFrame = globals().get("Frame", None)
if PyFrame is None:
return
pyframe = PyFrame(self.gdbframe)
self._pyop = pyframe.get_pyop()
return self._pyop
@StacktraceOnRaise()
def get_pyframe_argument(self):
frames_to_check = [self.gdbframe, self.gdbframe.older()]
def symbols(frame):
frame_vars = gdb.FrameDecorator.FrameVars(frame)
for wrapper in frame_vars.fetch_frame_args():
yield wrapper.sym
for wrapper in frame_vars.fetch_frame_locals():
yield wrapper.sym
for frame in frames_to_check:
for sym in symbols(frame):
if str(sym.type) == "PyFrameObject *":
val = sym.value(frame)
if not val.is_optimized_out:
pointer = int(val)
return pointer
return None
@StacktraceOnRaise()
def filename(self):
pyframe = self.get_pyframe_argument()
if pyframe is not None:
result = gdb.parse_and_eval(
f"PyUnicode_AsUTF8(((PyFrameObject*){pyframe})->f_code->co_filename)"
)
return result.string()
if self.pyop is not None:
return self.pyop.filename()
return "Unknown python file"
def line(self):
# This is what py-bt uses, which can return line numbers
# outside of the length of the file.
# return self.pyop.current_line_num()
# This gives the line number of the start of the function.
# Closer, but still not there.
# return self.pyop.f_lineno
# Instead, evil dirty hackery.
# Look for the an argument passed in that has type
# PyFrameObject*. If it can't be found, fall back to a
# slightly incorrect line number.
pyframe = self.get_pyframe_argument()
if pyframe is not None:
# Call PyFrame_GetLineNumber in the inferior, using whichever
# pointer was found as an argument. The cast of the function
# pointer is a workaround for incorrect debug symbols
# (observed in python3.7-dbg in ubuntu 18.04,
# PyFrame_GetLineNumber showed 4 arguments instead of 1).
line_num = gdb.parse_and_eval(
f"((int (*)(PyFrameObject*))PyFrame_GetLineNumber)((PyFrameObject*){pyframe})"
)
return int(line_num)
if self.pyop is not None:
return self.pyop.f_lineno
return None
@StacktraceOnRaise()
def frame_args(self):
# TODO: Extract python arguments to print here.
# python3.8-dbg.py provides pyop.iter_locals(), though that
# needs to be run in the _PyEval_EvalFrameDefault gdb frame.
# It doesn't distinguish between arguments and local
# variables, but that should be possible to determine, because
# inspect.getargvalues is a thing that exists.
return None
@StacktraceOnRaise()
def function(self):
pyframe = self.get_pyframe_argument()
if pyframe is not None:
result = gdb.parse_and_eval(
f"PyUnicode_AsUTF8(((PyFrameObject*){pyframe})->f_code->co_name)"
)
return result.string()
if self.pyop is not None:
return self.pyop.co_name.proxyval(set())
return "Unknown python function"
@StacktraceOnRaise()
def address(self):
return None
class ElideFilter(FrameFilter, filter_level=FilterLevel.Disabled):
@abstractmethod
def _elide_frame(self, frame: Union[gdb.Frame, FrameDecorator]) -> bool:
"""Whether the frame should be elided.
Return true if the frame should be elided as part of the previous,
false otherwise.
"""
class ElidedFrameDecorator(FrameDecorator):
def __init__(self, frame, elided):
super().__init__(frame)
self._elided = elided
def elided(self):
return self._elided
def filter(self, frame_iter):
prev_nonelided_frame = None
elided_frames = []
def yield_elided():
nonlocal prev_nonelided_frame
nonlocal elided_frames
if elided_frames and prev_nonelided_frame is None:
yield from elided_frames
elided_frames = []
elif elided_frames and prev_nonelided_frame is not None:
yield self.ElidedFrameDecorator(prev_nonelided_frame, elided_frames)
prev_nonelided_frame = None
elided_frames = []
elif not elided_frames and prev_nonelided_frame is not None:
yield prev_nonelided_frame
prev_nonelided_frame = None
for frame in frame_iter:
is_elided = self._elide_frame(frame)
if is_elided:
elided_frames.append(frame)
else:
yield from yield_elided()
prev_nonelided_frame = frame
yield from yield_elided()
class PytestFrameFilter(ElideFilter, filter_level=FilterLevel.Pytest, priority=90):
def _elide_frame(self, frame):
filename = frame.filename()
for package in ["_pytest", "pluggy"]:
if f"packages/{package}/" in filename:
return True
else:
return False
class PackedFuncFilter(ElideFilter, filter_level=FilterLevel.Dispatch):
def _elide_frame(self, frame):
packed_func_c_api = (
"TVMFuncCall(TVMFunctionHandle, TVMValue*, int*, int, TVMValue*, int*)"
)
return (
frame.function() == packed_func_c_api or "packed_func.h" in frame.filename()
)
class FunctorDispatchFilter(ElideFilter, filter_level=FilterLevel.Dispatch):
def _elide_frame(self, frame):
regices = [
r"tvm::.*Functor<.*>::operator\(\)",
r"tvm::.*Functor<.*>::Visit",
r"tvm::.*Functor<.*>::InitVTable",
r"tvm::tir::StmtExprVisitor::VisitExpr",
r"tvm::tir::StmtExprMutator::VisitExpr",
]
function = frame.function()
return isinstance(function, str) and any(
re.search(regex, function) for regex in regices
)
class TransformationBaseClassFilter(
ElideFilter, filter_level=FilterLevel.CommonBaseClass
):
def _elide_frame(self, frame):
return frame.function() in [
"tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const",
"tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const",
"tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const",
"tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const",
"tvm::transform::Pass::operator()(tvm::IRModule) const",
]
class StmtExprVisitorFilter(ElideFilter, filter_level=FilterLevel.CommonBaseClass):
def _elide_frame(self, frame):
regex = r"(((Expr|Stmt)(Visitor|Mutator))|(IR(Visitor|Mutator)WithAnalyzer))::Visit(Stmt|Expr)_"
return re.search(regex, frame.function())