#!/usr/bin/perl use strict; use warnings; use JSON::PP; use Time::Local; my ($file, $cutoff_start_iso, $cutoff_end_iso) = @ARGV; # optional: only count entries in [start,end] sub iso_to_epoch { my ($s) = @_; return 0 unless defined $s; if ($s =~ /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.(\d+)Z/) { return timegm($6,$5,$4,$3,$2-1,$1) * 1000 + $7; } return 0; } my $cutoff_start = $cutoff_start_iso ? iso_to_epoch($cutoff_start_iso) : 0; my $cutoff_end = $cutoff_end_iso ? iso_to_epoch($cutoff_end_iso) : 0; open(my $fh, "<:encoding(UTF-8)", $file) or die $!; my $nAssistant=0; my %toolCounts; my %callById; # toolu_id -> {name,input,issued_ts} my @durs; my @errs; my $maxCtx=0; my $sumIn=0; my $sumOut=0; my $firstTs; my $lastTs; while (my $line = <$fh>) { chomp $line; next unless $line; my $obj = eval { decode_json($line) }; next unless $obj; my $ts = iso_to_epoch($obj->{timestamp}); next unless $ts; next if $cutoff_start && $ts < $cutoff_start; next if $cutoff_end && $ts > $cutoff_end; $firstTs = $ts if !defined($firstTs) || $ts < $firstTs; $lastTs = $ts if !defined($lastTs) || $ts > $lastTs; if ($obj->{type} eq 'assistant') { $nAssistant++; my $msg = $obj->{message}; my $usage = $msg->{usage}; if ($usage) { my $ctx = ($usage->{input_tokens}//0) + ($usage->{cache_read_input_tokens}//0) + ($usage->{cache_creation_input_tokens}//0); $maxCtx = $ctx if $ctx > $maxCtx; $sumIn += $ctx; $sumOut += ($usage->{output_tokens}//0); } for my $c (@{$msg->{content}}) { if ($c->{type} eq 'tool_use') { $toolCounts{$c->{name}}++; $callById{$c->{id}} = { name=>$c->{name}, input=>encode_json($c->{input}), issued=>$ts }; } } } elsif ($obj->{type} eq 'user') { my $msg = $obj->{message}; next unless ref($msg->{content}) eq 'ARRAY'; for my $c (@{$msg->{content}}) { next unless $c->{type} && $c->{type} eq 'tool_result'; my $tcid = $c->{tool_use_id}; my $call = $callById{$tcid}; next unless $call; my $dur = $ts - $call->{issued}; my $isErr = $c->{is_error} ? 1 : 0; my $text = ref($c->{content}) ? encode_json($c->{content}) : ($c->{content}//''); push @durs, { name=>$call->{name}, input=>$call->{input}, dur=>$dur, isError=>$isErr, err=>substr($text,0,200) }; push @errs, $durs[-1] if $isErr; } } } @durs = sort { $b->{dur} <=> $a->{dur} } @durs; print "=== $file ===\n"; print "cutoff: $cutoff_start_iso .. ", ($cutoff_end_iso//''), "\n" if $cutoff_start_iso; print "assistant turns: $nAssistant\n"; print "tool calls total: ", scalar(keys %callById), "\n"; print "tool calls by type: "; for my $k (sort keys %toolCounts) { print "$k=$toolCounts{$k} "; } print "\n"; print "max context (in+cache_read+cache_creation) single call: $maxCtx sum_in(approx): $sumIn sum_out: $sumOut\n"; print "span: ", (($lastTs-$firstTs)//0)/1000, "s\n"; my $toolTimeSum = 0; $toolTimeSum += $_->{dur} for @durs; print "total tool time (sum all calls): ", $toolTimeSum/1000, "s\n"; print "--- top 10 longest tool calls ---\n"; for my $i (0..9) { last unless $durs[$i]; my $d = $durs[$i]; my $a = $d->{input}; $a =~ s/\s+/ /g; $a = substr($a,0,100); printf("%7.1fs %-10s %s %s\n", $d->{dur}/1000, $d->{name}, $a, ($d->{isError}?"ERROR":"")); } print "--- errors (", scalar(@errs), ") ---\n"; for my $d (@errs) { my $a = $d->{input}; $a =~ s/\s+/ /g; $a = substr($a,0,100); my $e = $d->{err}; $e =~ s/\s+/ /g; print "$d->{name} $a :: $e\n"; } # sum total tool time (appended quick calc, reuse @durs if still in scope - not, so separate script)