Skip to content

Commit ca7773c

Browse files
authored
Merge pull request #9 from victormlg/cf-profile
Moved cf-profile.py to cfengine-cli
2 parents b128a3c + d0f0d93 commit ca7773c

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

src/cfengine_cli/commands.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import sys
22
import os
3+
import re
4+
import json
5+
from cfengine_cli.profile import profile_cfengine, generate_callstack
36
from cfengine_cli.dev import dispatch_dev_subcommand
47
from cfengine_cli.lint import lint_cfbs_json, lint_json, lint_policy_file
58
from cfengine_cli.shell import user_command
@@ -128,3 +131,19 @@ def run() -> int:
128131

129132
def dev(subcommand, args) -> int:
130133
return dispatch_dev_subcommand(subcommand, args)
134+
135+
136+
def profile(args) -> int:
137+
data = None
138+
with open(args.profiling_input, "r") as f:
139+
m = re.search(r"\[[.\s\S]*\]", f.read())
140+
if m is not None:
141+
data = json.loads(m.group(0))
142+
143+
if data is not None and any([args.bundles, args.functions, args.promises]):
144+
profile_cfengine(data, args)
145+
146+
if args.flamegraph:
147+
generate_callstack(data, args.flamegraph)
148+
149+
return 0

src/cfengine_cli/main.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ def _get_arg_parser():
5959
"run", help="Run the CFEngine agent, fetching, evaluating, and enforcing policy"
6060
)
6161

62+
profile_parser = subp.add_parser(
63+
"profile", help="Parse CFEngine profiling output (cf-agent -Kp)"
64+
)
65+
profile_parser.add_argument(
66+
"profiling_input", help="Path to the profiling input file"
67+
)
68+
profile_parser.add_argument("--top", type=int, default=10)
69+
profile_parser.add_argument("--bundles", action="store_true")
70+
profile_parser.add_argument("--promises", action="store_true")
71+
profile_parser.add_argument("--functions", action="store_true")
72+
profile_parser.add_argument(
73+
"--flamegraph", type=str, help="Generate input file for ./flamegraph.pl"
74+
)
75+
6276
dev_parser = subp.add_parser(
6377
"dev", help="Utilities intended for developers / maintainers of CFEngine"
6478
)
@@ -101,6 +115,8 @@ def run_command_with_args(args) -> int:
101115
return commands.run()
102116
if args.command == "dev":
103117
return commands.dev(args.dev_command, args)
118+
if args.command == "profile":
119+
return commands.profile(args)
104120
raise UserError(f"Unknown command: '{args.command}'")
105121

106122

src/cfengine_cli/profile.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import os
2+
from collections import defaultdict
3+
4+
5+
def format_elapsed_time(elapsed_ns):
6+
elapsed_ms = float(elapsed_ns) / 1e6
7+
8+
if elapsed_ms < 1000:
9+
return "%.2f ms" % elapsed_ms
10+
elif elapsed_ms < 60000:
11+
elapsed_s = elapsed_ms / 1000.0
12+
return "%.2fs" % elapsed_s
13+
else:
14+
elapsed_s = elapsed_ms / 1000.0
15+
minutes = int(elapsed_s // 60)
16+
seconds = int(elapsed_s % 60)
17+
return "%dm%ds" % (minutes, seconds)
18+
19+
20+
def format_label(component, event_type, ns, name):
21+
if component == "function":
22+
return "%s %s" % (component, name)
23+
elif event_type == "methods":
24+
return "bundle invocation"
25+
elif component == "promise":
26+
return "%s %s" % (component, event_type)
27+
return "%s %s %s:%s" % (component, event_type, ns, name)
28+
29+
30+
def format_columns(events, top):
31+
32+
labels = []
33+
34+
for event in events[:top]:
35+
label = format_label(
36+
event["component"], event["type"], event["namespace"], event["name"]
37+
)
38+
location = "%s:%s" % (event["source"], event["offset"]["line"])
39+
time = format_elapsed_time(event["elapsed"])
40+
41+
labels.append((label, location, time))
42+
43+
return labels
44+
45+
46+
def get_max_column_lengths(lines, indent=4):
47+
48+
max_type, max_location, max_time = 0, 0, 0
49+
50+
for label, location, time_ms in lines:
51+
max_type = max(max_type, len(label))
52+
max_location = max(max_location, len(location))
53+
max_time = max(max_time, len(time_ms))
54+
55+
return max_type + indent, max_location + indent, max_time + indent
56+
57+
58+
def profile_cfengine(events, args):
59+
60+
filter = defaultdict(list)
61+
62+
if args.bundles:
63+
filter["component"].append("bundle")
64+
filter["type"].append("methods")
65+
66+
if args.promises:
67+
filter["type"] += list(
68+
set(
69+
event["type"]
70+
for event in events
71+
if event["component"] == "promise" and event["type"] != "methods"
72+
)
73+
)
74+
75+
if args.functions:
76+
filter["component"].append("function")
77+
78+
# filter events
79+
if filter is not None:
80+
events = [
81+
event
82+
for field in filter.keys()
83+
for event in events
84+
if event[field] in filter[field]
85+
]
86+
87+
# sort events
88+
events = sorted(events, key=lambda x: x["elapsed"], reverse=True)
89+
90+
lines = format_columns(events, args.top)
91+
line_format = "%-{}s %-{}s %{}s".format(*get_max_column_lengths(lines))
92+
93+
# print top k filtered events
94+
print(line_format % ("Type", "Location", "Time"))
95+
for label, location, time_ms in lines:
96+
print(line_format % (label, location, time_ms))
97+
98+
99+
def generate_callstack(data, stack_path):
100+
101+
with open(stack_path, "w") as f:
102+
for event in data:
103+
f.write("%s %d\n" % (event["callstack"], event["elapsed"]))
104+
105+
print(
106+
"Successfully generated callstack at '{}'".format(os.path.abspath(stack_path))
107+
)
108+
print(
109+
"Run './flamgraph {} > flamegraph.svg' to generate the flamegraph".format(
110+
stack_path
111+
)
112+
)

0 commit comments

Comments
 (0)