@@ -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,88 @@ 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+ )
671+
672+
673+ def _replay_with_reader (args , reader ):
674+ """Replay samples from an open binary reader."""
675+ info = reader .get_info ()
676+ interval = info ['sample_interval_us' ]
677+
678+ print (f"Replaying { info ['sample_count' ]} samples from { args .input_file } " )
679+ print (f" Sample interval: { interval } us" )
680+ print (
681+ " Compression: "
682+ f"{ 'zstd' if info .get ('compression_type' , 0 ) == 1 else 'none' } "
683+ )
684+
685+ collector = _create_collector (
686+ args .format , interval , skip_idle = False ,
687+ diff_baseline = args .diff_baseline
688+ )
689+
690+ def progress_callback (current , total ):
691+ if total > 0 :
692+ pct = current / total
693+ bar_width = 40
694+ filled = int (bar_width * pct )
695+ bar = '█' * filled + '░' * (bar_width - filled )
696+ print (
697+ f"\r [{ bar } ] { pct * 100 :5.1f} % ({ current :,} /{ total :,} )" ,
698+ end = "" ,
699+ flush = True ,
700+ )
701+
702+ count = reader .replay_samples (collector , progress_callback )
703+ print ()
704+
705+ if args .format == "pstats" :
706+ if args .outfile :
707+ collector .export (args .outfile )
708+ else :
709+ sort_choice = (
710+ args .sort if args .sort is not None else "nsamples"
711+ )
712+ limit = args .limit if args .limit is not None else 15
713+ sort_mode = _sort_to_mode (sort_choice )
714+ collector .print_stats (
715+ sort_mode , limit , not args .no_summary ,
716+ PROFILING_MODE_WALL
717+ )
718+ else :
719+ filename = (
720+ args .outfile
721+ or _generate_output_filename (args .format , os .getpid ())
722+ )
723+ collector .export (filename )
724+
725+ # Auto-open browser for HTML output if --browser flag is set
726+ if (
727+ args .format in (
728+ 'flamegraph' , 'diff_flamegraph' , 'heatmap'
729+ )
730+ and getattr (args , 'browser' , False )
731+ ):
732+ _open_in_browser (filename )
733+
734+ print (f"Replayed { count } samples" )
735+
736+
653737def _handle_output (collector , args , pid , mode ):
654738 """Handle output for the collector based on format and arguments.
655739
@@ -1201,47 +1285,13 @@ def _handle_replay(args):
12011285 if not os .path .exists (args .input_file ):
12021286 sys .exit (f"Error: Input file not found: { args .input_file } " )
12031287
1204- with BinaryReader (args .input_file ) as reader :
1205- info = reader .get_info ()
1206- interval = info ['sample_interval_us' ]
1288+ _validate_replay_input_file (args .input_file )
12071289
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 )
1224-
1225- count = reader .replay_samples (collector , progress_callback )
1226- print ()
1227-
1228- if args .format == "pstats" :
1229- if args .outfile :
1230- collector .export (args .outfile )
1231- 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 )
1243-
1244- print (f"Replayed { count } samples" )
1290+ try :
1291+ with BinaryReader (args .input_file ) as reader :
1292+ _replay_with_reader (args , reader )
1293+ except (OSError , ValueError ) as exc :
1294+ sys .exit (f"Error: { exc } " )
12451295
12461296
12471297if __name__ == "__main__" :
0 commit comments