Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions test_scripts/remeasure.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Re-run a single benchmark command and return @{ Time = <seconds>; Mem = <MB> }.
#
# Used by verify_memory.ps1 / verify_runtime.ps1 to retry a command that breached
# its threshold. CI runner noise is one-sided - a loaded machine can only make a
# command slower or fatter - so a breach that does not reproduce was noise, while
# a genuine regression reproduces every time.
#
# Measurement mirrors Measure-IQTree in test_iqtree.ps1 (poll WorkingSet64) so the
# retry is comparable with the original reading. The suite is stateful, so only
# this command's OWN outputs are cleared; IQ-TREE otherwise refuses to rerun
# ("previous run successfully finished").
function Measure-Once {
param ([string]$CommandLine)

$prefix = $null
if ($CommandLine -match '--prefix\s+(\S+)') { $prefix = $Matches[1] }
if ($prefix) {
Get-ChildItem -Path "$prefix.*" -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
}

$exe, $cmdArgs = $CommandLine -split '\s+', 2
$tempOut = [System.IO.Path]::GetTempFileName()
$startTime = Get-Date

$proc = Start-Process -FilePath $exe -ArgumentList $cmdArgs `
-RedirectStandardOutput $tempOut -NoNewWindow -PassThru

# Poll often: a command finishing inside one sleep interval would otherwise
# never be sampled and report 0 MB.
$peakMemory = 0
while (-not $proc.HasExited) {
try {
$currentMem = (Get-Process -Id $proc.Id -ErrorAction Stop).WorkingSet64 / 1MB
if ($currentMem -gt $peakMemory) { $peakMemory = $currentMem }
} catch { break }
Start-Sleep -Milliseconds 25
}
$proc.WaitForExit()

# The OS-recorded peak is authoritative and survives a process too short-lived
# to sample; fall back to the polled maximum where it is unavailable.
try {
$proc.Refresh()
$osPeak = $proc.PeakWorkingSet64 / 1MB
if ($osPeak -gt $peakMemory) { $peakMemory = $osPeak }
} catch { }

$elapsed = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2)
Remove-Item $tempOut -ErrorAction SilentlyContinue

# Ok distinguishes "the command failed" from "it legitimately used very little".
return @{ Time = $elapsed; Mem = [math]::Round($peakMemory, 2); Ok = ($proc.ExitCode -eq 0) }
}
32 changes: 32 additions & 0 deletions test_scripts/remeasure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
# Re-run a single benchmark command and echo "<seconds> <peak MB>".
#
# Used by verify_memory.sh / verify_runtime.sh to retry a command that breached
# its threshold. CI runner noise is one-sided - a loaded machine can only make a
# command slower or fatter - so a breach that does not reproduce was noise, while
# a genuine regression reproduces every time.
#
# The suite is stateful, so the command's OWN outputs are cleared first; IQ-TREE
# otherwise refuses to rerun ("previous run successfully finished"). Outputs of
# earlier commands are left alone because later commands still need them.
remeasure() {
local CMD="$1"
local PREFIX REAL MEM_MB MEM_KB PEAK_MEM tmp
PREFIX=$(echo "$CMD" | sed -n 's/.*--prefix \([^ ]*\).*/\1/p')
[ -n "$PREFIX" ] && rm -f "${PREFIX}".*
tmp=$(mktemp)
if [[ "$(uname)" == "Darwin" ]]; then
/usr/bin/time -l -o "$tmp" $CMD > /dev/null 2>&1
local rc=$?
REAL=$(awk '/real/{print $1; exit}' "$tmp")
PEAK_MEM=$(awk '/peak memory footprint/{print $1; exit}' "$tmp")
MEM_MB=$(awk "BEGIN {printf \"%.2f\", ${PEAK_MEM:-0} / (1024 * 1024)}")
else
/usr/bin/time -o "$tmp" -f "%e %U %S %M" $CMD > /dev/null 2>&1
local rc=$?
read -r REAL _ _ MEM_KB < "$tmp"
MEM_MB=$(awk "BEGIN {printf \"%.2f\", ${MEM_KB:-0} / 1024}")
fi
rm -f "$tmp"
if [ "$rc" -ne 0 ]; then echo "0 0"; else echo "${REAL:-0} ${MEM_MB:-0}"; fi
}
13 changes: 9 additions & 4 deletions test_scripts/test_iqtree.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@ Measure-IQTree "$IQTreeBin -s $WD/turtle.fa -m `"MIX+MF`" --prefix $OutDir/turtl
Measure-IQTree "$IQTreeBin -s $WD/turtle.fa -p $WD/turtle.nex -o phrynops -m GTR+G --prefix $OutDir/turtle.nex.outgroup -T 1 -seed $SEED"
Measure-IQTree "$IQTreeBin -s $WD/turtle.fa -lmap 100 -o phrynops -m GTR+G --prefix $OutDir/turtle.lmap.outgroup -T 1 -seed $SEED"

# user-defined codon frequencies must be honoured by GY-type models (issue #192)
$CodonFreq = (Get-Content $WD/codon_freq.txt -Raw).Trim()
Measure-IQTree "$IQTreeBin -s $WD/codon.fa -st CODON -te $WD/codon.tree -blfix -m `"GY{0.8,1.07}+FU{$CodonFreq}`" --prefix $OutDir/codon.gy.fu -T 1 -seed $SEED"
Measure-IQTree "$IQTreeBin -s $WD/codon.fa -st CODON -te $WD/codon.tree -blfix -m `"GY{0.8,1.07}+F3X4`" --prefix $OutDir/codon.gy.f3x4 -T 1 -seed $SEED"

## amino acid test cases
Write-Host "Running amino acid test cases..."
Expand Down Expand Up @@ -133,3 +129,12 @@ Measure-IQTree "$IQTreeBin -s $WD/turtle_aa.fasta -p $WD/turtle_aa.nex -g $WD/tu
Measure-IQTree "$IQTreeBin -s $WD/turtle_aa.fasta -p $WD/turtle_aa.nex -g $WD/turtle.constr.tree2 -B 1000 -alrt 1000 --prefix $OutDir/turtle_aa.nex.constr2 -T 1 -seed $SEED"

Measure-IQTree "$IQTreeBin -s $WD/turtle_aa.fasta -m MUTSEL -ft AUTO --prefix $OutDir/turtle_aa.mutsel -T 1 -seed $SEED"

# Kept at the END of the suite on purpose: verify_memory/verify_runtime join the
# threshold table to the log POSITIONALLY, so a command inserted mid-list shifts
# every later row onto the wrong threshold. These two have no table rows yet, so
# here they are simply skipped with a warning instead of corrupting the rest.
# user-defined codon frequencies must be honoured by GY-type models (issue #192)
$CodonFreq = (Get-Content $WD/codon_freq.txt -Raw).Trim()
Measure-IQTree "$IQTreeBin -s $WD/codon.fa -st CODON -te $WD/codon.tree -blfix -m `"GY{0.8,1.07}+FU{$CodonFreq}`" --prefix $OutDir/codon.gy.fu -T 1 -seed $SEED"
Measure-IQTree "$IQTreeBin -s $WD/codon.fa -st CODON -te $WD/codon.tree -blfix -m `"GY{0.8,1.07}+F3X4`" --prefix $OutDir/codon.gy.f3x4 -T 1 -seed $SEED"
15 changes: 9 additions & 6 deletions test_scripts/test_iqtree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ run_timed ${IQTREE_BIN} -s ${WD}/turtle.fa -p ${WD}/turtle.nex -o phrynops -m GT

run_timed ${IQTREE_BIN} -s ${WD}/turtle.fa -lmap 100 -o phrynops -m GTR+G --prefix ${OUT_DIR}/turtle.lmap.outgroup -T 1 -seed $SEED

# user-defined codon frequencies must be honoured by GY-type models (issue #192)

run_timed ${IQTREE_BIN} -s ${WD}/codon.fa -st CODON -te ${WD}/codon.tree -blfix -m "GY{0.8,1.07}+FU{$(cat ${WD}/codon_freq.txt)}" --prefix ${OUT_DIR}/codon.gy.fu -T 1 -seed $SEED

run_timed ${IQTREE_BIN} -s ${WD}/codon.fa -st CODON -te ${WD}/codon.tree -blfix -m "GY{0.8,1.07}+F3X4" --prefix ${OUT_DIR}/codon.gy.f3x4 -T 1 -seed $SEED


## amino acid test cases
Expand Down Expand Up @@ -131,4 +126,12 @@ run_timed ${IQTREE_BIN} -s $AA_FASTA -p $AA_NEX -g ${WD}/turtle.constr.tree --pr

run_timed ${IQTREE_BIN} -s $AA_FASTA -p $AA_NEX -g ${WD}/turtle.constr.tree2 -B 1000 -alrt 1000 --prefix ${OUT_DIR}/turtle_aa.nex.constr2 -T 1 -seed $SEED

run_timed ${IQTREE_BIN} -s $AA_FASTA -m "MUTSEL" -ft AUTO --prefix ${OUT_DIR}/turtle_aa.mutsel -T 1 -seed $SEED
run_timed ${IQTREE_BIN} -s $AA_FASTA -m "MUTSEL" -ft AUTO --prefix ${OUT_DIR}/turtle_aa.mutsel -T 1 -seed $SEED

# Kept at the END of the suite on purpose: verify_memory/verify_runtime join the
# threshold table to the log POSITIONALLY, so a command inserted mid-list shifts
# every later row onto the wrong threshold. These two have no table rows yet, so
# here they are simply skipped with a warning instead of corrupting the rest.
# user-defined codon frequencies must be honoured by GY-type models (issue #192)
run_timed ${IQTREE_BIN} -s ${WD}/codon.fa -st CODON -te ${WD}/codon.tree -blfix -m "GY{0.8,1.07}+FU{$(cat ${WD}/codon_freq.txt)}" --prefix ${OUT_DIR}/codon.gy.fu -T 1 -seed $SEED
run_timed ${IQTREE_BIN} -s ${WD}/codon.fa -st CODON -te ${WD}/codon.tree -blfix -m "GY{0.8,1.07}+F3X4" --prefix ${OUT_DIR}/codon.gy.f3x4 -T 1 -seed $SEED
43 changes: 42 additions & 1 deletion test_scripts/verify_memory.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ if ($FallbackColumn -ne "") {
$thrIdx = $hdr.IndexOf("thr-$FallbackColumn")
if ($thrIdx -ge 0) {
Write-Host "Using per-platform thresholds: thr-$FallbackColumn"
# NB: $nLog is not known yet here - bound the loop by the table itself.
for ($i = 0; $i -lt $thresholds.Count; $i++) {
$thresholds[$i].Threshold = [double]($thresholdLines[$i] -split "`t")[$thrIdx]
}
Expand Down Expand Up @@ -57,9 +58,32 @@ $iqtree2Mem = foreach ($line in $iqtree2Lines) { [double]($line -split "`t")[2]
$iqtree3Lines = Get-Content $IQTree3Log | Select-Object -Skip 1
$iqtree3Mem = foreach ($line in $iqtree3Lines) { [double]($line -split "`t")[2] }

# Column 0 is the command actually executed, kept so a breaching check can be retried.
$iqtree2Cmd = foreach ($line in $iqtree2Lines) { ($line -split "`t")[0] }
$iqtree3Cmd = foreach ($line in $iqtree3Lines) { ($line -split "`t")[0] }
. (Join-Path $PSScriptRoot "remeasure.ps1")

# Reconcile the number of benchmark commands with the number of table rows.
# They are joined POSITIONALLY, so a mismatch means the pairing is wrong.
$nRows = $thresholds.Count
$nLog = $iqtree3Mem.Count
if ($nLog -ne $nRows) {
Write-Host "WARNING: the suite ran $nLog commands but memory has $nRows threshold rows."
if ($nLog -gt $nRows) {
Write-Host " Skipping the last $($nLog - $nRows) command(s) - they have no threshold:"
$iqtree3Cmd[$nRows..($nLog - 1)] | ForEach-Object { Write-Host " $_" }
} else {
Write-Host " Ignoring the last $($nRows - $nLog) threshold row(s) - no command produced them."
$thresholds = $thresholds[0..($nLog - 1)]
}
Write-Host " NOTE: rows are matched by POSITION. If the extra command(s) were added in the"
Write-Host " middle rather than at the end, every later row is now compared against the"
Write-Host " wrong command. Add the missing row(s) to keep the table in step."
}

$failCount = 0

for ($i = 0; $i -lt $thresholds.Count; $i++) {
for ($i = 0; $i -lt [Math]::Min($thresholds.Count, $nLog); $i++) {
$command = $thresholds[$i].Command
$threshold = $thresholds[$i].Threshold
$expected = $iqtree2Mem[$i]
Expand All @@ -78,6 +102,23 @@ for ($i = 0; $i -lt $thresholds.Count; $i++) {
$allowed = $expected + $threshold
$diff = $reported - $expected

# Retry once before failing: re-run this one command for both binaries and
# re-evaluate. Costs nothing when everything passes.
if ($reported -gt $allowed -and $iqtree3Cmd.Count -gt $i) {
Write-Host "↻ $command exceeded (${diff}MB); retrying this command once..."
$r2 = Measure-Once $iqtree2Cmd[$i]
$r3 = Measure-Once $iqtree3Cmd[$i]
if ($r2.Ok -and $r3.Ok) {
$expected = $r2.Mem
$reported = $r3.Mem
$allowed = $expected + $threshold
$diff = $reported - $expected
Write-Host " retry: IQ-TREE2 $($r2.Mem)MB, IQ-TREE3 $($r3.Mem)MB, Diff ${diff}MB"
} else {
Write-Host " retry did not produce a usable measurement; keeping the first result"
}
}

if ($reported -gt $allowed) {
Write-Host "❌ $command exceeded the allowed memory usage."
Write-Host " Expected: ${expected}MB, Threshold: ${threshold}MB, IQ-TREE3: ${reported}MB, Diff: ${diff}MB"
Expand Down
55 changes: 51 additions & 4 deletions test_scripts/verify_memory.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,44 @@ if [ -n "$fallback_column" ]; then
fi
fi

# Memory is column 3 of each log
# Memory is column 3 of each log; column 1 is the command actually executed,
# kept so a breaching check can be retried.
tail -n +2 "$iqtree2_log" | cut -f3 > "$tmp_iqtree2"
tail -n +2 "$iqtree3_log" | cut -f3 > "$tmp_iqtree3"
tmp_cmd2=$(mktemp); tmp_cmd3=$(mktemp)
tail -n +2 "$iqtree2_log" | cut -f1 > "$tmp_cmd2"
tail -n +2 "$iqtree3_log" | cut -f1 > "$tmp_cmd3"
# shellcheck source=/dev/null
. "$(dirname "$0")/remeasure.sh"

# Reconcile the number of benchmark commands with the number of table rows.
# They are joined POSITIONALLY, so a mismatch means the pairing is wrong: without
# this, `paste` pads the short side and bc is handed an empty threshold
# ("Parse error: bad token" on macOS, "syntax error" on Linux), and rows print
# with a bare number in place of the command name.
n_rows=$(wc -l < "$tmp_thresholds")
n_log=$(wc -l < "$tmp_iqtree3")
if [ "$n_log" -ne "$n_rows" ]; then
echo "⚠️ WARNING: the suite ran ${n_log} commands but memory has ${n_rows} threshold rows."
if [ "$n_log" -gt "$n_rows" ]; then
echo " Skipping the last $((n_log - n_rows)) command(s) - they have no threshold:"
tail -n +$((n_rows + 1)) "$tmp_cmd3" | sed 's/^/ /'
for t in "$tmp_iqtree2" "$tmp_iqtree3" "$tmp_cmd2" "$tmp_cmd3"; do
head -n "$n_rows" "$t" > "${t}.cut" && mv "${t}.cut" "$t"
done
else
echo " Ignoring the last $((n_rows - n_log)) threshold row(s) - no command produced them."
head -n "$n_log" "$tmp_thresholds" > "${tmp_thresholds}.cut" && mv "${tmp_thresholds}.cut" "$tmp_thresholds"
fi
echo " NOTE: rows are matched by POSITION. If the extra command(s) were added in the"
echo " middle rather than at the end, every later row is now compared against the"
echo " wrong command. Add the missing row(s) to keep the table in step."
fi

fail_count=0
row=0

while IFS=$'\t' read -r command threshold iqtree2_val iqtree3_val; do
while IFS=$'\t' read -r command threshold iqtree2_val iqtree3_val cmd2 cmd3; do
((row++))
expected="$iqtree2_val"

Expand All @@ -71,6 +101,23 @@ while IFS=$'\t' read -r command threshold iqtree2_val iqtree3_val; do
is_exceed=$(echo "$iqtree3_val > $allowed" | bc -l)
diff=$(echo "$iqtree3_val - $expected" | bc -l)

# Retry once before failing: re-run this one command for both binaries and
# re-evaluate. Costs nothing when everything passes.
if [ "$is_exceed" = "1" ] && [ -n "$cmd3" ]; then
echo "↻ $command exceeded (${diff}MB); retrying this command once..."
read -r _ retry2_mem <<< "$(remeasure "$cmd2")"
read -r _ retry3_mem <<< "$(remeasure "$cmd3")"
if [ "$(echo "$retry2_mem > 0" | bc -l)" = "1" ] && [ "$(echo "$retry3_mem > 0" | bc -l)" = "1" ]; then
expected="$retry2_mem"; iqtree3_val="$retry3_mem"
allowed=$(echo "$expected + $threshold" | bc -l)
is_exceed=$(echo "$iqtree3_val > $allowed" | bc -l)
diff=$(echo "$iqtree3_val - $expected" | bc -l)
echo " retry: IQ-TREE2 ${retry2_mem}MB, IQ-TREE3 ${retry3_mem}MB, Diff ${diff}MB"
else
echo " retry did not produce a usable measurement; keeping the first result"
fi
fi

if [ "$is_exceed" = "1" ]; then
echo "❌ $command exceeded the allowed memory usage."
echo " Expected: ${expected}MB, Threshold: ${threshold}MB, IQ-TREE3: ${iqtree3_val}MB, Diff: ${diff}MB"
Expand All @@ -79,9 +126,9 @@ while IFS=$'\t' read -r command threshold iqtree2_val iqtree3_val; do
echo "✅ $command passed the memory check."
echo " Expected: ${expected}MB, Threshold: ${threshold}MB, IQ-TREE3: ${iqtree3_val}MB, Diff: ${diff}MB"
fi
done < <(paste "$tmp_thresholds" "$tmp_iqtree2" "$tmp_iqtree3")
done < <(paste "$tmp_thresholds" "$tmp_iqtree2" "$tmp_iqtree3" "$tmp_cmd2" "$tmp_cmd3")

rm -f "$tmp_thresholds" "$tmp_fallback" "$tmp_iqtree2" "$tmp_iqtree3"
rm -f "$tmp_thresholds" "$tmp_fallback" "$tmp_iqtree2" "$tmp_iqtree3" "$tmp_cmd2" "$tmp_cmd3"

if [ "$fail_count" -eq 0 ]; then
echo "✅ All memory checks passed."
Expand Down
43 changes: 42 additions & 1 deletion test_scripts/verify_runtime.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ if ($FallbackColumn -ne "") {
$thrIdx = $hdr.IndexOf("thr-$FallbackColumn")
if ($thrIdx -ge 0) {
Write-Host "Using per-platform thresholds: thr-$FallbackColumn"
# NB: $nLog is not known yet here - bound the loop by the table itself.
for ($i = 0; $i -lt $thresholds.Count; $i++) {
$thresholds[$i].Threshold = [double]($thresholdLines[$i] -split "`t")[$thrIdx]
}
Expand Down Expand Up @@ -57,9 +58,32 @@ $iqtree2Times = foreach ($line in $iqtree2Lines) { [double]($line -split "`t")[1
$iqtree3Lines = Get-Content $IQTree3Log | Select-Object -Skip 1
$iqtree3Times = foreach ($line in $iqtree3Lines) { [double]($line -split "`t")[1] }

# Column 0 is the command actually executed, kept so a breaching check can be retried.
$iqtree2Cmd = foreach ($line in $iqtree2Lines) { ($line -split "`t")[0] }
$iqtree3Cmd = foreach ($line in $iqtree3Lines) { ($line -split "`t")[0] }
. (Join-Path $PSScriptRoot "remeasure.ps1")

# Reconcile the number of benchmark commands with the number of table rows.
# They are joined POSITIONALLY, so a mismatch means the pairing is wrong.
$nRows = $thresholds.Count
$nLog = $iqtree3Times.Count
if ($nLog -ne $nRows) {
Write-Host "WARNING: the suite ran $nLog commands but runtime has $nRows threshold rows."
if ($nLog -gt $nRows) {
Write-Host " Skipping the last $($nLog - $nRows) command(s) - they have no threshold:"
$iqtree3Cmd[$nRows..($nLog - 1)] | ForEach-Object { Write-Host " $_" }
} else {
Write-Host " Ignoring the last $($nRows - $nLog) threshold row(s) - no command produced them."
$thresholds = $thresholds[0..($nLog - 1)]
}
Write-Host " NOTE: rows are matched by POSITION. If the extra command(s) were added in the"
Write-Host " middle rather than at the end, every later row is now compared against the"
Write-Host " wrong command. Add the missing row(s) to keep the table in step."
}

$failCount = 0

for ($i = 0; $i -lt $thresholds.Count; $i++) {
for ($i = 0; $i -lt [Math]::Min($thresholds.Count, $nLog); $i++) {
$command = $thresholds[$i].Command
$threshold = $thresholds[$i].Threshold
$expected = $iqtree2Times[$i]
Expand All @@ -78,6 +102,23 @@ for ($i = 0; $i -lt $thresholds.Count; $i++) {
$allowed = $expected + $threshold
$diff = $reported - $expected

# Retry once before failing: re-run this one command for both binaries and
# re-evaluate. Costs nothing when everything passes.
if ($reported -gt $allowed -and $iqtree3Cmd.Count -gt $i) {
Write-Host "↻ $command exceeded (${diff}s); retrying this command once..."
$r2 = Measure-Once $iqtree2Cmd[$i]
$r3 = Measure-Once $iqtree3Cmd[$i]
if ($r2.Ok -and $r3.Ok) {
$expected = $r2.Time
$reported = $r3.Time
$allowed = $expected + $threshold
$diff = $reported - $expected
Write-Host " retry: IQ-TREE2 $($r2.Time)s, IQ-TREE3 $($r3.Time)s, Diff ${diff}s"
} else {
Write-Host " retry did not produce a usable measurement; keeping the first result"
}
}

if ($reported -gt $allowed) {
Write-Host "❌ $command exceeded the allowed runtime usage."
Write-Host " Expected: ${expected}s, Threshold: ${threshold}s, IQ-TREE3: ${reported}s, Diff: ${diff}s"
Expand Down
Loading
Loading