kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

ecosystem_speed.sh (21712B)


      1 #!/usr/bin/env bash
      2 # Compare ecosystem build-phase speed across kit, clang, and tcc.
      3 #
      4 # Phases are measured separately:
      5 #   compile: compile library TUs plus the driver TU to objects.
      6 #   archive: archive the kit-compiled library objects with kit ar vs llvm-ar.
      7 #   link:    link precompiled driver object plus static library into an app.
      8 #
      9 # Correctness remains owned by test/ecosystem/run.sh; this script is a timing
     10 # report over the same pinned project recipes.
     11 
     12 set -euo pipefail
     13 
     14 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
     15 ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
     16 SELF="$ROOT/scripts/ecosystem_speed.sh"
     17 ECO_DIR="$ROOT/test/ecosystem"
     18 RECIPE_DIR="$ECO_DIR/recipes"
     19 ECOSYS="$ROOT/scripts/ecosystem.sh"
     20 
     21 KIT="${KIT:-$ROOT/build/release/kit}"
     22 CLANG="${CLANG:-clang}"
     23 TCC="${TCC:-tcc}"
     24 HOST_AR="${AR:-llvm-ar}"
     25 RUNS="${ECO_SPEED_RUNS:-${RUNS:-5}}"
     26 TIMEOUT_S="${ECO_SPEED_TIMEOUT:-${TIMEOUT:-20}}"
     27 OPTS="${ECO_SPEED_OPTS:-O0 O1}"
     28 OUT_ROOT="${ECO_SPEED_OUT:-$ROOT/build/ecosystem-speed}"
     29 QUIET="${ECO_SPEED_QUIET:-0}"
     30 
     31 die() {
     32   printf 'ecosystem-speed: %s\n' "$*" >&2
     33   exit 2
     34 }
     35 
     36 progress() {
     37   [ "$QUIET" = 1 ] && return 0
     38   printf 'ecosystem-speed: %s\n' "$*" >&2
     39 }
     40 
     41 usage() {
     42   cat <<EOF
     43 usage: scripts/ecosystem_speed.sh [project ...]
     44 
     45 env:
     46   KIT                 kit binary (default: build/release/kit)
     47   CLANG               clang binary (default: clang)
     48   TCC                 tcc binary (default: tcc; skipped if unavailable)
     49   AR                  host archive binary (default: llvm-ar)
     50   ECO_SPEED_RUNS      best-of-N runs per row (default: 5)
     51   ECO_SPEED_TIMEOUT   phase timeout seconds per run (default: 20)
     52   ECO_SPEED_OPTS      opt levels, e.g. "O0 O1" (default: "O0 O1")
     53   ECO_SPEED_PROJECTS  project list if no argv is passed
     54   ECO_SPEED_OUT       output dir for logs/results (default: build/ecosystem-speed)
     55   ECO_SPEED_QUIET     set to 1 to suppress progress lines
     56 EOF
     57 }
     58 
     59 recipe_reset() {
     60   ECO_VERSION=
     61   ECO_URL=
     62   ECO_SHA256=
     63   ECO_SRCDIR=.
     64   ECO_INCLUDES="."
     65   ECO_DEFINES=
     66   ECO_LIBS=
     67   ECO_DRIVER=
     68   ECO_BUG=
     69   eco_lib_srcs() { :; }
     70   eco_run() { :; }
     71 }
     72 
     73 load_recipe() {
     74   local name=$1
     75   local recipe="$RECIPE_DIR/$name.recipe"
     76   [ -f "$recipe" ] || die "unknown project '$name' (see: scripts/ecosystem.sh list)"
     77   recipe_reset
     78   # shellcheck disable=SC1090
     79   . "$recipe"
     80 }
     81 
     82 srcdir_for() {
     83   "$ECOSYS" srcdir "$1" 2>/dev/null || true
     84 }
     85 
     86 driver_for() {
     87   case "$ECO_DRIVER" in
     88     use/*) printf '%s/%s\n' "$ECO_DIR" "$ECO_DRIVER" ;;
     89     *)     printf '%s/%s\n' "$ECO_SRC" "$ECO_DRIVER" ;;
     90   esac
     91 }
     92 
     93 host_sdk() {
     94   case "$(uname -s)" in
     95     Darwin) xcrun --sdk macosx --show-sdk-path 2>/dev/null || true ;;
     96     *)      printf '\n' ;;
     97   esac
     98 }
     99 
    100 append_sysroot_args() {
    101   local compiler=$1
    102   local sdk=$2
    103   case "$(uname -s)" in
    104     Darwin)
    105       [ -n "$sdk" ] || return 0
    106       case "$compiler" in
    107         kit)   printf '%s\n' --sysroot "$sdk" ;;
    108         clang) printf '%s\n' -isysroot "$sdk" ;;
    109         tcc)   printf '%s\n' -isysroot "$sdk" ;;
    110       esac
    111       ;;
    112   esac
    113 }
    114 
    115 quote_cmd() {
    116   local first=1
    117   for arg in "$@"; do
    118     if [ "$first" = 0 ]; then printf ' '; fi
    119     first=0
    120     printf '%q' "$arg"
    121   done
    122   printf '\n'
    123 }
    124 
    125 compiler_available() {
    126   local compiler=$1
    127   case "$compiler" in
    128     kit)   [ -x "$KIT" ] ;;
    129     clang) command -v "$CLANG" >/dev/null 2>&1 ;;
    130     tcc)
    131       if [ -x "$TCC" ]; then return 0; fi
    132       command -v "$TCC" >/dev/null 2>&1
    133       ;;
    134     *) return 1 ;;
    135   esac
    136 }
    137 
    138 archiver_available() {
    139   local archiver=$1
    140   case "$archiver" in
    141     kit)  [ -x "$KIT" ] ;;
    142     host) command -v "$HOST_AR" >/dev/null 2>&1 ;;
    143     *) return 1 ;;
    144   esac
    145 }
    146 
    147 compile_one() {
    148   local compiler=$1
    149   local opt=$2
    150   local project=$3
    151   local out_dir=$4
    152   local src_list=$5
    153   local sdk=$6
    154 
    155   load_recipe "$project"
    156   ECO_SRC="$(srcdir_for "$project")"
    157   export ECO_SRC ECO_DIR
    158   [ -n "$ECO_SRC" ] && [ -d "$ECO_SRC" ] || die "$project is not provisioned"
    159 
    160   mkdir -p "$out_dir/obj"
    161   : > "$out_dir/build.err"
    162   : > "$out_dir/cmds.sh"
    163   : > "$out_dir/objects"
    164   : > "$out_dir/lib_objs"
    165   : > "$out_dir/driver_obj"
    166 
    167   local inc_args=()
    168   local def_args=()
    169   local sys_args=()
    170   local inc def
    171   for inc in $ECO_INCLUDES; do inc_args+=("-I$ECO_SRC/$inc"); done
    172   for def in $ECO_DEFINES; do def_args+=("$def"); done
    173   while IFS= read -r a; do sys_args+=("$a"); done \
    174     < <(append_sysroot_args "$compiler" "$sdk")
    175 
    176   local total=0
    177   total=$(awk 'NF {n++} END {print n + 0}' "$src_list")
    178   [ "$total" -gt 0 ] || die "$project has no sources"
    179 
    180   local idx=0
    181   local src obj
    182   while IFS= read -r src; do
    183     [ -n "$src" ] || continue
    184     idx=$((idx + 1))
    185     obj="$out_dir/obj/$idx.o"
    186 
    187     local cmd=()
    188     case "$compiler" in
    189       kit)
    190         cmd=("$KIT" cc "-$opt" "${sys_args[@]}" "${def_args[@]}"
    191              "${inc_args[@]}" -c "$src" -o "$obj")
    192         ;;
    193       clang)
    194         cmd=("$CLANG" "-$opt" "${sys_args[@]}" "${def_args[@]}"
    195              "${inc_args[@]}" -c "$src" -o "$obj")
    196         ;;
    197       tcc)
    198         cmd=("$TCC" "-$opt" "${sys_args[@]}" "${def_args[@]}"
    199              "${inc_args[@]}" -c "$src" -o "$obj")
    200         ;;
    201       *) die "unknown compiler '$compiler'" ;;
    202     esac
    203 
    204     quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
    205     if ! "${cmd[@]}" 2>>"$out_dir/build.err"; then
    206       return 1
    207     fi
    208     printf '%s\n' "$obj" >> "$out_dir/objects"
    209     if [ "$idx" -lt "$total" ]; then
    210       printf '%s\n' "$obj" >> "$out_dir/lib_objs"
    211     else
    212       printf '%s\n' "$obj" > "$out_dir/driver_obj"
    213     fi
    214   done < "$src_list"
    215 }
    216 
    217 archive_one() {
    218   local archiver=$1
    219   local out_dir=$2
    220   local obj_list=$3
    221 
    222   mkdir -p "$out_dir"
    223   : > "$out_dir/build.err"
    224   : > "$out_dir/cmds.sh"
    225 
    226   local objs=()
    227   local obj
    228   while IFS= read -r obj; do
    229     [ -n "$obj" ] || continue
    230     objs+=("$obj")
    231   done < "$obj_list"
    232   [ "${#objs[@]}" -gt 0 ] || die "archive has no objects: $obj_list"
    233 
    234   local cmd=()
    235   case "$archiver" in
    236     kit)  cmd=("$KIT" ar rcs "$out_dir/lib.a" "${objs[@]}") ;;
    237     host) cmd=("$HOST_AR" rcs "$out_dir/lib.a" "${objs[@]}") ;;
    238     *) die "unknown archiver '$archiver'" ;;
    239   esac
    240   quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
    241   "${cmd[@]}" 2>>"$out_dir/build.err"
    242 }
    243 
    244 link_one() {
    245   local compiler=$1
    246   local opt=$2
    247   local project=$3
    248   local out_dir=$4
    249   local fixture_dir=$5
    250   local sdk=$6
    251 
    252   load_recipe "$project"
    253   ECO_SRC="$(srcdir_for "$project")"
    254   export ECO_SRC ECO_DIR
    255   [ -n "$ECO_SRC" ] && [ -d "$ECO_SRC" ] || die "$project is not provisioned"
    256 
    257   mkdir -p "$out_dir"
    258   : > "$out_dir/build.err"
    259   : > "$out_dir/cmds.sh"
    260 
    261   local driver_obj lib
    262   driver_obj=$(cat "$fixture_dir/driver_obj")
    263   lib="$fixture_dir/lib.a"
    264   [ -f "$driver_obj" ] || die "missing driver object: $driver_obj"
    265   [ -f "$lib" ] || die "missing archive: $lib"
    266 
    267   local sys_args=()
    268   local lib_args=()
    269   local a
    270   while IFS= read -r a; do sys_args+=("$a"); done \
    271     < <(append_sysroot_args "$compiler" "$sdk")
    272   for a in $ECO_LIBS; do lib_args+=("$a"); done
    273 
    274   local cmd=()
    275   case "$compiler" in
    276     kit)
    277       cmd=("$KIT" cc "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
    278            "${lib_args[@]}" -lc -o "$out_dir/app")
    279       ;;
    280     clang)
    281       cmd=("$CLANG" "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
    282            "${lib_args[@]}" -o "$out_dir/app")
    283       ;;
    284     tcc)
    285       cmd=("$TCC" "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
    286            "${lib_args[@]}" -o "$out_dir/app")
    287       ;;
    288     *) die "unknown compiler '$compiler'" ;;
    289   esac
    290   quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
    291   "${cmd[@]}" 2>>"$out_dir/build.err"
    292 }
    293 
    294 run_with_timeout() {
    295   local timeout_s=$1
    296   shift
    297   perl -MTime::HiRes=time,sleep -MPOSIX=':sys_wait_h,setpgid' -e '
    298     my ($timeout, @cmd) = @ARGV;
    299     my $start = time;
    300     my $pid = fork();
    301     die "fork failed\n" unless defined $pid;
    302     if ($pid == 0) {
    303       setpgid(0, 0);
    304       exec @cmd;
    305       print STDERR "exec failed: $!\n";
    306       exit 127;
    307     }
    308     setpgid($pid, $pid);
    309     my $timed_out = 0;
    310     my $status;
    311     while (1) {
    312       my $r = waitpid($pid, WNOHANG);
    313       if ($r == $pid) {
    314         $status = $?;
    315         last;
    316       }
    317       if (time - $start >= $timeout) {
    318         $timed_out = 1;
    319         kill "TERM", -$pid;
    320         my $until = time + 2.0;
    321         while (time < $until) {
    322           $r = waitpid($pid, WNOHANG);
    323           if ($r == $pid) {
    324             $status = $?;
    325             last;
    326           }
    327           sleep 0.02;
    328         }
    329         if (!defined $status) {
    330           kill "KILL", -$pid;
    331           waitpid($pid, 0);
    332           $status = $?;
    333         }
    334         last;
    335       }
    336       sleep 0.01;
    337     }
    338     my $elapsed = time - $start;
    339     my $rc;
    340     if ($timed_out) {
    341       $rc = 124;
    342     } elsif ($status & 127) {
    343       $rc = 128 + ($status & 127);
    344     } else {
    345       $rc = ($status >> 8) & 255;
    346     }
    347     printf "elapsed %.6f\nrc %d\n", $elapsed, $rc;
    348     exit $rc;
    349   ' "$timeout_s" "$@"
    350 }
    351 
    352 timed_phase() {
    353   local phase=$1
    354   local tool=$2
    355   local run_dir=$3
    356   shift 3
    357 
    358   mkdir -p "$run_dir"
    359   local cmd=()
    360   case "$phase" in
    361     compile)
    362       [ $# -eq 4 ] || die "compile phase expects opt project src_list sdk"
    363       cmd=(bash "$SELF" --compile-one "$tool" "$1" "$2" "$run_dir" "$3" "$4")
    364       ;;
    365     archive)
    366       [ $# -eq 1 ] || die "archive phase expects obj_list"
    367       cmd=(bash "$SELF" --archive-one "$tool" "$run_dir" "$1")
    368       ;;
    369     link)
    370       [ $# -eq 4 ] || die "link phase expects opt project fixture_dir sdk"
    371       cmd=(bash "$SELF" --link-one "$tool" "$1" "$2" "$run_dir" "$3" "$4")
    372       ;;
    373     *) die "unknown phase '$phase'" ;;
    374   esac
    375   set +e
    376   run_with_timeout "$TIMEOUT_S" "${cmd[@]}" \
    377     > "$run_dir/time.txt" 2> "$run_dir/time.err"
    378   local rc=$?
    379   set -e
    380 
    381   local elapsed
    382   elapsed=$(awk '/^elapsed / {print $2}' "$run_dir/time.txt" | tail -1)
    383   [ -n "$elapsed" ] || elapsed=0
    384   printf '%s,%s\n' "$rc" "$elapsed"
    385 }
    386 
    387 record_best() {
    388   local phase=$1
    389   local tool=$2
    390   local project=$3
    391   local opt=$4
    392   local row_dir=$5
    393   shift 5
    394 
    395   local best=""
    396   local best_rc=125
    397   local run
    398   for run in $(seq 1 "$RUNS"); do
    399     progress "$project $opt $phase $tool run $run/$RUNS"
    400     local run_dir="$row_dir/$phase-$tool-$run"
    401     local got rc elapsed
    402     got=$(timed_phase "$phase" "$tool" "$run_dir" "$@")
    403     rc=${got%,*}
    404     elapsed=${got#*,}
    405     printf '%s,%s,%s,%s,%s,%s,%s\n' "$project" "$opt" "$phase" "$tool" \
    406       "$run" "$rc" "$elapsed" >> "$OUT_ROOT/runs.csv"
    407     if [ "$rc" -eq 0 ]; then
    408       local ms
    409       ms=$(awk -v s="$elapsed" 'BEGIN { printf "%.3f", s * 1000.0 }')
    410       if [ -z "$best" ] || awk -v a="$ms" -v b="$best" 'BEGIN { exit !(a < b) }'; then
    411         best=$ms
    412         best_rc=0
    413       fi
    414     else
    415       best_rc=$rc
    416     fi
    417   done
    418 
    419   if [ -n "$best" ]; then
    420     printf '%s\n' "$best"
    421   elif [ "$best_rc" -eq 124 ]; then
    422     printf 'timeout\n'
    423   else
    424     printf 'fail\n'
    425   fi
    426 }
    427 
    428 best_compile_ms_for() {
    429   local compiler=$1
    430   local project=$2
    431   local opt=$3
    432   local src_list=$4
    433   local sdk=$5
    434   local row_dir=$6
    435 
    436   if ! compiler_available "$compiler"; then
    437     printf 'missing\n'
    438     return 0
    439   fi
    440   record_best compile "$compiler" "$project" "$opt" "$row_dir" \
    441     "$opt" "$project" "$src_list" "$sdk"
    442 }
    443 
    444 best_archive_ms_for() {
    445   local archiver=$1
    446   local project=$2
    447   local opt=$3
    448   local fixture_dir=$4
    449   local row_dir=$5
    450 
    451   if ! archiver_available "$archiver"; then
    452     printf 'missing\n'
    453     return 0
    454   fi
    455   [ -f "$fixture_dir/lib_objs" ] || { printf 'fail\n'; return 0; }
    456   record_best archive "$archiver" "$project" "$opt" "$row_dir" \
    457     "$fixture_dir/lib_objs"
    458 }
    459 
    460 best_link_ms_for() {
    461   local compiler=$1
    462   local project=$2
    463   local opt=$3
    464   local fixture_dir=$4
    465   local sdk=$5
    466   local row_dir=$6
    467 
    468   if ! compiler_available "$compiler"; then
    469     printf 'missing\n'
    470     return 0
    471   fi
    472   [ -f "$fixture_dir/lib.a" ] || { printf 'fail\n'; return 0; }
    473   record_best link "$compiler" "$project" "$opt" "$row_dir" \
    474     "$opt" "$project" "$fixture_dir" "$sdk"
    475 }
    476 
    477 make_fixture() {
    478   local compiler=$1
    479   local project=$2
    480   local opt=$3
    481   local src_list=$4
    482   local sdk=$5
    483   local fixture_dir=$6
    484 
    485   if ! compiler_available "$compiler"; then
    486     return 1
    487   fi
    488   rm -rf "$fixture_dir"
    489   mkdir -p "$fixture_dir"
    490   local got rc
    491   got=$(timed_phase compile "$compiler" "$fixture_dir" \
    492     "$opt" "$project" "$src_list" "$sdk")
    493   rc=${got%,*}
    494   [ "$rc" -eq 0 ] || return 1
    495 
    496   local archiver=host
    497   [ "$compiler" = kit ] && archiver=kit
    498   got=$(timed_phase archive "$archiver" "$fixture_dir/archive" \
    499     "$fixture_dir/lib_objs")
    500   rc=${got%,*}
    501   [ "$rc" -eq 0 ] || return 1
    502   mv "$fixture_dir/archive/lib.a" "$fixture_dir/lib.a"
    503   return 0
    504 }
    505 
    506 prepare_project() {
    507   local project=$1
    508   local out_dir=$2
    509   local src_list="$out_dir/srcs"
    510   local lib_src_list="$out_dir/lib_srcs"
    511   load_recipe "$project"
    512   ECO_SRC="$(srcdir_for "$project")"
    513   export ECO_SRC ECO_DIR
    514   if [ -z "$ECO_SRC" ] || [ ! -d "$ECO_SRC" ]; then
    515     return 1
    516   fi
    517   mkdir -p "$out_dir"
    518   eco_lib_srcs > "$lib_src_list"
    519   cp "$lib_src_list" "$src_list"
    520   driver_for >> "$src_list"
    521   return 0
    522 }
    523 
    524 print_toolchain_phase() {
    525   local results=$1
    526   local phase=$2
    527   local units_label=$3
    528 
    529   awk -F, -v phase_name="$phase" -v units_label="$units_label" \
    530     -v opts_list="$OPTS" '
    531     function isnum(x) { return x ~ /^[0-9]+([.][0-9]+)?$/ }
    532     function cell(x) { return isnum(x) ? sprintf("%8.1f", x) : sprintf("%8s", x) }
    533     function speed(ref, kit) {
    534       return (isnum(ref) && isnum(kit) && kit > 0) ? sprintf("%7.3fx", ref / kit) : sprintf("%7s", "-")
    535     }
    536     function add_geo(tag, ref, kit) {
    537       if (isnum(ref) && isnum(kit) && kit > 0 && ref > 0) {
    538         geo_sum[tag] += log(ref / kit)
    539         geo_n[tag]++
    540       }
    541     }
    542     BEGIN {
    543       nopts = split(opts_list, opts, /[ \t]+/)
    544       printf "\n[%s]\n", phase_name
    545       printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
    546              "project", "opt", units_label, "kit_ms", "clang_ms", "k/clang", "tcc_ms", "k/tcc"
    547       printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
    548              "----------", "---", "----", "--------", "--------", "-------", "--------", "-------"
    549     }
    550     NR == 1 { next }
    551     $1 == phase_name {
    552       opt=$3; units=$4; kit=$5; clang=$6; tcc=$7
    553       printf "%-10s %-3s %4s %s %s %s %s %s\n", \
    554              $2, opt, units, cell(kit), cell(clang), speed(clang, kit), cell(tcc), speed(tcc, kit)
    555       add_geo(opt ":clang", clang, kit)
    556       add_geo(opt ":tcc", tcc, kit)
    557       add_geo("all:clang", clang, kit)
    558       add_geo("all:tcc", tcc, kit)
    559     }
    560     END {
    561       printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
    562              "----------", "---", "----", "--------", "--------", "-------", "--------", "-------"
    563       for (i = 1; i <= nopts; ++i) {
    564         opt = opts[i]
    565         kc = opt ":clang"
    566         kt = opt ":tcc"
    567         cnt_clang = geo_n[kc] + 0
    568         cnt_tcc = geo_n[kt] + 0
    569         clang_geo = geo_n[kc] ? sprintf("%7.3fx", exp(geo_sum[kc] / geo_n[kc])) : sprintf("%7s", "-")
    570         tcc_geo = geo_n[kt] ? sprintf("%7.3fx", exp(geo_sum[kt] / geo_n[kt])) : sprintf("%7s", "-")
    571         printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
    572                "geomean", opt, cnt_clang "/" cnt_tcc, "", "", clang_geo, "", tcc_geo
    573       }
    574       cnt_clang = geo_n["all:clang"] + 0
    575       cnt_tcc = geo_n["all:tcc"] + 0
    576       clang_geo = geo_n["all:clang"] ? sprintf("%7.3fx", exp(geo_sum["all:clang"] / geo_n["all:clang"])) : sprintf("%7s", "-")
    577       tcc_geo = geo_n["all:tcc"] ? sprintf("%7.3fx", exp(geo_sum["all:tcc"] / geo_n["all:tcc"])) : sprintf("%7s", "-")
    578       printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
    579              "geomean", "all", cnt_clang "/" cnt_tcc, "", "", clang_geo, "", tcc_geo
    580     }
    581   ' "$results"
    582 }
    583 
    584 print_archive_phase() {
    585   local results=$1
    586 
    587   awk -F, -v opts_list="$OPTS" '
    588     function isnum(x) { return x ~ /^[0-9]+([.][0-9]+)?$/ }
    589     function cell(x) { return isnum(x) ? sprintf("%8.1f", x) : sprintf("%8s", x) }
    590     function speed(ref, kit) {
    591       return (isnum(ref) && isnum(kit) && kit > 0) ? sprintf("%7.3fx", ref / kit) : sprintf("%7s", "-")
    592     }
    593     function add_geo(tag, ref, kit) {
    594       if (isnum(ref) && isnum(kit) && kit > 0 && ref > 0) {
    595         geo_sum[tag] += log(ref / kit)
    596         geo_n[tag]++
    597       }
    598     }
    599     BEGIN {
    600       nopts = split(opts_list, opts, /[ \t]+/)
    601       printf "\n[archive]\n"
    602       printf "%-10s %-3s %4s %8s %8s %7s\n", \
    603              "project", "opt", "objs", "kit_ar", "host_ar", "k/host"
    604       printf "%-10s %-3s %4s %8s %8s %7s\n", \
    605              "----------", "---", "----", "--------", "--------", "-------"
    606     }
    607     NR == 1 { next }
    608     $1 == "archive" {
    609       opt=$3; units=$4; kit=$5; host=$8
    610       printf "%-10s %-3s %4s %s %s %s\n", \
    611              $2, opt, units, cell(kit), cell(host), speed(host, kit)
    612       add_geo(opt ":host", host, kit)
    613       add_geo("all:host", host, kit)
    614     }
    615     END {
    616       printf "%-10s %-3s %4s %8s %8s %7s\n", \
    617              "----------", "---", "----", "--------", "--------", "-------"
    618       for (i = 1; i <= nopts; ++i) {
    619         opt = opts[i]
    620         key = opt ":host"
    621         host_geo = geo_n[key] ? sprintf("%7.3fx", exp(geo_sum[key] / geo_n[key])) : sprintf("%7s", "-")
    622         printf "%-10s %-3s %4s %8s %8s %7s\n", \
    623                "geomean", opt, geo_n[key], "", "", host_geo
    624       }
    625       host_geo = geo_n["all:host"] ? sprintf("%7.3fx", exp(geo_sum["all:host"] / geo_n["all:host"])) : sprintf("%7s", "-")
    626       printf "%-10s %-3s %4s %8s %8s %7s\n", \
    627              "geomean", "all", geo_n["all:host"], "", "", host_geo
    628     }
    629   ' "$results"
    630 }
    631 
    632 print_report() {
    633   local results=$1
    634   printf 'ecosystem speed: best-of-%s, timeout=%ss\n' "$RUNS" "$TIMEOUT_S"
    635   printf 'kit=%s\n' "$KIT"
    636   printf 'clang=%s\n' "$CLANG"
    637   if compiler_available tcc; then
    638     printf 'tcc=%s\n' "$TCC"
    639   else
    640     printf 'tcc=%s (missing; rows skipped)\n' "$TCC"
    641   fi
    642   printf 'host_ar=%s\n' "$HOST_AR"
    643   printf 'archive_input=kit-compiled library objects\n'
    644   print_toolchain_phase "$results" compile srcs
    645   print_archive_phase "$results"
    646   print_toolchain_phase "$results" link apps
    647   printf '\nlogs=%s\n' "$OUT_ROOT"
    648 }
    649 
    650 main() {
    651   if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
    652     usage
    653     exit 0
    654   fi
    655   case "${1:-}" in
    656     --compile-one)
    657       shift
    658       [ $# -eq 6 ] || die "--compile-one expects compiler opt project out_dir src_list sdk"
    659       compile_one "$@"
    660       exit $?
    661       ;;
    662     --archive-one)
    663       shift
    664       [ $# -eq 3 ] || die "--archive-one expects archiver out_dir obj_list"
    665       archive_one "$@"
    666       exit $?
    667       ;;
    668     --link-one)
    669       shift
    670       [ $# -eq 6 ] || die "--link-one expects compiler opt project out_dir fixture_dir sdk"
    671       link_one "$@"
    672       exit $?
    673       ;;
    674   esac
    675 
    676   [ -x "$KIT" ] || die "kit binary not found at $KIT (run: make RELEASE=1 bin)"
    677   command -v "$CLANG" >/dev/null 2>&1 || die "clang not found: $CLANG"
    678   command -v "$HOST_AR" >/dev/null 2>&1 || die "host ar not found: $HOST_AR"
    679 
    680   mkdir -p "$OUT_ROOT"
    681   printf 'project,opt,phase,tool,run,rc,seconds\n' > "$OUT_ROOT/runs.csv"
    682   local results="$OUT_ROOT/results.csv"
    683   printf 'phase,project,opt,units,kit_ms,clang_ms,tcc_ms,host_ms\n' > "$results"
    684 
    685   local sdk
    686   sdk="$(host_sdk)"
    687 
    688   local projects
    689   if [ "$#" -gt 0 ]; then
    690     projects="$*"
    691   elif [ -n "${ECO_SPEED_PROJECTS:-}" ]; then
    692     projects="$ECO_SPEED_PROJECTS"
    693   else
    694     projects="$("$ECOSYS" list)"
    695   fi
    696 
    697   local any=0
    698   local project opt
    699   for project in $projects; do
    700     local project_dir="$OUT_ROOT/$project"
    701     if ! prepare_project "$project" "$project_dir"; then
    702       printf 'ecosystem-speed: skip %-10s not provisioned (run: make provision-ecosystem)\n' "$project" >&2
    703       continue
    704     fi
    705     any=1
    706 
    707     local src_list="$project_dir/srcs"
    708     local lib_src_list="$project_dir/lib_srcs"
    709     local srcs lib_objs
    710     srcs=$(awk 'NF {n++} END {print n + 0}' "$src_list")
    711     lib_objs=$(awk 'NF {n++} END {print n + 0}' "$lib_src_list")
    712 
    713     for opt in $OPTS; do
    714       local row_dir="$project_dir/$opt"
    715       mkdir -p "$row_dir"
    716 
    717       local kit_compile clang_compile tcc_compile
    718       progress "$project $opt compile"
    719       kit_compile=$(best_compile_ms_for kit "$project" "$opt" "$src_list" "$sdk" "$row_dir")
    720       clang_compile=$(best_compile_ms_for clang "$project" "$opt" "$src_list" "$sdk" "$row_dir")
    721       tcc_compile=$(best_compile_ms_for tcc "$project" "$opt" "$src_list" "$sdk" "$row_dir")
    722       printf 'compile,%s,%s,%s,%s,%s,%s,\n' "$project" "$opt" "$srcs" \
    723         "$kit_compile" "$clang_compile" "$tcc_compile" >> "$results"
    724 
    725       local kit_fixture="$row_dir/fixture-kit"
    726       local clang_fixture="$row_dir/fixture-clang"
    727       local tcc_fixture="$row_dir/fixture-tcc"
    728       progress "$project $opt fixtures"
    729       make_fixture kit "$project" "$opt" "$src_list" "$sdk" "$kit_fixture" || true
    730       make_fixture clang "$project" "$opt" "$src_list" "$sdk" "$clang_fixture" || true
    731       make_fixture tcc "$project" "$opt" "$src_list" "$sdk" "$tcc_fixture" || true
    732 
    733       local kit_ar host_ar
    734       progress "$project $opt archive"
    735       kit_ar=$(best_archive_ms_for kit "$project" "$opt" "$kit_fixture" "$row_dir")
    736       host_ar=$(best_archive_ms_for host "$project" "$opt" "$kit_fixture" "$row_dir")
    737       printf 'archive,%s,%s,%s,%s,,,%s\n' "$project" "$opt" "$lib_objs" \
    738         "$kit_ar" "$host_ar" >> "$results"
    739 
    740       local kit_link clang_link tcc_link
    741       progress "$project $opt link"
    742       kit_link=$(best_link_ms_for kit "$project" "$opt" "$kit_fixture" "$sdk" "$row_dir")
    743       clang_link=$(best_link_ms_for clang "$project" "$opt" "$clang_fixture" "$sdk" "$row_dir")
    744       tcc_link=$(best_link_ms_for tcc "$project" "$opt" "$tcc_fixture" "$sdk" "$row_dir")
    745       printf 'link,%s,%s,1,%s,%s,%s,\n' "$project" "$opt" \
    746         "$kit_link" "$clang_link" "$tcc_link" >> "$results"
    747     done
    748   done
    749 
    750   [ "$any" = 1 ] || die "no provisioned ecosystem projects found"
    751   print_report "$results"
    752 }
    753 
    754 main "$@"