szuncover 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.
- szuncover-0.1.0/PKG-INFO +73 -0
- szuncover-0.1.0/README.md +63 -0
- szuncover-0.1.0/pyproject.toml +63 -0
- szuncover-0.1.0/pyproject.toml.orig +64 -0
- szuncover-0.1.0/szuncover/__init__.py +6 -0
- szuncover-0.1.0/szuncover/calltree.py +136 -0
- szuncover-0.1.0/szuncover/config.py +30 -0
- szuncover-0.1.0/szuncover/gcc_vcg_parser.py +160 -0
- szuncover-0.1.0/szuncover/python_szuncover.egg-info/PKG-INFO +15 -0
- szuncover-0.1.0/szuncover/python_szuncover.egg-info/SOURCES.txt +10 -0
- szuncover-0.1.0/szuncover/python_szuncover.egg-info/dependency_links.txt +1 -0
- szuncover-0.1.0/szuncover/python_szuncover.egg-info/requires.txt +2 -0
- szuncover-0.1.0/szuncover/python_szuncover.egg-info/top_level.txt +1 -0
- szuncover-0.1.0/szuncover/report_output.py +44 -0
- szuncover-0.1.0/szuncover/szuncover.egg-info/PKG-INFO +16 -0
- szuncover-0.1.0/szuncover/szuncover.egg-info/SOURCES.txt +10 -0
- szuncover-0.1.0/szuncover/szuncover.egg-info/dependency_links.txt +1 -0
- szuncover-0.1.0/szuncover/szuncover.egg-info/requires.txt +3 -0
- szuncover-0.1.0/szuncover/szuncover.egg-info/top_level.txt +1 -0
- szuncover-0.1.0/szuncover/szuncover.py +54 -0
szuncover-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: szuncover
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Uncover stack-usages for your threads
|
|
5
|
+
Author: Paul Würtz
|
|
6
|
+
Author-email: Paul Würtz <paulwuertz@posteo.de>
|
|
7
|
+
Requires-Dist: pydantic>=2.13.5
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# szuncover
|
|
13
|
+
|
|
14
|
+
Uncover your threads stack usage. Working only with GCC, using the .ci files generated by the `-callgraph-info=cu,da` options to construct a callgraph with stack-usages.
|
|
15
|
+
|
|
16
|
+
## Installation and Usage
|
|
17
|
+
|
|
18
|
+
Install with pip:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install szuncover
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Create a config listing your thread entry functions:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{ "threads": [
|
|
28
|
+
{
|
|
29
|
+
"thread_entry_name": "bg_thread_main",
|
|
30
|
+
"stack_size": 1024
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"thread_entry_name": "gs_usb_tx_thread",
|
|
34
|
+
"stack_size": 1024
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"thread_entry_name": "gs_usb_rx_thread",
|
|
38
|
+
"stack_size": 1024
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"thread_entry_name": "log_process_thread_func",
|
|
42
|
+
"stack_size": 768
|
|
43
|
+
}
|
|
44
|
+
]}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
then build your firmware with `-callgraph-info=cu,da` and run `szuncover --build_dir ./build/ --config_file config.json` for some basic output:
|
|
48
|
+
|
|
49
|
+
```md
|
|
50
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
51
|
+
│ bg_thread_main - ( 68.0%) 696/ 1024 │█████████████-------│
|
|
52
|
+
│ gs_usb_tx_thread - ( 68.8%) 704/ 1024 │█████████████-------│
|
|
53
|
+
│ gs_usb_rx_thread - ( 60.2%) 616/ 1024 │████████████--------│
|
|
54
|
+
│ log_process_thread_func - ( 34.4%) 264/ 768 │██████--------------│
|
|
55
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
56
|
+
WARNING: Given calltree info may limit this analysis:
|
|
57
|
+
|
|
58
|
+
* number of functions with unresolved calls in thread:
|
|
59
|
+
* bg_thread_main: 22 resulting in 34 missing calls in the calltree
|
|
60
|
+
* gs_usb_tx_thread: 4 resulting in 5 missing calls in the calltree
|
|
61
|
+
* gs_usb_rx_thread: 2 resulting in 2 missing calls in the calltree
|
|
62
|
+
* log_process_thread_func: 3 resulting in 4 missing calls in the calltree
|
|
63
|
+
|
|
64
|
+
* number of functions with missing stacksize in calltree:
|
|
65
|
+
* bg_thread_main : 29
|
|
66
|
+
* gs_usb_tx_thread : 4
|
|
67
|
+
* gs_usb_rx_thread : 0
|
|
68
|
+
* log_process_thread_func : 2
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Running Tests
|
|
72
|
+
|
|
73
|
+
python -m unittest
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
# szuncover
|
|
3
|
+
|
|
4
|
+
Uncover your threads stack usage. Working only with GCC, using the .ci files generated by the `-callgraph-info=cu,da` options to construct a callgraph with stack-usages.
|
|
5
|
+
|
|
6
|
+
## Installation and Usage
|
|
7
|
+
|
|
8
|
+
Install with pip:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install szuncover
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Create a config listing your thread entry functions:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{ "threads": [
|
|
18
|
+
{
|
|
19
|
+
"thread_entry_name": "bg_thread_main",
|
|
20
|
+
"stack_size": 1024
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"thread_entry_name": "gs_usb_tx_thread",
|
|
24
|
+
"stack_size": 1024
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"thread_entry_name": "gs_usb_rx_thread",
|
|
28
|
+
"stack_size": 1024
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"thread_entry_name": "log_process_thread_func",
|
|
32
|
+
"stack_size": 768
|
|
33
|
+
}
|
|
34
|
+
]}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
then build your firmware with `-callgraph-info=cu,da` and run `szuncover --build_dir ./build/ --config_file config.json` for some basic output:
|
|
38
|
+
|
|
39
|
+
```md
|
|
40
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
41
|
+
│ bg_thread_main - ( 68.0%) 696/ 1024 │█████████████-------│
|
|
42
|
+
│ gs_usb_tx_thread - ( 68.8%) 704/ 1024 │█████████████-------│
|
|
43
|
+
│ gs_usb_rx_thread - ( 60.2%) 616/ 1024 │████████████--------│
|
|
44
|
+
│ log_process_thread_func - ( 34.4%) 264/ 768 │██████--------------│
|
|
45
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
46
|
+
WARNING: Given calltree info may limit this analysis:
|
|
47
|
+
|
|
48
|
+
* number of functions with unresolved calls in thread:
|
|
49
|
+
* bg_thread_main: 22 resulting in 34 missing calls in the calltree
|
|
50
|
+
* gs_usb_tx_thread: 4 resulting in 5 missing calls in the calltree
|
|
51
|
+
* gs_usb_rx_thread: 2 resulting in 2 missing calls in the calltree
|
|
52
|
+
* log_process_thread_func: 3 resulting in 4 missing calls in the calltree
|
|
53
|
+
|
|
54
|
+
* number of functions with missing stacksize in calltree:
|
|
55
|
+
* bg_thread_main : 29
|
|
56
|
+
* gs_usb_tx_thread : 4
|
|
57
|
+
* gs_usb_rx_thread : 0
|
|
58
|
+
* log_process_thread_func : 2
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Running Tests
|
|
62
|
+
|
|
63
|
+
python -m unittest
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "szuncover"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Uncover stack-usages for your threads"
|
|
5
|
+
long_description = """
|
|
6
|
+
|
|
7
|
+
Stack size
|
|
8
|
+
===========
|
|
9
|
+
|
|
10
|
+
Estimate stack usage in RTOS threads when compiling with GCC -callgraph-info=cu,da option.
|
|
11
|
+
|
|
12
|
+
From all .ci files a calltree with stack use for each function is constructed.
|
|
13
|
+
|
|
14
|
+
Given a config file a list of threads and associtated stack sizes a report will be generated to each threads stack utilization.
|
|
15
|
+
|
|
16
|
+
For dynamic function-calls not know at compile-time a list of dynamic-calls can complete this missing parts of the runtime callgraph to get better results in the estimation."""
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
requires-python = ">=3.12"
|
|
19
|
+
dependencies = ["pydantic>=2.13.5"]
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
name = "Paul Würtz"
|
|
23
|
+
email = "paulwuertz@posteo.de"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
szuncover = "szuncover.szuncover:main"
|
|
27
|
+
|
|
28
|
+
[tool.uv]
|
|
29
|
+
default-groups = ["dev"]
|
|
30
|
+
|
|
31
|
+
[tool.uv.build-backend]
|
|
32
|
+
module-root = ""
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py312"
|
|
36
|
+
preview = true
|
|
37
|
+
line-length = 100
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
classifiers = [
|
|
41
|
+
"Development Status :: 5 - Production/Stable",
|
|
42
|
+
"Intended Audience :: Developers",
|
|
43
|
+
"Natural Language :: English",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["szuncover"]
|
|
48
|
+
include = ["szuncover*"]
|
|
49
|
+
namespaces = false
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build >= 0.12.19, <0.13.0"]
|
|
53
|
+
build-backend = "uv_build"
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
dev = [
|
|
57
|
+
"codecov==2.1.13",
|
|
58
|
+
"tox==3.27.1; python_version < '3.12'",
|
|
59
|
+
"tox==4.11.3; python_version >= '3.12'",
|
|
60
|
+
"setuptools>=75",
|
|
61
|
+
"ruff>=0.12.10",
|
|
62
|
+
"pydantic>=2.13.3",
|
|
63
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "szuncover"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Uncover stack-usages for your threads"
|
|
5
|
+
long_description = """
|
|
6
|
+
|
|
7
|
+
Stack size
|
|
8
|
+
===========
|
|
9
|
+
|
|
10
|
+
Estimate stack usage in RTOS threads when compiling with GCC -callgraph-info=cu,da option.
|
|
11
|
+
|
|
12
|
+
From all .ci files a calltree with stack use for each function is constructed.
|
|
13
|
+
|
|
14
|
+
Given a config file a list of threads and associtated stack sizes a report will be generated to each threads stack utilization.
|
|
15
|
+
|
|
16
|
+
For dynamic function-calls not know at compile-time a list of dynamic-calls can complete this missing parts of the runtime callgraph to get better results in the estimation."""
|
|
17
|
+
authors = [
|
|
18
|
+
{ name = "Paul Würtz", email = "paulwuertz@posteo.de" }
|
|
19
|
+
]
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
requires-python = ">=3.12"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pydantic>=2.13.5",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
szuncover = "szuncover.szuncover:main"
|
|
28
|
+
|
|
29
|
+
[tool.uv]
|
|
30
|
+
default-groups = ["dev"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py312"
|
|
34
|
+
preview = true
|
|
35
|
+
line-length = 100
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build >= 0.12.19, <0.13.0"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[tool.uv.build-backend]
|
|
42
|
+
module-root = ""
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"codecov==2.1.13",
|
|
47
|
+
"tox==3.27.1; python_version < '3.12'",
|
|
48
|
+
"tox==4.11.3; python_version >= '3.12'",
|
|
49
|
+
"setuptools>=75",
|
|
50
|
+
"ruff>=0.12.10",
|
|
51
|
+
"pydantic>=2.13.3",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools]
|
|
55
|
+
classifiers = [
|
|
56
|
+
"Development Status :: 5 - Production/Stable",
|
|
57
|
+
"Intended Audience :: Developers",
|
|
58
|
+
"Natural Language :: English",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["szuncover"]
|
|
63
|
+
include = [ "szuncover*" ]
|
|
64
|
+
namespaces = false
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from gcc_vcg_parser import (
|
|
5
|
+
GCC_INDIRECT_CALL_LABEL,
|
|
6
|
+
FunctionCalleeEntry,
|
|
7
|
+
VCG_Edge,
|
|
8
|
+
VCG_Graph,
|
|
9
|
+
VCG_Parser,
|
|
10
|
+
)
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
NO_WORST_STACK_SIZE_YET = -1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ThreadStats(BaseModel):
|
|
17
|
+
unresolved_calls: dict[str, int]
|
|
18
|
+
missing_stack_sizes: list[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_files_with_extension(extension: str, top: str):
|
|
22
|
+
for path, dirlist, filelist in os.walk(top):
|
|
23
|
+
for name in fnmatch.filter(filelist, extension):
|
|
24
|
+
yield os.path.join(path, name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_vcg_graphs_from_build_dir(build_dir: str) -> list[VCG_Graph]:
|
|
28
|
+
# print("parsing callgraph starting at %s" % build_dir)
|
|
29
|
+
vcg_graphs = []
|
|
30
|
+
call_files = find_files_with_extension("*.ci", build_dir)
|
|
31
|
+
|
|
32
|
+
for ci_file in call_files:
|
|
33
|
+
with open(ci_file) as file:
|
|
34
|
+
ci_file_content = file.read()
|
|
35
|
+
vcg_graph = VCG_Parser.parse_vcg_graph(ci_file_content)
|
|
36
|
+
vcg_graphs += [vcg_graph]
|
|
37
|
+
|
|
38
|
+
return vcg_graphs
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_all_edges_from_vcg_graphs(vcg_graphs: list[VCG_Graph]) -> list[VCG_Edge]:
|
|
42
|
+
all_edges = []
|
|
43
|
+
for vcg_graph in vcg_graphs:
|
|
44
|
+
all_edges += vcg_graph.edges
|
|
45
|
+
return all_edges
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def map_vcg_nodes_to_call_entries(vcg_graphs: list[VCG_Graph]) -> dict[str, FunctionCalleeEntry]:
|
|
49
|
+
fn_callee_map: dict[str, FunctionCalleeEntry] = {}
|
|
50
|
+
for vcg_graph in vcg_graphs:
|
|
51
|
+
for n in vcg_graph.nodes:
|
|
52
|
+
has_label = "label" in n.model_fields_set
|
|
53
|
+
has_function_name = has_label and "function_name" in n.label.model_fields_set
|
|
54
|
+
fn_name = n.label.function_name if has_function_name else n.title
|
|
55
|
+
if "." in fn_name: # removes .constprop.0, .isra, ...
|
|
56
|
+
split_dots = fn_name.split(".")
|
|
57
|
+
fn_name = split_dots[0]
|
|
58
|
+
is_symbol_mapped = fn_name in fn_callee_map
|
|
59
|
+
is_stack_size_defined = is_symbol_mapped and isinstance(
|
|
60
|
+
fn_callee_map[fn_name].node.label.stack_size, int
|
|
61
|
+
)
|
|
62
|
+
if not is_symbol_mapped:
|
|
63
|
+
fn_callee_map[fn_name] = FunctionCalleeEntry(
|
|
64
|
+
title=fn_name,
|
|
65
|
+
node=n,
|
|
66
|
+
worst_stack_use=-1,
|
|
67
|
+
edges=[],
|
|
68
|
+
visited=False,
|
|
69
|
+
)
|
|
70
|
+
elif not is_stack_size_defined and isinstance(n.label.stack_size, int):
|
|
71
|
+
fn_callee_map[fn_name].node.label = n.label
|
|
72
|
+
else:
|
|
73
|
+
# TODO add definition or declaration source path?!
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
return fn_callee_map
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def map_vcg_call_edges_to_call_entries(
|
|
80
|
+
fn_callee_map: dict[str, FunctionCalleeEntry], edges: list[VCG_Edge]
|
|
81
|
+
) -> dict[str, FunctionCalleeEntry]:
|
|
82
|
+
err_unmatched_edges: set[str] = set()
|
|
83
|
+
for e in edges:
|
|
84
|
+
fn_name = e.sourcename
|
|
85
|
+
fn_name_and_path = fn_name.split(":", 1)
|
|
86
|
+
if fn_name not in fn_callee_map and len(fn_name_and_path) == 2:
|
|
87
|
+
fn_name = fn_name_and_path[1]
|
|
88
|
+
if "." in fn_name: # removes .constprop.0, .isra, ...
|
|
89
|
+
split_dots = fn_name.split(".")
|
|
90
|
+
fn_name = split_dots[0]
|
|
91
|
+
if fn_name in fn_callee_map:
|
|
92
|
+
fn_callee_map[fn_name].edges += [e]
|
|
93
|
+
else:
|
|
94
|
+
err_unmatched_edges.add(fn_name)
|
|
95
|
+
print("err #", len(err_unmatched_edges), " no node matches call edge:", e)
|
|
96
|
+
return fn_callee_map
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def deepest_call_tree(
|
|
100
|
+
f: FunctionCalleeEntry,
|
|
101
|
+
fn_callee_map: dict[str, FunctionCalleeEntry],
|
|
102
|
+
stats: ThreadStats,
|
|
103
|
+
visited=None,
|
|
104
|
+
) -> tuple[int, list[FunctionCalleeEntry]]:
|
|
105
|
+
if f.worst_stack_use != NO_WORST_STACK_SIZE_YET:
|
|
106
|
+
return (f.worst_stack_use, [])
|
|
107
|
+
visited = [f] + (visited or [])
|
|
108
|
+
result: tuple[int, list[FunctionCalleeEntry]] = (0, [])
|
|
109
|
+
|
|
110
|
+
for call_edge in f.edges:
|
|
111
|
+
if call_edge.targetname == GCC_INDIRECT_CALL_LABEL:
|
|
112
|
+
if f.title in stats.unresolved_calls:
|
|
113
|
+
stats.unresolved_calls[f.title] += 1
|
|
114
|
+
else:
|
|
115
|
+
stats.unresolved_calls[f.title] = 1
|
|
116
|
+
fn_target_name_and_path = call_edge.targetname.split(":", 1)
|
|
117
|
+
callee_name = call_edge.targetname
|
|
118
|
+
if callee_name not in fn_callee_map and len(fn_target_name_and_path) == 2:
|
|
119
|
+
callee_name = fn_target_name_and_path[1]
|
|
120
|
+
if "." in callee_name: # removes .constprop.0, .isra, ...
|
|
121
|
+
split_dots = callee_name.split(".")
|
|
122
|
+
callee_name = split_dots[0]
|
|
123
|
+
if callee_name not in fn_callee_map:
|
|
124
|
+
continue
|
|
125
|
+
callee_node = fn_callee_map[callee_name]
|
|
126
|
+
if callee_node not in visited:
|
|
127
|
+
candidate = deepest_call_tree(callee_node, fn_callee_map, stats, visited)
|
|
128
|
+
if candidate[0] > result[0]:
|
|
129
|
+
result = candidate
|
|
130
|
+
|
|
131
|
+
if not isinstance(f.node.label.stack_size, int):
|
|
132
|
+
stats.missing_stack_sizes += [f.title]
|
|
133
|
+
stack_size = f.node.label.stack_size if isinstance(f.node.label.stack_size, int) else 0
|
|
134
|
+
result = (result[0] + stack_size, [f] + result[1])
|
|
135
|
+
f.worst_stack_use = result[0]
|
|
136
|
+
return result
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThreadConfig(BaseModel):
|
|
9
|
+
thread_entry_name: str
|
|
10
|
+
stack_size: int
|
|
11
|
+
# TODO only functions so far, so cannot deduce from variable symbol atm...
|
|
12
|
+
# stack_variable_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DynamicCallResolutions(BaseModel):
|
|
16
|
+
caller: str
|
|
17
|
+
callees: list[str | None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SZuncoverConfig(BaseModel):
|
|
21
|
+
threads: list[ThreadConfig]
|
|
22
|
+
dynamic_calls: list[DynamicCallResolutions]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_config(conf_path: str) -> SZuncoverConfig | None:
|
|
26
|
+
try:
|
|
27
|
+
return SZuncoverConfig.parse_file(path=conf_path)
|
|
28
|
+
except BaseException:
|
|
29
|
+
logger.exception("Error loading szuncover config file")
|
|
30
|
+
return None
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from pydantic.experimental.missing_sentinel import MISSING
|
|
5
|
+
|
|
6
|
+
GCC_INDIRECT_CALL_LABEL = "__indirect_call"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# TODO add dynamic allocation parsing?
|
|
10
|
+
class VCG_Label(BaseModel):
|
|
11
|
+
file_name: str | MISSING = MISSING
|
|
12
|
+
function_name: str | MISSING = MISSING
|
|
13
|
+
line: int | MISSING = MISSING
|
|
14
|
+
column: int | MISSING = MISSING
|
|
15
|
+
stack_size: int | MISSING = MISSING
|
|
16
|
+
stack_qualifier: str | MISSING = MISSING
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VCG_Node(BaseModel):
|
|
20
|
+
title: str
|
|
21
|
+
label: VCG_Label | MISSING = MISSING
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VCG_Edge(BaseModel):
|
|
25
|
+
sourcename: str
|
|
26
|
+
targetname: str
|
|
27
|
+
label: str | MISSING = MISSING
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VCG_Graph(BaseModel):
|
|
31
|
+
title: str
|
|
32
|
+
nodes: list[VCG_Node]
|
|
33
|
+
edges: list[VCG_Edge]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FunctionCalleeEntry(BaseModel):
|
|
37
|
+
title: str
|
|
38
|
+
node: VCG_Node
|
|
39
|
+
node_declaration: str | MISSING = MISSING
|
|
40
|
+
node_definition: str | MISSING = MISSING
|
|
41
|
+
edges: list[VCG_Edge]
|
|
42
|
+
worst_stack_use: int
|
|
43
|
+
visited: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FunctionCallTree(BaseModel):
|
|
47
|
+
root: FunctionCalleeEntry
|
|
48
|
+
node_declaration: str | MISSING = MISSING
|
|
49
|
+
node_definition: str | MISSING = MISSING
|
|
50
|
+
edges: list[VCG_Edge]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class VCG_Parser:
|
|
54
|
+
su_marker_pattern = r"(\d+)\s+bytes\s+\((.*?)\)"
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def parse_vcg_label_attributes(label_str: str):
|
|
58
|
+
vcg_label_lines = label_str.split("\\n")
|
|
59
|
+
|
|
60
|
+
function_name = vcg_label_lines[0] if len(vcg_label_lines) >= 1 else None
|
|
61
|
+
fp_split = (
|
|
62
|
+
vcg_label_lines[1].split(":") if len(vcg_label_lines) >= 2 else [None, None, None]
|
|
63
|
+
)
|
|
64
|
+
line = int(fp_split[-2]) if len(fp_split) >= 3 and fp_split[-2] else None
|
|
65
|
+
column = int(fp_split[-1]) if len(fp_split) >= 3 and fp_split[-1] else None
|
|
66
|
+
file_name = ":".join(fp_split[:-2]) if len(fp_split) >= 3 and fp_split[0] else None
|
|
67
|
+
# callgraph with su MARKERS - Match stacksize and qualifier in i.e. '15 bytes (static)'
|
|
68
|
+
stacksize_marker = vcg_label_lines[2] if len(vcg_label_lines) >= 3 else ""
|
|
69
|
+
match = re.search(VCG_Parser.su_marker_pattern, stacksize_marker)
|
|
70
|
+
stack_size = int(match.group(1)) if match else None
|
|
71
|
+
stack_qualifier = match.group(2) if match else None
|
|
72
|
+
if function_name and len(vcg_label_lines) == 1:
|
|
73
|
+
return VCG_Label(
|
|
74
|
+
function_name=function_name,
|
|
75
|
+
)
|
|
76
|
+
if function_name and line and column and file_name and len(vcg_label_lines) == 2:
|
|
77
|
+
return VCG_Label(
|
|
78
|
+
function_name=function_name,
|
|
79
|
+
file_name=file_name,
|
|
80
|
+
line=line,
|
|
81
|
+
column=column,
|
|
82
|
+
)
|
|
83
|
+
elif function_name and line and column and file_name and stack_size and stack_qualifier:
|
|
84
|
+
return VCG_Label(
|
|
85
|
+
function_name=function_name,
|
|
86
|
+
file_name=file_name,
|
|
87
|
+
line=line,
|
|
88
|
+
column=column,
|
|
89
|
+
stack_size=stack_size,
|
|
90
|
+
stack_qualifier=stack_qualifier,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
# TODO log error/warning?
|
|
94
|
+
return VCG_Label()
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def parse_vcg_attributes(attr_str: str):
|
|
98
|
+
attribute_pattern = r'(\w+)\s*:\s*(?:"([^"]*)"|(\S+))'
|
|
99
|
+
attributes = re.findall(attribute_pattern, attr_str)
|
|
100
|
+
parsed_dict = {match[0]: match[1] or match[2] for match in attributes}
|
|
101
|
+
return parsed_dict
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def parse_node(node_str: str) -> VCG_Node:
|
|
105
|
+
attributes = VCG_Parser.parse_vcg_attributes(node_str)
|
|
106
|
+
title = attributes.get("title", "")
|
|
107
|
+
label = attributes.get("label", "")
|
|
108
|
+
vcg_label = VCG_Parser.parse_vcg_label_attributes(label)
|
|
109
|
+
if title and label:
|
|
110
|
+
return VCG_Node(title=title, label=vcg_label)
|
|
111
|
+
if title:
|
|
112
|
+
return VCG_Node(title=title)
|
|
113
|
+
else:
|
|
114
|
+
return VCG_Node(title=title) # TODO error handling/logwarning?
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def parse_edge(node_str: str) -> VCG_Edge:
|
|
118
|
+
attributes = VCG_Parser.parse_vcg_attributes(node_str)
|
|
119
|
+
sourcename = attributes.get("sourcename", "")
|
|
120
|
+
targetname = attributes.get("targetname", "")
|
|
121
|
+
label = attributes.get("label", "")
|
|
122
|
+
if sourcename and targetname and label:
|
|
123
|
+
return VCG_Edge(sourcename=sourcename, targetname=targetname, label=label)
|
|
124
|
+
if sourcename and targetname:
|
|
125
|
+
return VCG_Edge(sourcename=sourcename, targetname=targetname)
|
|
126
|
+
else:
|
|
127
|
+
# TODO log error/warning or empty ok?
|
|
128
|
+
return VCG_Edge(sourcename=sourcename, targetname=targetname)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def parse_vcg_graph(graph_str: str):
|
|
132
|
+
lines = graph_str.splitlines()
|
|
133
|
+
# looked at gcc/toplev.cc::lang_dependent_init
|
|
134
|
+
# assume exactly one edge / node per line and graph+title in first line
|
|
135
|
+
graph_decl = lines[0] if len(lines) >= 1 else ""
|
|
136
|
+
title = ""
|
|
137
|
+
# strip graph+title and quotes
|
|
138
|
+
if graph_decl.startswith("graph: { title: "):
|
|
139
|
+
title = graph_decl.replace("graph: { title: ", "")
|
|
140
|
+
title = title[1:-1]
|
|
141
|
+
else:
|
|
142
|
+
pass # TODO error log
|
|
143
|
+
|
|
144
|
+
nodes = []
|
|
145
|
+
edges = []
|
|
146
|
+
for line in lines[1:]:
|
|
147
|
+
line = line.removesuffix(" }")
|
|
148
|
+
if line.startswith("node: { "):
|
|
149
|
+
node_str = line.replace("^node: { ", "")
|
|
150
|
+
n = VCG_Parser.parse_node(node_str)
|
|
151
|
+
nodes += [n]
|
|
152
|
+
elif line.startswith("edge: { "):
|
|
153
|
+
edge_str = line.replace("edge: { ", "")
|
|
154
|
+
e = VCG_Parser.parse_edge(edge_str)
|
|
155
|
+
edges += [e]
|
|
156
|
+
elif line.startswith("}"):
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
graph = VCG_Graph(title=title, edges=edges, nodes=nodes)
|
|
160
|
+
return graph
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-szuncover
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Home-page: https://github.com/zephyrproject-rtos/zephyr
|
|
6
|
+
Author: Zephyr Project
|
|
7
|
+
Author-email: devel@lists.zephyrproject.org
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pydantic>=2.13.5
|
|
11
|
+
Requires-Dist: tox>=4.54.0
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
szuncover/python_szuncover.egg-info/PKG-INFO
|
|
5
|
+
szuncover/python_szuncover.egg-info/SOURCES.txt
|
|
6
|
+
szuncover/python_szuncover.egg-info/dependency_links.txt
|
|
7
|
+
szuncover/python_szuncover.egg-info/requires.txt
|
|
8
|
+
szuncover/python_szuncover.egg-info/top_level.txt
|
|
9
|
+
tests/test_ctx.py
|
|
10
|
+
tests/test_gcc_vcg_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from calltree import ThreadStats, deepest_call_tree
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def print_stack_size_report(fn_callee_map, config):
|
|
7
|
+
overflows = False
|
|
8
|
+
print("┌" + 78 * "─" + "┐")
|
|
9
|
+
threadStats = {}
|
|
10
|
+
for thread in config.threads:
|
|
11
|
+
thread_fn_name = thread.thread_entry_name
|
|
12
|
+
thread_stack_size = thread.stack_size
|
|
13
|
+
if thread_fn_name in fn_callee_map:
|
|
14
|
+
fn_node = fn_callee_map[thread_fn_name]
|
|
15
|
+
stats = ThreadStats(unresolved_calls={}, missing_stack_sizes=[])
|
|
16
|
+
thread_calltree = deepest_call_tree(fn_node, fn_callee_map, stats)
|
|
17
|
+
threadStats[thread_fn_name] = stats
|
|
18
|
+
worst_stack_size = thread_calltree[0]
|
|
19
|
+
# stack_report = stack_report[function_name]
|
|
20
|
+
stack_use_percent = worst_stack_size / thread_stack_size * 100
|
|
21
|
+
progressbar_20_full = int(stack_use_percent / 100.0 * 20)
|
|
22
|
+
progressbar_20_empty = 20 - progressbar_20_full
|
|
23
|
+
# │ log_process_thread_func - 100.0% - 11111/22222 - │███████████████-----│
|
|
24
|
+
print(
|
|
25
|
+
f"│ {thread_fn_name:<30} - ({f'{stack_use_percent:.1f}':>5}%) {worst_stack_size:>6}"
|
|
26
|
+
f"/{thread_stack_size:>6} │{progressbar_20_full * '█'}{progressbar_20_empty * '-'}│"
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
print(f"│ {thread_fn_name:<30} - (function not found in build artifacts) │")
|
|
30
|
+
print("└" + 78 * "─" + "┘")
|
|
31
|
+
|
|
32
|
+
print("WARNING: Given calltree info may limit this analysis: ")
|
|
33
|
+
print("* number of functions with unresolved calls in thread:")
|
|
34
|
+
for thread_fn_name, stats in threadStats.items():
|
|
35
|
+
total_calls_to_unresolved_all_calls = sum(stats.unresolved_calls.values())
|
|
36
|
+
print(
|
|
37
|
+
f"\t* {thread_fn_name}: {len(stats.unresolved_calls)} resulting in {total_calls_to_unresolved_all_calls} missing calls in the calltree"
|
|
38
|
+
)
|
|
39
|
+
print("* number of functions with missing stacksize in calltree:")
|
|
40
|
+
for thread_fn_name, stats in threadStats.items():
|
|
41
|
+
print("\t* ", thread_fn_name, ": ", len(stats.missing_stack_sizes))
|
|
42
|
+
|
|
43
|
+
if overflows:
|
|
44
|
+
sys.exit(1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: szuncover
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Home-page: https://github.com/zephyrproject-rtos/zephyr
|
|
6
|
+
Author: Paul Würtz
|
|
7
|
+
Author-email: paulwuertz@posteo.de
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pydantic>=2.13.5
|
|
11
|
+
Requires-Dist: ruff>=0.17.1
|
|
12
|
+
Requires-Dist: tox>=4.54.0
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: home-page
|
|
16
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
szuncover/szuncover.egg-info/PKG-INFO
|
|
5
|
+
szuncover/szuncover.egg-info/SOURCES.txt
|
|
6
|
+
szuncover/szuncover.egg-info/dependency_links.txt
|
|
7
|
+
szuncover/szuncover.egg-info/requires.txt
|
|
8
|
+
szuncover/szuncover.egg-info/top_level.txt
|
|
9
|
+
tests/test_ctx.py
|
|
10
|
+
tests/test_gcc_vcg_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from calltree import (
|
|
8
|
+
get_all_edges_from_vcg_graphs,
|
|
9
|
+
get_vcg_graphs_from_build_dir,
|
|
10
|
+
map_vcg_call_edges_to_call_entries,
|
|
11
|
+
map_vcg_nodes_to_call_entries,
|
|
12
|
+
)
|
|
13
|
+
from config import load_config
|
|
14
|
+
from report_output import print_stack_size_report
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_args():
|
|
20
|
+
"""Parse command line arguments"""
|
|
21
|
+
argparser = argparse.ArgumentParser(allow_abbrev=False)
|
|
22
|
+
|
|
23
|
+
argparser.add_argument("--build_dir", help="")
|
|
24
|
+
argparser.add_argument("--config_file", help="")
|
|
25
|
+
argparser.add_argument("--debug", action="store_true", help="Print extra debugging information")
|
|
26
|
+
|
|
27
|
+
return argparser.parse_args()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
"""Main function of log parser"""
|
|
32
|
+
args = parse_args()
|
|
33
|
+
|
|
34
|
+
if args.debug:
|
|
35
|
+
logger.setLevel(logging.DEBUG)
|
|
36
|
+
else:
|
|
37
|
+
logger.setLevel(logging.INFO)
|
|
38
|
+
|
|
39
|
+
conf = load_config(args.config_file)
|
|
40
|
+
if not conf:
|
|
41
|
+
print("Config file exception, exiting...")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
vcg_graphs = get_vcg_graphs_from_build_dir(args.build_dir)
|
|
45
|
+
edges = get_all_edges_from_vcg_graphs(vcg_graphs)
|
|
46
|
+
call_entries = map_vcg_nodes_to_call_entries(vcg_graphs)
|
|
47
|
+
fn_callee_map = map_vcg_call_edges_to_call_entries(call_entries, edges)
|
|
48
|
+
|
|
49
|
+
print_stack_size_report(fn_callee_map, conf)
|
|
50
|
+
print("hi")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|