Skip to content

Commit 292960d

Browse files
authored
Merge pull request #5915 from victormlg/cf_profile_py
Added cf-profile.py script for processing profiling output
2 parents 6e7aec4 + c0e36cd commit 292960d

File tree

5 files changed

+266
-115
lines changed

5 files changed

+266
-115
lines changed

cf-agent/cf-agent.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ int main(int argc, char *argv[])
327327

328328
EvalContextProfilingStart(ctx);
329329
KeepPromises(ctx, policy, config);
330-
EvalContextProfilingEnd(ctx, policy);
330+
EvalContextProfilingEnd(ctx);
331331

332332
if (EvalAborted(ctx))
333333
{

contrib/cf-profile/cf-profile.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
return "%s %s %s:%s" % (component, event_type, ns, name)
55+
56+
57+
def format_columns(events, top):
58+
59+
labels = []
60+
61+
for event in events[:top]:
62+
label = format_label(
63+
event["component"], event["type"], event["namespace"], event["name"]
64+
)
65+
location = "%s:%s" % (event["source"], event["offset"]["line"])
66+
time = format_elapsed_time(event["elapsed"])
67+
68+
labels.append((label, location, time))
69+
70+
return labels
71+
72+
73+
def get_max_column_lengths(lines, indent=4):
74+
75+
max_type, max_location, max_time = 0, 0, 0
76+
77+
for label, location, time_ms in lines:
78+
max_type = max(max_type, len(label))
79+
max_location = max(max_location, len(location))
80+
max_time = max(max_time, len(time_ms))
81+
82+
return max_type + indent, max_location + indent, max_time + indent
83+
84+
85+
def profile(events, args):
86+
87+
filter = defaultdict(list)
88+
89+
if args.bundles:
90+
filter["component"].append("bundle")
91+
filter["type"].append("methods")
92+
93+
if args.promises:
94+
filter["type"] += list(
95+
set(
96+
event["type"]
97+
for event in events
98+
if event["component"] == "promise" and event["type"] != "methods"
99+
)
100+
)
101+
102+
if args.functions:
103+
filter["component"].append("function")
104+
105+
# filter events
106+
if filter is not None:
107+
events = [
108+
event
109+
for field in filter.keys()
110+
for event in events
111+
if event[field] in filter[field]
112+
]
113+
114+
# sort events
115+
events = sorted(events, key=lambda x: x["elapsed"], reverse=True)
116+
117+
lines = format_columns(events, args.top)
118+
line_format = "%-{}s %-{}s %{}s".format(*get_max_column_lengths(lines))
119+
120+
# print top k filtered events
121+
print(line_format % ("Type", "Location", "Time"))
122+
for label, location, time_ms in lines:
123+
print(line_format % (label, location, time_ms))
124+
125+
126+
def generate_callstacks(data, stack_path):
127+
128+
with open(stack_path, "w") as f:
129+
for event in data:
130+
f.write("%s %d\n" % (event["callstack"], event["elapsed"]))
131+
132+
133+
def main():
134+
args = parse_args()
135+
m = re.search(r"\[[.\s\S]*\]", sys.stdin.read())
136+
data = json.loads(m.group(0))
137+
138+
if any([args.bundles, args.functions, args.promises]):
139+
profile(data, args)
140+
141+
if args.flamegraph:
142+
generate_callstacks(data, args.flamegraph)
143+
144+
145+
if __name__ == "__main__":
146+
main()

0 commit comments

Comments
 (0)