@@ -151,6 +151,17 @@ def open_browser(url):
151151 import webbrowser
152152 webbrowser .open_new_tab (url )
153153
154+ def _can_open_browser ():
155+ """Check if we're in an environment where a browser can be opened."""
156+ # SSH session — no local display
157+ if os .environ .get ('SSH_TTY' ) or os .environ .get ('SSH_CONNECTION' ):
158+ return False
159+ # Linux: need X11 or Wayland
160+ if sys .platform == 'linux' :
161+ if not os .environ .get ('DISPLAY' ) and not os .environ .get ('WAYLAND_DISPLAY' ):
162+ return False
163+ return True
164+
154165def parse_profile (profile_path , include_raw = False ):
155166 """Parse .coz or .jsonl profile and return aggregated data and metadata."""
156167 import json
@@ -184,8 +195,12 @@ def parse_profile(profile_path, include_raw=False):
184195 record_type = record .get ('type' , '' )
185196
186197 if record_type == 'experiment' :
198+ selected_line = record .get ('selected' , '' )
199+ if '/coz.h:' in selected_line :
200+ experiment = None
201+ continue
187202 experiment = {
188- 'selected' : record . get ( 'selected' , '' ) ,
203+ 'selected' : selected_line ,
189204 'speedup' : float (record .get ('speedup' , 0 )),
190205 'duration' : int (record .get ('duration' , 0 )),
191206 'selected_samples' : int (record .get ('selected_samples' , 0 ))
@@ -223,8 +238,9 @@ def parse_profile(profile_path, include_raw=False):
223238 runtime = int (record .get ('time' , 0 ))
224239 elif record_type == 'samples' :
225240 loc = record .get ('location' , '' )
226- count = int (record .get ('count' , 0 ))
227- samples [loc ] = samples .get (loc , 0 ) + count
241+ if '/coz.h:' not in loc :
242+ count = int (record .get ('count' , 0 ))
243+ samples [loc ] = samples .get (loc , 0 ) + count
228244 else :
229245 # Legacy tab-separated format
230246 parts = line .split ('\t ' )
@@ -236,8 +252,12 @@ def parse_profile(profile_path, include_raw=False):
236252 fields [k ] = v
237253
238254 if record_type == 'experiment' :
255+ selected_line = fields .get ('selected' , '' )
256+ if '/coz.h:' in selected_line :
257+ experiment = None
258+ continue
239259 experiment = {
240- 'selected' : fields . get ( 'selected' , '' ) ,
260+ 'selected' : selected_line ,
241261 'speedup' : float (fields .get ('speedup' , 0 )),
242262 'duration' : int (fields .get ('duration' , 0 )),
243263 'selected_samples' : int (fields .get ('selected-samples' , 0 ))
@@ -275,8 +295,9 @@ def parse_profile(profile_path, include_raw=False):
275295 runtime = int (fields .get ('time' , 0 ))
276296 elif record_type == 'samples' :
277297 loc = fields .get ('location' , '' )
278- count = int (fields .get ('count' , 0 ))
279- samples [loc ] = samples .get (loc , 0 ) + count
298+ if '/coz.h:' not in loc :
299+ count = int (fields .get ('count' , 0 ))
300+ samples [loc ] = samples .get (loc , 0 ) + count
280301
281302 return data , experiment_count , runtime , samples , raw_experiments
282303
@@ -615,8 +636,7 @@ def _coz_plot(args):
615636 profile_path = default_profile
616637 break
617638
618- # Find an available port
619- port = args .port
639+ profile_basename = os .path .basename (profile_path ) if profile_path else None
620640
621641 class CozHandler (http .server .SimpleHTTPRequestHandler ):
622642 protocol_version = 'HTTP/1.1'
@@ -867,11 +887,14 @@ def _coz_plot(args):
867887 self .wfile .write (content )
868888 return
869889
870- # Serve profile.coz from current directory when requested
871- if self .path == '/current-profile.coz' and profile_path :
890+ # Serve the profile file when requested by its basename
891+ if profile_basename and self .path == '/' + profile_basename and profile_path :
872892 try :
873- with open (profile_path , 'rb' ) as f :
874- content = f .read ()
893+ with open (profile_path , 'r' ) as f :
894+ lines = f .readlines ()
895+ # Filter out coz.h self-instrumentation from old profiles
896+ filtered = [l for l in lines if '/coz.h:' not in l ]
897+ content = '' .join (filtered ).encode ('utf-8' )
875898 self .send_response (200 )
876899 self .send_header ('Content-Type' , 'text/plain' )
877900 self .send_header ('Content-Length' , len (content ))
@@ -1191,30 +1214,74 @@ Based on the causal profiling data, suggest specific optimizations for the targe
11911214 allow_reuse_address = True
11921215 daemon_threads = True
11931216
1194- try :
1195- with ThreadedHTTPServer (("" , port ), CozHandler ) as httpd :
1196- # Build URL with query parameter if profile exists
1197- if profile_path :
1198- url = f'http://localhost:{ port } /?load=current-profile.coz'
1199- print (f'Loading profile: { profile_path } ' )
1200- else :
1201- url = f'http://localhost:{ port } /'
1202- print (f'Serving coz viewer at http://localhost:{ port } /' )
1203- print (f'Press Ctrl+C to stop the server' )
1217+ # Find an available port with retry
1218+ import random
1219+ import signal
1220+ preferred = args .port
1221+ candidates = list (range (preferred , preferred + 10 ))
1222+ candidates += [random .randint (49152 , 65535 ) for _ in range (5 )]
1223+ httpd = None
1224+ port = None
1225+ for candidate in candidates :
1226+ try :
1227+ httpd = ThreadedHTTPServer (("" , candidate ), CozHandler )
1228+ port = candidate
1229+ break
1230+ except OSError as e :
1231+ if e .errno in (48 , 98 ): # EADDRINUSE (macOS / Linux)
1232+ continue
1233+ raise
1234+ if httpd is None :
1235+ sys .stderr .write ('error: could not find an available port (tried %d-%d and random high ports)\n '
1236+ % (preferred , preferred + 9 ))
1237+ sys .exit (1 )
12041238
1205- # Open browser in a separate thread
1239+ with httpd :
1240+ # Build URL with query parameter if profile exists
1241+ if profile_path :
1242+ url = f'http://localhost:{ port } /?load={ profile_basename } '
1243+ print (f'Loading profile: { profile_path } ' )
1244+ else :
1245+ url = f'http://localhost:{ port } /'
1246+ if port != preferred :
1247+ print (f'Port { preferred } was busy, using { port } instead' )
1248+ print (f'Serving coz viewer at { url } ' )
1249+ print ('Press Ctrl+C to stop the server' )
1250+
1251+ # Open browser unless suppressed or headless
1252+ if not args .no_browser and _can_open_browser ():
12061253 t1 = threading .Thread (target = open_browser , args = (url ,))
12071254 t1 .start ()
1255+ elif not _can_open_browser ():
1256+ print ('No display detected. Connect from your browser or use SSH port forwarding:' )
1257+ print (f' ssh -L { port } :localhost:{ port } user@host' )
1258+
1259+ # Self-pipe trick via set_wakeup_fd: Python's C signal handler
1260+ # writes a byte to the pipe, which wakes select() immediately —
1261+ # no dependence on EINTR, kqueue, or Python-level handlers.
1262+ import select as _select
1263+ sig_r , sig_w = os .pipe ()
1264+ os .set_blocking (sig_w , False )
1265+ signal .set_wakeup_fd (sig_w )
1266+ signal .signal (signal .SIGINT , lambda s , f : None )
1267+ signal .signal (signal .SIGTERM , lambda s , f : None )
12081268
1209- try :
1210- httpd .serve_forever ()
1211- except KeyboardInterrupt :
1212- print ('\n Shutting down server...' )
1213- except OSError as e :
1214- if e .errno == 48 or e .errno == 98 : # Address already in use (macOS/Linux)
1215- sys .stderr .write (f'error: port { port } is already in use. Try --port <number>\n ' )
1216- sys .exit (1 )
1217- raise
1269+ try :
1270+ while True :
1271+ ready , _ , _ = _select .select ([httpd .socket , sig_r ], [], [], 1.0 )
1272+ for fd in ready :
1273+ if fd == sig_r :
1274+ os .read (sig_r , 256 )
1275+ raise StopIteration
1276+ else :
1277+ httpd ._handle_request_noblock ()
1278+ except StopIteration :
1279+ pass
1280+ finally :
1281+ signal .set_wakeup_fd (- 1 )
1282+ os .close (sig_r )
1283+ os .close (sig_w )
1284+ print ('\n Shutting down server...' )
12181285
12191286
12201287# Special format handler for line reference arguments
@@ -1303,6 +1370,10 @@ _plot_parser.add_argument('--verbose', '-v',
13031370 action = 'store_true' , default = False ,
13041371 help = 'Show detailed scatter plots for each source line (with --text)' )
13051372
1373+ _plot_parser .add_argument ('--no-browser' ,
1374+ action = 'store_true' , default = False ,
1375+ help = 'Start server without opening a browser' )
1376+
13061377_plot_parser .add_argument ('--json' , '-j' ,
13071378 metavar = '<output.json>' ,
13081379 default = None ,
0 commit comments