Skip to content

Commit 734f8a4

Browse files
committed
Merge branch 'master' of https://github.com/plasma-umass/coz
2 parents ab8afc4 + 0702e23 commit 734f8a4

File tree

2 files changed

+232
-2
lines changed

2 files changed

+232
-2
lines changed

coz

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,221 @@ def open_browser(url):
125125
import webbrowser
126126
webbrowser.open_new_tab(url)
127127

128+
def parse_profile(profile_path):
129+
"""Parse .coz profile and return aggregated data and metadata."""
130+
# Data structure: {selected_line: {progress_point: {speedup: {'delta': n, 'duration': n}}}}
131+
data = {}
132+
experiment_count = 0
133+
runtime = 0
134+
samples = {}
135+
136+
with open(profile_path, 'r') as f:
137+
experiment = None
138+
for line in f:
139+
line = line.strip()
140+
if not line:
141+
continue
142+
parts = line.split('\t')
143+
record_type = parts[0]
144+
fields = {}
145+
for part in parts[1:]:
146+
if '=' in part:
147+
k, v = part.split('=', 1)
148+
fields[k] = v
149+
150+
if record_type == 'experiment':
151+
experiment = {
152+
'selected': fields.get('selected', ''),
153+
'speedup': float(fields.get('speedup', 0)),
154+
'duration': int(fields.get('duration', 0))
155+
}
156+
experiment_count += 1
157+
elif record_type in ('throughput-point', 'progress-point'):
158+
if experiment:
159+
selected = experiment['selected']
160+
speedup = experiment['speedup']
161+
duration = experiment['duration']
162+
pp_name = fields.get('name', '')
163+
delta = int(fields.get('delta', 0))
164+
165+
if selected not in data:
166+
data[selected] = {}
167+
if pp_name not in data[selected]:
168+
data[selected][pp_name] = {}
169+
if speedup not in data[selected][pp_name]:
170+
data[selected][pp_name][speedup] = {'delta': 0, 'duration': 0}
171+
172+
data[selected][pp_name][speedup]['delta'] += delta
173+
data[selected][pp_name][speedup]['duration'] += duration
174+
elif record_type == 'runtime':
175+
runtime = int(fields.get('time', 0))
176+
elif record_type == 'samples':
177+
loc = fields.get('location', '')
178+
count = int(fields.get('count', 0))
179+
samples[loc] = samples.get(loc, 0) + count
180+
181+
return data, experiment_count, runtime, samples
182+
183+
def calculate_speedups(data, min_points=1):
184+
"""Calculate program speedup for each source line."""
185+
results = []
186+
for selected, progress_points in data.items():
187+
for pp_name, speedups in progress_points.items():
188+
if 0.0 not in speedups:
189+
continue # Need baseline
190+
baseline_entry = speedups[0.0]
191+
if baseline_entry['delta'] == 0:
192+
continue
193+
baseline = baseline_entry['duration'] / baseline_entry['delta']
194+
195+
measurements = []
196+
for speedup, agg in sorted(speedups.items()):
197+
if agg['delta'] == 0:
198+
continue
199+
data_point = agg['duration'] / agg['delta']
200+
progress_speedup = (baseline - data_point) / baseline
201+
measurements.append((speedup, progress_speedup))
202+
203+
if len(measurements) >= min_points:
204+
# Calculate max speedup
205+
max_speedup = max(m[1] for m in measurements) if measurements else 0
206+
results.append({
207+
'line': selected,
208+
'progress_point': pp_name,
209+
'measurements': measurements,
210+
'max_speedup': max_speedup,
211+
'num_points': len(measurements)
212+
})
213+
214+
# Sort by max speedup (highest first)
215+
results.sort(key=lambda x: x['max_speedup'], reverse=True)
216+
return results
217+
218+
def print_text_summary(profile_path, results, experiment_count, runtime, samples):
219+
"""Print summary table of profiling results."""
220+
print(f"Profile: {profile_path}")
221+
runtime_sec = runtime / 1e9 if runtime > 0 else 0
222+
print(f"Experiments: {experiment_count} | Runtime: {runtime_sec:.1f}s")
223+
print()
224+
225+
if not results:
226+
print("No profiling results found.")
227+
print("Make sure you specified a progress point and ran your program long enough.")
228+
return
229+
230+
# Find max line width for formatting
231+
max_line_len = max(len(r['line']) for r in results)
232+
max_line_len = max(max_line_len, 11) # "Source Line" header
233+
234+
# Print header
235+
header = f"{'Source Line':<{max_line_len}} | Max Speedup | Points"
236+
print(header)
237+
print('-' * max_line_len + '-+-------------+-------')
238+
239+
# Print each result
240+
for r in results:
241+
speedup_pct = r['max_speedup'] * 100
242+
sign = '+' if speedup_pct >= 0 else ''
243+
print(f"{r['line']:<{max_line_len}} | {sign}{speedup_pct:>9.1f}% | {r['num_points']:>5}")
244+
245+
def print_scatter_plot(result):
246+
"""Print an ASCII scatter plot for a single result."""
247+
line = result['line']
248+
pp = result['progress_point']
249+
measurements = result['measurements']
250+
251+
print()
252+
print(f"=== {line} -> {pp} ===")
253+
print()
254+
255+
if not measurements:
256+
print(" No data points")
257+
return
258+
259+
# Filter out extreme outliers (keep values in reasonable range -100% to +200%)
260+
filtered = [(x, y) for x, y in measurements if -1.0 <= y <= 2.0]
261+
if not filtered:
262+
filtered = measurements # Fall back to all data if all are outliers
263+
264+
# Find ranges
265+
min_speedup = min(m[1] for m in filtered)
266+
max_speedup = max(m[1] for m in filtered)
267+
268+
# Expand range slightly for display
269+
if max_speedup == min_speedup:
270+
max_speedup = min_speedup + 0.1
271+
272+
# Plot dimensions
273+
width = 60
274+
height = 15
275+
276+
# Y-axis range: from min(0, min_speedup) to max_speedup
277+
y_min = min(0, min_speedup)
278+
y_max = max(max_speedup, 0.01)
279+
y_range = y_max - y_min
280+
281+
# Create plot grid
282+
grid = [[' ' for _ in range(width)] for _ in range(height)]
283+
284+
# Plot points (including outliers, clamped to grid)
285+
for line_speedup, prog_speedup in measurements:
286+
x = int(line_speedup * (width - 1))
287+
x = max(0, min(width - 1, x))
288+
# Clamp y to the visible range
289+
clamped_speedup = max(y_min, min(y_max, prog_speedup))
290+
y = int((clamped_speedup - y_min) / y_range * (height - 1))
291+
y = max(0, min(height - 1, y))
292+
y = height - 1 - y # Flip Y axis
293+
grid[y][x] = '*'
294+
295+
# Find zero line position
296+
zero_y = int((0 - y_min) / y_range * (height - 1))
297+
zero_y = height - 1 - zero_y
298+
zero_y = max(0, min(height - 1, zero_y))
299+
300+
# Print plot
301+
print("Program")
302+
print("Speedup")
303+
for i, row in enumerate(grid):
304+
# Y-axis label
305+
y_val = y_max - (i / (height - 1)) * y_range
306+
label = f"{y_val*100:>6.0f}% |"
307+
line_str = ''.join(row)
308+
# Add zero line marker
309+
if i == zero_y:
310+
line_str = line_str.replace(' ', '-')
311+
print(f"{label}{line_str}")
312+
313+
# X-axis
314+
print(" +" + "-" * width)
315+
print(" 0% 20% 40% 60% 80% 100%")
316+
print(" Line Speedup")
317+
318+
def _coz_plot_text(args):
319+
"""Handle text-based profile output."""
320+
profile_path = abspath(args.input) if args.input else None
321+
if profile_path is None:
322+
default_profile = abspath(curdir + path_sep + 'profile.coz')
323+
if os.path.exists(default_profile):
324+
profile_path = default_profile
325+
326+
if not profile_path or not os.path.exists(profile_path):
327+
sys.stderr.write('error: no profile found. Specify with -i or run from directory with profile.coz\n')
328+
sys.exit(1)
329+
330+
data, experiment_count, runtime, samples = parse_profile(profile_path)
331+
results = calculate_speedups(data)
332+
333+
print_text_summary(profile_path, results, experiment_count, runtime, samples)
334+
335+
if args.verbose and results:
336+
print()
337+
print("=" * 70)
338+
print("DETAILED SCATTER PLOTS")
339+
print("=" * 70)
340+
for r in results:
341+
print_scatter_plot(r)
342+
128343
def _find_viewer_directory():
129344
"""Find the viewer directory relative to this script's location."""
130345
coz_prefix = dirname(realpath(sys.argv[0]))
@@ -146,6 +361,11 @@ def _find_viewer_directory():
146361
return None
147362

