commit 7087a3c382b08472a57ec07f33498c2b300e41f8
parent ed5f4832cdf3eb4a5193b6a54bcde06f79ba578f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 09:48:49 -0700
scripts: ecosystem build-speed benchmark + ecosystem-speed target
Preserves uncommitted working-tree edits found on main (kit/clang/tcc
ecosystem build-phase timing harness) so the O1-PATTERNS integration merge
can proceed without discarding them.
Diffstat:
2 files changed, 749 insertions(+), 1 deletion(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -323,13 +323,19 @@ test-extlink: bin
# (network); the gate then runs fully offline. Known kit bugs are kept RED on
# purpose (see test/ecosystem/known_bugs/); failing artifacts are preserved
# under build/ecosystem-fail/.
-.PHONY: test-ecosystem provision-ecosystem
+.PHONY: test-ecosystem provision-ecosystem ecosystem-speed
provision-ecosystem:
@sh scripts/ecosystem.sh fetch all
test-ecosystem: bin
@KIT=$(abspath $(BIN)) sh test/ecosystem/run.sh
+ecosystem-speed:
+ @if [ ! -x build/release/kit ]; then \
+ $(MAKE) --no-print-directory RELEASE=1 bin; \
+ fi
+ @KIT=$(abspath build/release/kit) AR=$(AR) bash scripts/ecosystem_speed.sh
+
# test-dist: one-command verification of the dist subsystem (content-addressed
# store + signed packaging). Runs the CAS and pkg driver corpora, which cover
# round-trip, integrity/tamper detection, trust store, and the read-side
diff --git a/scripts/ecosystem_speed.sh b/scripts/ecosystem_speed.sh
@@ -0,0 +1,742 @@
+#!/usr/bin/env bash
+# Compare ecosystem build-phase speed across kit, clang, and tcc.
+#
+# Phases are measured separately:
+# compile: compile library TUs plus the driver TU to objects.
+# archive: archive the kit-compiled library objects with kit ar vs host ar.
+# link: link precompiled driver object plus static library into an app.
+#
+# Correctness remains owned by test/ecosystem/run.sh; this script is a timing
+# report over the same pinned project recipes.
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
+SELF="$ROOT/scripts/ecosystem_speed.sh"
+ECO_DIR="$ROOT/test/ecosystem"
+RECIPE_DIR="$ECO_DIR/recipes"
+ECOSYS="$ROOT/scripts/ecosystem.sh"
+
+KIT="${KIT:-$ROOT/build/release/kit}"
+CLANG="${CLANG:-clang}"
+TCC="${TCC:-tcc}"
+HOST_AR="${AR:-ar}"
+RUNS="${ECO_SPEED_RUNS:-${RUNS:-5}}"
+TIMEOUT_S="${ECO_SPEED_TIMEOUT:-${TIMEOUT:-20}}"
+OPTS="${ECO_SPEED_OPTS:-O0 O1}"
+OUT_ROOT="${ECO_SPEED_OUT:-$ROOT/build/ecosystem-speed}"
+
+die() {
+ printf 'ecosystem-speed: %s\n' "$*" >&2
+ exit 2
+}
+
+usage() {
+ cat <<EOF
+usage: scripts/ecosystem_speed.sh [project ...]
+
+env:
+ KIT kit binary (default: build/release/kit)
+ CLANG clang binary (default: clang)
+ TCC tcc binary (default: tcc; skipped if unavailable)
+ AR host ar binary (default: ar)
+ ECO_SPEED_RUNS best-of-N runs per row (default: 5)
+ ECO_SPEED_TIMEOUT phase timeout seconds per run (default: 20)
+ ECO_SPEED_OPTS opt levels, e.g. "O0 O1" (default: "O0 O1")
+ ECO_SPEED_PROJECTS project list if no argv is passed
+ ECO_SPEED_OUT output dir for logs/results (default: build/ecosystem-speed)
+EOF
+}
+
+recipe_reset() {
+ ECO_VERSION=
+ ECO_URL=
+ ECO_SHA256=
+ ECO_SRCDIR=.
+ ECO_INCLUDES="."
+ ECO_DEFINES=
+ ECO_LIBS=
+ ECO_DRIVER=
+ ECO_BUG=
+ eco_lib_srcs() { :; }
+ eco_run() { :; }
+}
+
+load_recipe() {
+ local name=$1
+ local recipe="$RECIPE_DIR/$name.recipe"
+ [ -f "$recipe" ] || die "unknown project '$name' (see: scripts/ecosystem.sh list)"
+ recipe_reset
+ # shellcheck disable=SC1090
+ . "$recipe"
+}
+
+srcdir_for() {
+ "$ECOSYS" srcdir "$1" 2>/dev/null || true
+}
+
+driver_for() {
+ case "$ECO_DRIVER" in
+ use/*) printf '%s/%s\n' "$ECO_DIR" "$ECO_DRIVER" ;;
+ *) printf '%s/%s\n' "$ECO_SRC" "$ECO_DRIVER" ;;
+ esac
+}
+
+host_sdk() {
+ case "$(uname -s)" in
+ Darwin) xcrun --sdk macosx --show-sdk-path 2>/dev/null || true ;;
+ *) printf '\n' ;;
+ esac
+}
+
+append_sysroot_args() {
+ local compiler=$1
+ local sdk=$2
+ case "$(uname -s)" in
+ Darwin)
+ [ -n "$sdk" ] || return 0
+ case "$compiler" in
+ kit) printf '%s\n' --sysroot "$sdk" ;;
+ clang) printf '%s\n' -isysroot "$sdk" ;;
+ tcc) printf '%s\n' -isysroot "$sdk" ;;
+ esac
+ ;;
+ esac
+}
+
+quote_cmd() {
+ local first=1
+ for arg in "$@"; do
+ if [ "$first" = 0 ]; then printf ' '; fi
+ first=0
+ printf '%q' "$arg"
+ done
+ printf '\n'
+}
+
+compiler_available() {
+ local compiler=$1
+ case "$compiler" in
+ kit) [ -x "$KIT" ] ;;
+ clang) command -v "$CLANG" >/dev/null 2>&1 ;;
+ tcc)
+ if [ -x "$TCC" ]; then return 0; fi
+ command -v "$TCC" >/dev/null 2>&1
+ ;;
+ *) return 1 ;;
+ esac
+}
+
+archiver_available() {
+ local archiver=$1
+ case "$archiver" in
+ kit) [ -x "$KIT" ] ;;
+ host) command -v "$HOST_AR" >/dev/null 2>&1 ;;
+ *) return 1 ;;
+ esac
+}
+
+compile_one() {
+ local compiler=$1
+ local opt=$2
+ local project=$3
+ local out_dir=$4
+ local src_list=$5
+ local sdk=$6
+
+ load_recipe "$project"
+ ECO_SRC="$(srcdir_for "$project")"
+ export ECO_SRC ECO_DIR
+ [ -n "$ECO_SRC" ] && [ -d "$ECO_SRC" ] || die "$project is not provisioned"
+
+ mkdir -p "$out_dir/obj"
+ : > "$out_dir/build.err"
+ : > "$out_dir/cmds.sh"
+ : > "$out_dir/objects"
+ : > "$out_dir/lib_objs"
+ : > "$out_dir/driver_obj"
+
+ local inc_args=()
+ local def_args=()
+ local sys_args=()
+ local inc def
+ for inc in $ECO_INCLUDES; do inc_args+=("-I$ECO_SRC/$inc"); done
+ for def in $ECO_DEFINES; do def_args+=("$def"); done
+ while IFS= read -r a; do sys_args+=("$a"); done \
+ < <(append_sysroot_args "$compiler" "$sdk")
+
+ local total=0
+ total=$(awk 'NF {n++} END {print n + 0}' "$src_list")
+ [ "$total" -gt 0 ] || die "$project has no sources"
+
+ local idx=0
+ local src obj
+ while IFS= read -r src; do
+ [ -n "$src" ] || continue
+ idx=$((idx + 1))
+ obj="$out_dir/obj/$idx.o"
+
+ local cmd=()
+ case "$compiler" in
+ kit)
+ cmd=("$KIT" cc "-$opt" "${sys_args[@]}" "${def_args[@]}"
+ "${inc_args[@]}" -c "$src" -o "$obj")
+ ;;
+ clang)
+ cmd=("$CLANG" "-$opt" "${sys_args[@]}" "${def_args[@]}"
+ "${inc_args[@]}" -c "$src" -o "$obj")
+ ;;
+ tcc)
+ cmd=("$TCC" "-$opt" "${sys_args[@]}" "${def_args[@]}"
+ "${inc_args[@]}" -c "$src" -o "$obj")
+ ;;
+ *) die "unknown compiler '$compiler'" ;;
+ esac
+
+ quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
+ if ! "${cmd[@]}" 2>>"$out_dir/build.err"; then
+ return 1
+ fi
+ printf '%s\n' "$obj" >> "$out_dir/objects"
+ if [ "$idx" -lt "$total" ]; then
+ printf '%s\n' "$obj" >> "$out_dir/lib_objs"
+ else
+ printf '%s\n' "$obj" > "$out_dir/driver_obj"
+ fi
+ done < "$src_list"
+}
+
+archive_one() {
+ local archiver=$1
+ local out_dir=$2
+ local obj_list=$3
+
+ mkdir -p "$out_dir"
+ : > "$out_dir/build.err"
+ : > "$out_dir/cmds.sh"
+
+ local objs=()
+ local obj
+ while IFS= read -r obj; do
+ [ -n "$obj" ] || continue
+ objs+=("$obj")
+ done < "$obj_list"
+ [ "${#objs[@]}" -gt 0 ] || die "archive has no objects: $obj_list"
+
+ local cmd=()
+ case "$archiver" in
+ kit) cmd=("$KIT" ar rcs "$out_dir/lib.a" "${objs[@]}") ;;
+ host) cmd=("$HOST_AR" rcs "$out_dir/lib.a" "${objs[@]}") ;;
+ *) die "unknown archiver '$archiver'" ;;
+ esac
+ quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
+ "${cmd[@]}" 2>>"$out_dir/build.err"
+}
+
+link_one() {
+ local compiler=$1
+ local opt=$2
+ local project=$3
+ local out_dir=$4
+ local fixture_dir=$5
+ local sdk=$6
+
+ load_recipe "$project"
+ ECO_SRC="$(srcdir_for "$project")"
+ export ECO_SRC ECO_DIR
+ [ -n "$ECO_SRC" ] && [ -d "$ECO_SRC" ] || die "$project is not provisioned"
+
+ mkdir -p "$out_dir"
+ : > "$out_dir/build.err"
+ : > "$out_dir/cmds.sh"
+
+ local driver_obj lib
+ driver_obj=$(cat "$fixture_dir/driver_obj")
+ lib="$fixture_dir/lib.a"
+ [ -f "$driver_obj" ] || die "missing driver object: $driver_obj"
+ [ -f "$lib" ] || die "missing archive: $lib"
+
+ local sys_args=()
+ local lib_args=()
+ local a
+ while IFS= read -r a; do sys_args+=("$a"); done \
+ < <(append_sysroot_args "$compiler" "$sdk")
+ for a in $ECO_LIBS; do lib_args+=("$a"); done
+
+ local cmd=()
+ case "$compiler" in
+ kit)
+ cmd=("$KIT" cc "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
+ "${lib_args[@]}" -lc -o "$out_dir/app")
+ ;;
+ clang)
+ cmd=("$CLANG" "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
+ "${lib_args[@]}" -o "$out_dir/app")
+ ;;
+ tcc)
+ cmd=("$TCC" "-$opt" "${sys_args[@]}" "$driver_obj" "$lib"
+ "${lib_args[@]}" -o "$out_dir/app")
+ ;;
+ *) die "unknown compiler '$compiler'" ;;
+ esac
+ quote_cmd "${cmd[@]}" >> "$out_dir/cmds.sh"
+ "${cmd[@]}" 2>>"$out_dir/build.err"
+}
+
+run_with_timeout() {
+ local timeout_s=$1
+ shift
+ perl -MTime::HiRes=time,sleep -MPOSIX=':sys_wait_h,setpgid' -e '
+ my ($timeout, @cmd) = @ARGV;
+ my $start = time;
+ my $pid = fork();
+ die "fork failed\n" unless defined $pid;
+ if ($pid == 0) {
+ setpgid(0, 0);
+ exec @cmd;
+ print STDERR "exec failed: $!\n";
+ exit 127;
+ }
+ setpgid($pid, $pid);
+ my $timed_out = 0;
+ my $status;
+ while (1) {
+ my $r = waitpid($pid, WNOHANG);
+ if ($r == $pid) {
+ $status = $?;
+ last;
+ }
+ if (time - $start >= $timeout) {
+ $timed_out = 1;
+ kill "TERM", -$pid;
+ my $until = time + 2.0;
+ while (time < $until) {
+ $r = waitpid($pid, WNOHANG);
+ if ($r == $pid) {
+ $status = $?;
+ last;
+ }
+ sleep 0.02;
+ }
+ if (!defined $status) {
+ kill "KILL", -$pid;
+ waitpid($pid, 0);
+ $status = $?;
+ }
+ last;
+ }
+ sleep 0.01;
+ }
+ my $elapsed = time - $start;
+ my $rc;
+ if ($timed_out) {
+ $rc = 124;
+ } elsif ($status & 127) {
+ $rc = 128 + ($status & 127);
+ } else {
+ $rc = ($status >> 8) & 255;
+ }
+ printf "elapsed %.6f\nrc %d\n", $elapsed, $rc;
+ exit $rc;
+ ' "$timeout_s" "$@"
+}
+
+timed_phase() {
+ local phase=$1
+ local tool=$2
+ local run_dir=$3
+ shift 3
+
+ mkdir -p "$run_dir"
+ local cmd=()
+ case "$phase" in
+ compile)
+ [ $# -eq 4 ] || die "compile phase expects opt project src_list sdk"
+ cmd=(bash "$SELF" --compile-one "$tool" "$1" "$2" "$run_dir" "$3" "$4")
+ ;;
+ archive)
+ [ $# -eq 1 ] || die "archive phase expects obj_list"
+ cmd=(bash "$SELF" --archive-one "$tool" "$run_dir" "$1")
+ ;;
+ link)
+ [ $# -eq 4 ] || die "link phase expects opt project fixture_dir sdk"
+ cmd=(bash "$SELF" --link-one "$tool" "$1" "$2" "$run_dir" "$3" "$4")
+ ;;
+ *) die "unknown phase '$phase'" ;;
+ esac
+ set +e
+ run_with_timeout "$TIMEOUT_S" "${cmd[@]}" \
+ > "$run_dir/time.txt" 2> "$run_dir/time.err"
+ local rc=$?
+ set -e
+
+ local elapsed
+ elapsed=$(awk '/^elapsed / {print $2}' "$run_dir/time.txt" | tail -1)
+ [ -n "$elapsed" ] || elapsed=0
+ printf '%s,%s\n' "$rc" "$elapsed"
+}
+
+record_best() {
+ local phase=$1
+ local tool=$2
+ local project=$3
+ local opt=$4
+ local row_dir=$5
+ shift 5
+
+ local best=""
+ local best_rc=125
+ local run
+ for run in $(seq 1 "$RUNS"); do
+ local run_dir="$row_dir/$phase-$tool-$run"
+ local got rc elapsed
+ got=$(timed_phase "$phase" "$tool" "$run_dir" "$@")
+ rc=${got%,*}
+ elapsed=${got#*,}
+ printf '%s,%s,%s,%s,%s,%s,%s\n' "$project" "$opt" "$phase" "$tool" \
+ "$run" "$rc" "$elapsed" >> "$OUT_ROOT/runs.csv"
+ if [ "$rc" -eq 0 ]; then
+ local ms
+ ms=$(awk -v s="$elapsed" 'BEGIN { printf "%.3f", s * 1000.0 }')
+ if [ -z "$best" ] || awk -v a="$ms" -v b="$best" 'BEGIN { exit !(a < b) }'; then
+ best=$ms
+ best_rc=0
+ fi
+ else
+ best_rc=$rc
+ fi
+ done
+
+ if [ -n "$best" ]; then
+ printf '%s\n' "$best"
+ elif [ "$best_rc" -eq 124 ]; then
+ printf 'timeout\n'
+ else
+ printf 'fail\n'
+ fi
+}
+
+best_compile_ms_for() {
+ local compiler=$1
+ local project=$2
+ local opt=$3
+ local src_list=$4
+ local sdk=$5
+ local row_dir=$6
+
+ if ! compiler_available "$compiler"; then
+ printf 'missing\n'
+ return 0
+ fi
+ record_best compile "$compiler" "$project" "$opt" "$row_dir" \
+ "$opt" "$project" "$src_list" "$sdk"
+}
+
+best_archive_ms_for() {
+ local archiver=$1
+ local project=$2
+ local opt=$3
+ local fixture_dir=$4
+ local row_dir=$5
+
+ if ! archiver_available "$archiver"; then
+ printf 'missing\n'
+ return 0
+ fi
+ [ -f "$fixture_dir/lib_objs" ] || { printf 'fail\n'; return 0; }
+ record_best archive "$archiver" "$project" "$opt" "$row_dir" \
+ "$fixture_dir/lib_objs"
+}
+
+best_link_ms_for() {
+ local compiler=$1
+ local project=$2
+ local opt=$3
+ local fixture_dir=$4
+ local sdk=$5
+ local row_dir=$6
+
+ if ! compiler_available "$compiler"; then
+ printf 'missing\n'
+ return 0
+ fi
+ [ -f "$fixture_dir/lib.a" ] || { printf 'fail\n'; return 0; }
+ record_best link "$compiler" "$project" "$opt" "$row_dir" \
+ "$opt" "$project" "$fixture_dir" "$sdk"
+}
+
+make_fixture() {
+ local compiler=$1
+ local project=$2
+ local opt=$3
+ local src_list=$4
+ local sdk=$5
+ local fixture_dir=$6
+
+ if ! compiler_available "$compiler"; then
+ return 1
+ fi
+ rm -rf "$fixture_dir"
+ mkdir -p "$fixture_dir"
+ local got rc
+ got=$(timed_phase compile "$compiler" "$fixture_dir" \
+ "$opt" "$project" "$src_list" "$sdk")
+ rc=${got%,*}
+ [ "$rc" -eq 0 ] || return 1
+
+ local archiver=host
+ [ "$compiler" = kit ] && archiver=kit
+ got=$(timed_phase archive "$archiver" "$fixture_dir/archive" \
+ "$fixture_dir/lib_objs")
+ rc=${got%,*}
+ [ "$rc" -eq 0 ] || return 1
+ mv "$fixture_dir/archive/lib.a" "$fixture_dir/lib.a"
+ return 0
+}
+
+prepare_project() {
+ local project=$1
+ local out_dir=$2
+ local src_list="$out_dir/srcs"
+ local lib_src_list="$out_dir/lib_srcs"
+ load_recipe "$project"
+ ECO_SRC="$(srcdir_for "$project")"
+ export ECO_SRC ECO_DIR
+ if [ -z "$ECO_SRC" ] || [ ! -d "$ECO_SRC" ]; then
+ return 1
+ fi
+ mkdir -p "$out_dir"
+ eco_lib_srcs > "$lib_src_list"
+ cp "$lib_src_list" "$src_list"
+ driver_for >> "$src_list"
+ return 0
+}
+
+print_toolchain_phase() {
+ local results=$1
+ local phase=$2
+ local units_label=$3
+
+ awk -F, -v phase_name="$phase" -v units_label="$units_label" \
+ -v opts_list="$OPTS" '
+ function isnum(x) { return x ~ /^[0-9]+([.][0-9]+)?$/ }
+ function cell(x) { return isnum(x) ? sprintf("%8.1f", x) : sprintf("%8s", x) }
+ function speed(ref, kit) {
+ return (isnum(ref) && isnum(kit) && kit > 0) ? sprintf("%7.3fx", ref / kit) : sprintf("%7s", "-")
+ }
+ function add_geo(tag, ref, kit) {
+ if (isnum(ref) && isnum(kit) && kit > 0 && ref > 0) {
+ geo_sum[tag] += log(ref / kit)
+ geo_n[tag]++
+ }
+ }
+ BEGIN {
+ nopts = split(opts_list, opts, /[ \t]+/)
+ printf "\n[%s]\n", phase_name
+ printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
+ "project", "opt", units_label, "kit_ms", "clang_ms", "k/clang", "tcc_ms", "k/tcc"
+ printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
+ "----------", "---", "----", "--------", "--------", "-------", "--------", "-------"
+ }
+ NR == 1 { next }
+ $1 == phase_name {
+ opt=$3; units=$4; kit=$5; clang=$6; tcc=$7
+ printf "%-10s %-3s %4s %s %s %s %s %s\n", \
+ $2, opt, units, cell(kit), cell(clang), speed(clang, kit), cell(tcc), speed(tcc, kit)
+ add_geo(opt ":clang", clang, kit)
+ add_geo(opt ":tcc", tcc, kit)
+ add_geo("all:clang", clang, kit)
+ add_geo("all:tcc", tcc, kit)
+ }
+ END {
+ printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
+ "----------", "---", "----", "--------", "--------", "-------", "--------", "-------"
+ for (i = 1; i <= nopts; ++i) {
+ opt = opts[i]
+ kc = opt ":clang"
+ kt = opt ":tcc"
+ cnt_clang = geo_n[kc] + 0
+ cnt_tcc = geo_n[kt] + 0
+ clang_geo = geo_n[kc] ? sprintf("%7.3fx", exp(geo_sum[kc] / geo_n[kc])) : sprintf("%7s", "-")
+ tcc_geo = geo_n[kt] ? sprintf("%7.3fx", exp(geo_sum[kt] / geo_n[kt])) : sprintf("%7s", "-")
+ printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
+ "geomean", opt, cnt_clang "/" cnt_tcc, "", "", clang_geo, "", tcc_geo
+ }
+ cnt_clang = geo_n["all:clang"] + 0
+ cnt_tcc = geo_n["all:tcc"] + 0
+ clang_geo = geo_n["all:clang"] ? sprintf("%7.3fx", exp(geo_sum["all:clang"] / geo_n["all:clang"])) : sprintf("%7s", "-")
+ tcc_geo = geo_n["all:tcc"] ? sprintf("%7.3fx", exp(geo_sum["all:tcc"] / geo_n["all:tcc"])) : sprintf("%7s", "-")
+ printf "%-10s %-3s %4s %8s %8s %7s %8s %7s\n", \
+ "geomean", "all", cnt_clang "/" cnt_tcc, "", "", clang_geo, "", tcc_geo
+ }
+ ' "$results"
+}
+
+print_archive_phase() {
+ local results=$1
+
+ awk -F, -v opts_list="$OPTS" '
+ function isnum(x) { return x ~ /^[0-9]+([.][0-9]+)?$/ }
+ function cell(x) { return isnum(x) ? sprintf("%8.1f", x) : sprintf("%8s", x) }
+ function speed(ref, kit) {
+ return (isnum(ref) && isnum(kit) && kit > 0) ? sprintf("%7.3fx", ref / kit) : sprintf("%7s", "-")
+ }
+ function add_geo(tag, ref, kit) {
+ if (isnum(ref) && isnum(kit) && kit > 0 && ref > 0) {
+ geo_sum[tag] += log(ref / kit)
+ geo_n[tag]++
+ }
+ }
+ BEGIN {
+ nopts = split(opts_list, opts, /[ \t]+/)
+ printf "\n[archive]\n"
+ printf "%-10s %-3s %4s %8s %8s %7s\n", \
+ "project", "opt", "objs", "kit_ar", "host_ar", "k/host"
+ printf "%-10s %-3s %4s %8s %8s %7s\n", \
+ "----------", "---", "----", "--------", "--------", "-------"
+ }
+ NR == 1 { next }
+ $1 == "archive" {
+ opt=$3; units=$4; kit=$5; host=$8
+ printf "%-10s %-3s %4s %s %s %s\n", \
+ $2, opt, units, cell(kit), cell(host), speed(host, kit)
+ add_geo(opt ":host", host, kit)
+ add_geo("all:host", host, kit)
+ }
+ END {
+ printf "%-10s %-3s %4s %8s %8s %7s\n", \
+ "----------", "---", "----", "--------", "--------", "-------"
+ for (i = 1; i <= nopts; ++i) {
+ opt = opts[i]
+ key = opt ":host"
+ host_geo = geo_n[key] ? sprintf("%7.3fx", exp(geo_sum[key] / geo_n[key])) : sprintf("%7s", "-")
+ printf "%-10s %-3s %4s %8s %8s %7s\n", \
+ "geomean", opt, geo_n[key], "", "", host_geo
+ }
+ host_geo = geo_n["all:host"] ? sprintf("%7.3fx", exp(geo_sum["all:host"] / geo_n["all:host"])) : sprintf("%7s", "-")
+ printf "%-10s %-3s %4s %8s %8s %7s\n", \
+ "geomean", "all", geo_n["all:host"], "", "", host_geo
+ }
+ ' "$results"
+}
+
+print_report() {
+ local results=$1
+ printf 'ecosystem speed: best-of-%s, timeout=%ss\n' "$RUNS" "$TIMEOUT_S"
+ printf 'kit=%s\n' "$KIT"
+ printf 'clang=%s\n' "$CLANG"
+ if compiler_available tcc; then
+ printf 'tcc=%s\n' "$TCC"
+ else
+ printf 'tcc=%s (missing; rows skipped)\n' "$TCC"
+ fi
+ printf 'host_ar=%s\n' "$HOST_AR"
+ printf 'archive_input=kit-compiled library objects\n'
+ print_toolchain_phase "$results" compile srcs
+ print_archive_phase "$results"
+ print_toolchain_phase "$results" link apps
+ printf '\nlogs=%s\n' "$OUT_ROOT"
+}
+
+main() {
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
+ usage
+ exit 0
+ fi
+ case "${1:-}" in
+ --compile-one)
+ shift
+ [ $# -eq 6 ] || die "--compile-one expects compiler opt project out_dir src_list sdk"
+ compile_one "$@"
+ exit $?
+ ;;
+ --archive-one)
+ shift
+ [ $# -eq 3 ] || die "--archive-one expects archiver out_dir obj_list"
+ archive_one "$@"
+ exit $?
+ ;;
+ --link-one)
+ shift
+ [ $# -eq 6 ] || die "--link-one expects compiler opt project out_dir fixture_dir sdk"
+ link_one "$@"
+ exit $?
+ ;;
+ esac
+
+ [ -x "$KIT" ] || die "kit binary not found at $KIT (run: make RELEASE=1 bin)"
+ command -v "$CLANG" >/dev/null 2>&1 || die "clang not found: $CLANG"
+ command -v "$HOST_AR" >/dev/null 2>&1 || die "host ar not found: $HOST_AR"
+
+ mkdir -p "$OUT_ROOT"
+ printf 'project,opt,phase,tool,run,rc,seconds\n' > "$OUT_ROOT/runs.csv"
+ local results="$OUT_ROOT/results.csv"
+ printf 'phase,project,opt,units,kit_ms,clang_ms,tcc_ms,host_ms\n' > "$results"
+
+ local sdk
+ sdk="$(host_sdk)"
+
+ local projects
+ if [ "$#" -gt 0 ]; then
+ projects="$*"
+ elif [ -n "${ECO_SPEED_PROJECTS:-}" ]; then
+ projects="$ECO_SPEED_PROJECTS"
+ else
+ projects="$("$ECOSYS" list)"
+ fi
+
+ local any=0
+ local project opt
+ for project in $projects; do
+ local project_dir="$OUT_ROOT/$project"
+ if ! prepare_project "$project" "$project_dir"; then
+ printf 'ecosystem-speed: skip %-10s not provisioned (run: make provision-ecosystem)\n' "$project" >&2
+ continue
+ fi
+ any=1
+
+ local src_list="$project_dir/srcs"
+ local lib_src_list="$project_dir/lib_srcs"
+ local srcs lib_objs
+ srcs=$(awk 'NF {n++} END {print n + 0}' "$src_list")
+ lib_objs=$(awk 'NF {n++} END {print n + 0}' "$lib_src_list")
+
+ for opt in $OPTS; do
+ local row_dir="$project_dir/$opt"
+ mkdir -p "$row_dir"
+
+ local kit_compile clang_compile tcc_compile
+ kit_compile=$(best_compile_ms_for kit "$project" "$opt" "$src_list" "$sdk" "$row_dir")
+ clang_compile=$(best_compile_ms_for clang "$project" "$opt" "$src_list" "$sdk" "$row_dir")
+ tcc_compile=$(best_compile_ms_for tcc "$project" "$opt" "$src_list" "$sdk" "$row_dir")
+ printf 'compile,%s,%s,%s,%s,%s,%s,\n' "$project" "$opt" "$srcs" \
+ "$kit_compile" "$clang_compile" "$tcc_compile" >> "$results"
+
+ local kit_fixture="$row_dir/fixture-kit"
+ local clang_fixture="$row_dir/fixture-clang"
+ local tcc_fixture="$row_dir/fixture-tcc"
+ make_fixture kit "$project" "$opt" "$src_list" "$sdk" "$kit_fixture" || true
+ make_fixture clang "$project" "$opt" "$src_list" "$sdk" "$clang_fixture" || true
+ make_fixture tcc "$project" "$opt" "$src_list" "$sdk" "$tcc_fixture" || true
+
+ local kit_ar host_ar
+ kit_ar=$(best_archive_ms_for kit "$project" "$opt" "$kit_fixture" "$row_dir")
+ host_ar=$(best_archive_ms_for host "$project" "$opt" "$kit_fixture" "$row_dir")
+ printf 'archive,%s,%s,%s,%s,,,%s\n' "$project" "$opt" "$lib_objs" \
+ "$kit_ar" "$host_ar" >> "$results"
+
+ local kit_link clang_link tcc_link
+ kit_link=$(best_link_ms_for kit "$project" "$opt" "$kit_fixture" "$sdk" "$row_dir")
+ clang_link=$(best_link_ms_for clang "$project" "$opt" "$clang_fixture" "$sdk" "$row_dir")
+ tcc_link=$(best_link_ms_for tcc "$project" "$opt" "$tcc_fixture" "$sdk" "$row_dir")
+ printf 'link,%s,%s,1,%s,%s,%s,\n' "$project" "$opt" \
+ "$kit_link" "$clang_link" "$tcc_link" >> "$results"
+ done
+ done
+
+ [ "$any" = 1 ] || die "no provisioned ecosystem projects found"
+ print_report "$results"
+}
+
+main "$@"