Skip to content

Commit 876ccae

Browse files
committed
Added cf-profile.py script for processing profiling output
Changelog: Title Signed-off-by: Victor Moene <[email protected]>
1 parent 9525710 commit 876ccae

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

contrib/cf-profile/cf-profile.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from argparse import ArgumentParser
2+
import sys
3+
import json
4+
import re
5+
from collections import defaultdict
6+
7+
"""
8+
This script parses profiling output from cf-agent and lists policy bundles, promises, and functions sorted by execution time.
9+
10+
Usage:
11+
$ sudo /var/cfengine/cf-agent -Kp > profiling_output.json
12+
$ cat profiling_output.json | python3 cf-profile.py --bundles --promises --functions
13+
14+
To generate a flamegraph:
15+
$ cat profiling_output.json | python3 cf-profile.py --flamegraph callstack.txt
16+
$ ./flamegraph.pl callstack.txt > flamegraph.svg
17+
"""
18+
19+
20+
def parse_args():
21+
parser = ArgumentParser()
22+
23+
parser.add_argument("--top", type=int, default=10)
24+
parser.add_argument("--bundles", action="store_true")
25+
parser.add_argument("--promises", action="store_true")
26+
parser.add_argument("--functions", action="store_true")
27+
parser.add_argument("--flamegraph", type=str, default="callstack.txt")
28+
29+
return parser.parse_args()
30+
31+
32+
def format_elapsed_time(elapsed_ns):
33+
elapsed_ms = float(elapsed_ns) / 1e6
34+
35+
if elapsed_ms < 1000:
36+
return "%.2f ms" % elapsed_ms
37+
elif elapsed_ms < 60000:
38+
elapsed_s = elapsed_ms / 1000.0
39+
return "%.2fs" % elapsed_s
40+
else:
41+
elapsed_s = elapsed_ms / 1000.0
42+
minutes = int(elapsed_s // 60)
43+
seconds = int(elapsed_s % 60)
44+
return "%dm%ds" % (minutes, seconds)
45+
46+
47+
def format_label(component, event_type, ns, name):
48+
if component == "function":
49+
return "%s %s" % (component, name)
50+
elif event_type == "methods":
51+
return "bundle invocation"
52+
elif component == "promise":
53+
return "%s %s" % (component, event_type)
54+
else:
55+
return "%s %s %s:%s" % (component, event_type, ns, name)
56+
57+
58+
def format_columns(events, top):
59+
60+
labels = []
61+
62+
for event in events[:top]:
63+
label = format_label(
64+
event["component"], event["type"], event["namespace"], event["name"]
65+
)
66+
location = "%s:%s" % (event["source"], event["offset"]["line"])
67+
time = format_elapsed_time(event["elapsed"])
68+
69+
labels.append((label, location, time))
70+
71+
return labels
72+
73+
74+
def get_max_column_lengths(lines, indent=4):
75+
76+
max_type, max_location, max_time = 0, 0, 0
77+
78+
for label, location, time_ms in lines:
79+
max_type = max(max_type, len(label))
80+
max_location = max(max_location, len(location))
81+
max_time = max(max_time, len(time_ms))
82+
83+
return max_type + indent, max_location + indent, max_time + indent
84+
85+
86+
def profile(events, args):
87+
88+
filter = defaultdict(list)
89+
90+
if args.bundles:
91+
filter["component"].append("bundle")
92+
filter["type"].append("methods")
93+
94+
if args.promises:
95+
filter["type"] += list(
96+
set(
97+
event["type"]
98+
for event in events
99+
if event["component"] == "promise" and event["type"] != "methods"
100+
)
101+
)
102+
103+
if args.functions:
104+
filter["component"].append("function")
105+
106+
print(filter)
107+
108+
# filter events
109+
if filter is not None:
110+
events = [
111+
event
112+
for field in filter.keys()
113+
for event in events
114+
if event[field] in filter[field]
115+
]
116+
117+
# sort events
118+
events = sorted(events, key=lambda x: x["elapsed"], reverse=True)
119+
120+
lines = format_columns(events, args.top)
121+
line_format = "%-{}s %-{}s %{}s".format(*get_max_column_lengths(lines))
122+
123+
# print top k filtered events
124+
print(line_format % ("Type", "Location", "Time"))
125+
for label, location, time_ms in lines:
126+
print(line_format % (label, location, time_ms))
127+
128+
129+
def generate_callstacks(data, stack_path):
130+
131+
with open(stack_path, "w") as f:
132+
for event in data:
133+
f.write("%s %d\n" % (event["callstack"], event["elapsed"]))
134+
135+
136+
def main():
137+
args = parse_args()
138+
m = re.search(r"\[[.\s\S]*\]", sys.stdin.read())
139+
data = json.loads(m.group(0))
140+
141+
if any([args.bundles, args.functions, args.promises]):
142+
profile(data, args)
143+
144+
if args.flamegraph:
145+
generate_callstacks(data, args.flamegraph)
146+
147+
148+
if __name__ == "__main__":
149+
main()

0 commit comments

Comments
 (0)