148363
def _coz_plot(args):
364+
# Handle text-based output mode
365+
if args.text:
366+
_coz_plot_text(args)
367+
return
368+
149369
import http.server
150370
import socketserver
151371
import threading
@@ -293,6 +513,14 @@ _plot_parser.add_argument('--port', '-p',
293513
type=int, default=8080,
294514
help='Port for the local web server (default=8080)')
295515

516+
_plot_parser.add_argument('--text', '-t',
517+
action='store_true', default=False,
518+
help='Output text-based graphs instead of web viewer')
519+
520+
_plot_parser.add_argument('--verbose', '-v',
521+
action='store_true', default=False,
522+
help='Show detailed scatter plots for each source line (with --text)')
523+
296524
# Use defaults to recover handler function and parser object from parser output
297525
_plot_parser.set_defaults(func=_coz_plot, parser=_plot_parser)
298526

libcoz/macho_support.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,10 @@ bool get_section_type(const char* sectname, dwarf::section_type* out) {
335335
else if(std::strcmp(suffix, "pubtypes") == 0) *out = dwarf::section_type::pubtypes;
336336
else if(std::strcmp(suffix, "ranges") == 0) *out = dwarf::section_type::ranges;
337337
else if(std::strcmp(suffix, "str") == 0) *out = dwarf::section_type::str;
338-
// Note: str_offsets section is used in DWARF 5 but not all libelfin versions support it
339-
// Skip it for now - the essential sections for line info are present
338+
// DWARF 5 str_offsets section - handle both full name and Mach-O truncated name (16-char limit)
339+
else if(std::strcmp(suffix, "str_offsets") == 0 ||
340+
std::strcmp(suffix, "str_offs") == 0)
341+
*out = dwarf::section_type::str_offsets;
340342
else if(std::strcmp(suffix, "types") == 0) *out = dwarf::section_type::types;
341343
else if(assign_line_str(out,
342344
suffix,

0 commit comments

Comments
 (0)