95 lines
3.5 KiB
Perl
95 lines
3.5 KiB
Perl
#!/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)
|