@@ -86,6 +86,8 @@ def __call__(self, parser, namespace, values, option_string=None):
8686_PROCESS_KILL_TIMEOUT_SEC = 2.0
8787_READY_MESSAGE = b"ready"
8888_RECV_BUFFER_SIZE = 1024
89+ _BINARY_PROFILE_HEADER_SIZE = 64
90+ _BINARY_PROFILE_MAGICS = (b"HCAT" , b"TACH" )
8991
9092# Format configuration
9193FORMAT_EXTENSIONS = {
@@ -650,6 +652,25 @@ def _open_in_browser(path):
650652 print (f"Warning: Could not open browser: { e } " , file = sys .stderr )
651653
652654
655+ def _validate_replay_input_file (filename ):
656+ """Validate that the replay input looks like a sampling binary profile."""
657+ try :
658+ with open (filename , "rb" ) as file :
659+ header = file .read (_BINARY_PROFILE_HEADER_SIZE )
660+ except OSError as exc :
661+ sys .exit (f"Error: Could not read input file { filename } : { exc } " )
662+
663+ if (
664+ len (header ) < _BINARY_PROFILE_HEADER_SIZE
665+ or header [:4 ] not in _BINARY_PROFILE_MAGICS
666+ ):
667+ sys .exit (
668+ "Error: Input file is not a binary sampling profile. "
669+ "The replay command only accepts files created with --binary; "
670+ "the default --pstats output written by -o cannot be replayed."
671+ )
672+
673+
653674def _handle_output (collector , args , pid , mode ):
654675 """Handle output for the collector based on format and arguments.
655676
@@ -1201,47 +1222,72 @@ def _handle_replay(args):
12011222 if not os .path .exists (args .input_file ):
12021223 sys .exit (f"Error: Input file not found: { args .input_file } " )
12031224
1204- with BinaryReader (args .input_file ) as reader :
1205- info = reader .get_info ()
1206- interval = info ['sample_interval_us' ]
1225+ _validate_replay_input_file (args .input_file )
12071226
1208- print (f"Replaying { info ['sample_count' ]} samples from { args .input_file } " )
1209- print (f" Sample interval: { interval } us" )
1210- print (f" Compression: { 'zstd' if info .get ('compression_type' , 0 ) == 1 else 'none' } " )
1211-
1212- collector = _create_collector (
1213- args .format , interval , skip_idle = False ,
1214- diff_baseline = args .diff_baseline
1215- )
1216-
1217- def progress_callback (current , total ):
1218- if total > 0 :
1219- pct = current / total
1220- bar_width = 40
1221- filled = int (bar_width * pct )
1222- bar = '█' * filled + '░' * (bar_width - filled )
1223- print (f"\r [{ bar } ] { pct * 100 :5.1f} % ({ current :,} /{ total :,} )" , end = "" , flush = True )
1227+ try :
1228+ with BinaryReader (args .input_file ) as reader :
1229+ info = reader .get_info ()
1230+ interval = info ['sample_interval_us' ]
1231+
1232+ print (f"Replaying { info ['sample_count' ]} samples from { args .input_file } " )
1233+ print (f" Sample interval: { interval } us" )
1234+ print (
1235+ " Compression: "
1236+ f"{ 'zstd' if info .get ('compression_type' , 0 ) == 1 else 'none' } "
1237+ )
12241238
1225- count = reader .replay_samples (collector , progress_callback )
1226- print ()
1239+ collector = _create_collector (
1240+ args .format , interval , skip_idle = False ,
1241+ diff_baseline = args .diff_baseline
1242+ )
12271243
1228- if args .format == "pstats" :
1229- if args .outfile :
1230- collector .export (args .outfile )
1244+ def progress_callback (current , total ):
1245+ if total > 0 :
1246+ pct = current / total
1247+ bar_width = 40
1248+ filled = int (bar_width * pct )
1249+ bar = '█' * filled + '░' * (bar_width - filled )
1250+ print (
1251+ f"\r [{ bar } ] { pct * 100 :5.1f} % ({ current :,} /{ total :,} )" ,
1252+ end = "" ,
1253+ flush = True ,
1254+ )
1255+
1256+ count = reader .replay_samples (collector , progress_callback )
1257+ print ()
1258+
1259+ if args .format == "pstats" :
1260+ if args .outfile :
1261+ collector .export (args .outfile )
1262+ else :
1263+ sort_choice = (
1264+ args .sort if args .sort is not None else "nsamples"
1265+ )
1266+ limit = args .limit if args .limit is not None else 15
1267+ sort_mode = _sort_to_mode (sort_choice )
1268+ collector .print_stats (
1269+ sort_mode , limit , not args .no_summary ,
1270+ PROFILING_MODE_WALL
1271+ )
12311272 else :
1232- sort_choice = args .sort if args .sort is not None else "nsamples"
1233- limit = args .limit if args .limit is not None else 15
1234- sort_mode = _sort_to_mode (sort_choice )
1235- collector .print_stats (sort_mode , limit , not args .no_summary , PROFILING_MODE_WALL )
1236- else :
1237- filename = args .outfile or _generate_output_filename (args .format , os .getpid ())
1238- collector .export (filename )
1239-
1240- # Auto-open browser for HTML output if --browser flag is set
1241- if args .format in ('flamegraph' , 'diff_flamegraph' , 'heatmap' ) and getattr (args , 'browser' , False ):
1242- _open_in_browser (filename )
1273+ filename = (
1274+ args .outfile
1275+ or _generate_output_filename (args .format , os .getpid ())
1276+ )
1277+ collector .export (filename )
12431278
1244- print (f"Replayed { count } samples" )
1279+ # Auto-open browser for HTML output if --browser flag is set
1280+ if (
1281+ args .format in (
1282+ 'flamegraph' , 'diff_flamegraph' , 'heatmap'
1283+ )
1284+ and getattr (args , 'browser' , False )
1285+ ):
1286+ _open_in_browser (filename )
1287+
1288+ print (f"Replayed { count } samples" )
1289+ except (OSError , ValueError ) as exc :
1290+ sys .exit (f"Error: { exc } " )
12451291
12461292
12471293if __name__ == "__main__" :
0 commit comments