Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pydumpling/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .debug_dumpling import debug_dumpling, load_dumpling
from .helpers import print_traceback_and_except, validate_file_name
from .rpdb import r_post_mortem

from .vis import generate_vis

parser = argparse.ArgumentParser(
description="pydumpling cli tools",
Expand Down Expand Up @@ -32,6 +32,10 @@
help="enter rpdb debugging interface"
)

pydumpling_cli_action_group.add_argument(
"--vis", action="store_true", help="visualize the traceback"
)

parser.add_argument(
"filename",
type=validate_file_name,
Expand All @@ -49,3 +53,5 @@ def main() -> None:
debug_dumpling(file_name)
elif args.rdebug:
r_post_mortem(file_name)
elif args.vis:
generate_vis(file_name)
19 changes: 19 additions & 0 deletions pydumpling/fake_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ def __init__(self, traceback=None):
traceback.tb_next) if traceback and traceback.tb_next else None
self.tb_lasti = traceback.tb_lasti if traceback else 0

def serialize(self):
return {
'tb_frame': self.tb_frame.serialize() if self.tb_frame else None,
'tb_lineno': self.tb_lineno,
'tb_next': self.tb_next.serialize() if self.tb_next else None,
}


class FakeFrame(FakeType):
def __init__(self, frame):
Expand All @@ -83,6 +90,12 @@ def __init__(self, frame):
self.f_lasti = frame.f_lasti
self.f_builtins = frame.f_builtins

def serialize(self):
return {
'f_code': self.f_code.serialize(),
'f_lineno': self.f_lineno,
}


class FakeClass(FakeType):

Expand Down Expand Up @@ -116,3 +129,9 @@ def __init__(self, code):

def co_lines(self):
return iter(self._co_lines)

def serialize(self):
return {
'co_filename': self.co_filename,
'co_name': self.co_name,
}
161 changes: 161 additions & 0 deletions pydumpling/templates/traceback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>Network</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<script id="traceback-data" type="application/json">{{traceback}}</script>
<style type="text/css">
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}

.container {
display: flex;
flex-direction: column;
align-items: center;
width: 90vw;
}

.header {
text-align: center;
font-size: 1.2em;
font-weight: bold;
margin-bottom: 10px;
padding: 6px;
border-radius: 8px;
display: flex;
justify-content: space-around;
gap: 10px;
width: 100%;
}

.header-item {
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
color: white;
font-weight: bold;
text-align: center;
flex: 1;
}

.dump-file {
background-color: #007BFF;
/* 蓝色 */
}

.exc-type {
background-color: #DC3545;
/* 红色 */
}

.exc-value {
background-color: #28A745;
/* 绿色 */
}

#traceback {
width: 100%;
height: 88vh;
border: 2px dashed red;
}
</style>
</head>

<body>
<div class="container">
<!-- Header displaying additional information -->
<div class="header">
<div class="header-item dump-file">
current dump file: <span>{{dump_file_name}}</span>
</div>
<div class="header-item exc-type">
exc_type: <code>{{exc_type}}</code>
</div>
<div class="header-item exc-value">
exc_value: <code>{{exc_value}}</code>
</div>
</div>
<!-- Container for the network graph -->
<div id="traceback"></div>
</div>
<script type="text/javascript">
const options = {
interaction: {
navigationButtons: true,
keyboard: true,
},
edges: {
font: {
size: 10
},
widthConstraint: {
maximum: 300,
},
},
nodes: {
shape: "box",
margin: 10,
widthConstraint: {
maximum: 1000,
},
},
physics: {
enabled: true, // 启用物理效果
},
layout: {
hierarchical: {
direction: 'UD',
sortMethod: 'directed',
}
}
};


function draw(nodes, edges, container_id, options) {
const networkNodes = new vis.DataSet(nodes);

// create an array with edges
const networkEdges = new vis.DataSet(edges);

// create a network
const container = document.getElementById(container_id);
const data = {
nodes: networkNodes,
edges: networkEdges,
};
const network = new vis.Network(container, data, options);
}

const tracebackData = JSON.parse(document.getElementById('traceback-data').text);
const rawNodes = tracebackData.map(trace => {
return {
id: trace.id,
label: trace.label,
title: `file: ${trace.extra.co_filename}`,
font: { multi: 'html' },
color: trace.extra.co_type === '<module>' ? 'lime' : 'orange'
}
});
const rawEdges = rawNodes.slice(0, -1).map((item, idx) => ({
from: item.id,
to: rawNodes[idx + 1].id,
arrows: {
to: {
enabled: true,
type: 'arrow'
}
},
color: '#ff0000'
}))
draw(rawNodes, rawEdges, 'traceback', options);
</script>
</body>

</html>
60 changes: 60 additions & 0 deletions pydumpling/vis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from linecache import getline
from os import system
from uuid import uuid4
from os.path import splitext, abspath, dirname, join as pjoin
from json import dumps

from pydumpling import load_dumpling

__dir__ = dirname(abspath(__file__))

def _wrap_code(raw_code: str) -> str:
return f'<code>{raw_code}</code>'

def _generate_label(f_code,lineno) -> str:
return f'<b>line: {lineno}</b> \n{_wrap_code("func: " + f_code["co_name"])} \n{_wrap_code(getline(f_code["co_filename"], lineno).strip())}'


def extract_frame(traceback: dict) -> list[dict]:
frames = []
for key, value in traceback.items():
if key == 'tb_frame':
f_code = value['f_code']
frames.append(
{
'id': str(uuid4()),
'label': _generate_label(f_code, value["f_lineno"]),
'extra': {
'co_filename': f_code['co_filename'],
'co_type': f_code['co_name'] == '<module>' and 'module' or 'function',
},
}
)
if isinstance(value, dict):
frames.extend(extract_frame(value))

return frames

def generate_vis(dump_file: str) -> None:
dumpling_result = load_dumpling(dump_file)
traceback = dumpling_result['traceback'].serialize()
raw_frames = extract_frame(traceback)

res = dumps(raw_frames)
vis_html_file = splitext(dump_file)[0] + '.html'
with open(f'{__dir__}/templates/traceback.html', mode='r', encoding='utf-8') as handler:
template = handler.read()
traceback_html = template.replace('{{traceback}}', res)
traceback_html = traceback_html.replace('{{dump_file_name}}', dump_file)
traceback_html = traceback_html.replace(
'{{exc_type}}', repr(dumpling_result['exc_extra']['exc_type']).replace('<', '').replace('>', '')
)
traceback_html = traceback_html.replace(
'{{exc_value}}', repr(dumpling_result['exc_extra']['exc_value'])
)


with open(vis_html_file, mode="w", encoding="utf-8") as f:
f.write(traceback_html)

system(f'start {vis_html_file}')
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ docs_export = { shell = "pdm export -G doc -o docs/requirements.txt --without-ha
docs_preview = {shell = 'python -m http.server -d docs\build\html'}

[tool.pdm.build]
includes = ["pydumpling/*.py"]
includes = ["pydumpling/*.py", "pydumpling/templates/*.html"]

[tool.pdm.dev-dependencies]
test = [
Expand Down
Binary file added static/visualize_traceback.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.