@@ -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+
128343def _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
148363def _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
0 commit comments