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.
@@ -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,6 @@
1
+ # Copyright (c) 2026 Paul Würtz
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ __author__ = "paulwuertz"
5
+
6
+ from . import *
@@ -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,2 @@
1
+ pydantic>=2.13.5
2
+ tox>=4.54.0
@@ -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,3 @@
1
+ pydantic>=2.13.5
2
+ ruff>=0.17.1
3
+ tox>=4.54.0
@@ -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()