chore: import upstream snapshot with attribution
This commit is contained in:
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fold changelog.d/ fragments into CHANGELOG.md's Unreleased section.
|
||||
#
|
||||
# scripts/changelog-merge.sh
|
||||
#
|
||||
# Each changelog.d/<slug>.md fragment is tagged on its first line
|
||||
# (feature: / improvement: / fix: — see changelog.d/README.md). Bullets are
|
||||
# appended to the END of the matching "### ..." section under
|
||||
# "## Unreleased", preserving everything already there. Missing sections
|
||||
# are created in canonical order (New Features, Improvements, Bug Fixes)
|
||||
# before "### Contributors"; a missing "## Unreleased" block is created
|
||||
# above the newest release heading. Merged fragments are deleted;
|
||||
# changelog.d/README.md is never touched.
|
||||
#
|
||||
# Fails loudly on unknown tags or malformed fragments — a silently dropped
|
||||
# changelog entry is worse than a broken merge.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo_root" || exit 1
|
||||
|
||||
changelog="CHANGELOG.md"
|
||||
fragments_dir="changelog.d"
|
||||
|
||||
[ -f "$changelog" ] || { echo "changelog-merge: $changelog not found" >&2; exit 1; }
|
||||
[ -d "$fragments_dir" ] || { echo "changelog-merge: $fragments_dir/ not found" >&2; exit 1; }
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
: > "$tmp_dir/features"
|
||||
: > "$tmp_dir/improvements"
|
||||
: > "$tmp_dir/fixes"
|
||||
|
||||
section_file_for_tag() {
|
||||
case "$1" in
|
||||
feature) echo "$tmp_dir/features" ;;
|
||||
improvement) echo "$tmp_dir/improvements" ;;
|
||||
fix) echo "$tmp_dir/fixes" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
merged_fragments=()
|
||||
for fragment in "$fragments_dir"/*.md; do
|
||||
[ -e "$fragment" ] || continue
|
||||
[ "$(basename "$fragment")" = "README.md" ] && continue
|
||||
|
||||
first_line=""
|
||||
rest_started=false
|
||||
out=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [ -z "$first_line" ] && ! $rest_started; then
|
||||
[ -z "$line" ] && continue
|
||||
first_line="$line"
|
||||
continue
|
||||
fi
|
||||
rest_started=true
|
||||
[ -z "$line" ] && continue
|
||||
case "$line" in
|
||||
"- "*|" "*|$'\t'*) out="$out$line"$'\n' ;;
|
||||
*) out="$out- $line"$'\n' ;;
|
||||
esac
|
||||
done < "$fragment"
|
||||
|
||||
tag="${first_line%%:*}"
|
||||
body="${first_line#*:}"
|
||||
body="${body# }"
|
||||
section_file="$(section_file_for_tag "$tag")" || {
|
||||
echo "changelog-merge: $fragment: unknown tag '${tag}:' (expected feature:, improvement:, or fix:)" >&2
|
||||
exit 1
|
||||
}
|
||||
if [ "$first_line" = "$tag" ] || [ -z "$body" ]; then
|
||||
echo "changelog-merge: $fragment: first line must be '<tag>: <bullet text>'" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$body" in
|
||||
"- "*) printf '%s\n' "$body" >> "$section_file" ;;
|
||||
*) printf -- '- %s\n' "$body" >> "$section_file" ;;
|
||||
esac
|
||||
[ -n "$out" ] && printf '%s' "$out" >> "$section_file"
|
||||
merged_fragments+=("$fragment")
|
||||
done
|
||||
|
||||
if [ "${#merged_fragments[@]}" -eq 0 ]; then
|
||||
echo "changelog-merge: no fragments to merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
awk -v features_file="$tmp_dir/features" \
|
||||
-v improvements_file="$tmp_dir/improvements" \
|
||||
-v fixes_file="$tmp_dir/fixes" '
|
||||
function load(file, line, out) {
|
||||
out = ""
|
||||
while ((getline line < file) > 0) out = out line "\n"
|
||||
close(file)
|
||||
return out
|
||||
}
|
||||
function emit_pending_blanks( i) {
|
||||
for (i = 0; i < blanks; i++) print ""
|
||||
blanks = 0
|
||||
}
|
||||
# Append the current section pending bullets at the end of its existing
|
||||
# content (held blank lines are emitted afterwards, so spacing before the
|
||||
# next heading is preserved).
|
||||
function close_section() {
|
||||
if (cur != "" && pending[cur] != "") {
|
||||
printf "%s", pending[cur]
|
||||
pending[cur] = ""
|
||||
}
|
||||
cur = ""
|
||||
}
|
||||
# Emit any sections that had no heading yet, in canonical order.
|
||||
function emit_missing_sections( i, name) {
|
||||
for (i = 1; i <= 3; i++) {
|
||||
name = order[i]
|
||||
if (pending[name] != "") {
|
||||
print "### " name
|
||||
print ""
|
||||
printf "%s", pending[name]
|
||||
print ""
|
||||
pending[name] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BEGIN {
|
||||
pending["New Features"] = load(features_file)
|
||||
pending["Improvements"] = load(improvements_file)
|
||||
pending["Bug Fixes"] = load(fixes_file)
|
||||
order[1] = "New Features"; order[2] = "Improvements"; order[3] = "Bug Fixes"
|
||||
in_unreleased = 0
|
||||
seen_unreleased = 0
|
||||
cur = ""
|
||||
blanks = 0
|
||||
}
|
||||
/^## / {
|
||||
if (in_unreleased) {
|
||||
close_section()
|
||||
emit_pending_blanks()
|
||||
emit_missing_sections()
|
||||
in_unreleased = 0
|
||||
} else if (!seen_unreleased && $0 != "## Unreleased") {
|
||||
# No Unreleased block exists; create one above the newest release.
|
||||
emit_pending_blanks()
|
||||
print "## Unreleased"
|
||||
print ""
|
||||
emit_missing_sections()
|
||||
seen_unreleased = 1
|
||||
} else {
|
||||
emit_pending_blanks()
|
||||
}
|
||||
if ($0 == "## Unreleased") { in_unreleased = 1; seen_unreleased = 1 }
|
||||
print
|
||||
next
|
||||
}
|
||||
/^### / {
|
||||
if (in_unreleased) {
|
||||
close_section()
|
||||
emit_pending_blanks()
|
||||
if ($0 == "### Contributors") emit_missing_sections()
|
||||
sub(/^### /, "")
|
||||
if ($0 in pending) cur = $0
|
||||
print "### " $0
|
||||
next
|
||||
}
|
||||
emit_pending_blanks()
|
||||
print
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*$/ { blanks++; next }
|
||||
{
|
||||
emit_pending_blanks()
|
||||
print
|
||||
next
|
||||
}
|
||||
END {
|
||||
if (in_unreleased) {
|
||||
close_section()
|
||||
emit_missing_sections()
|
||||
} else if (!seen_unreleased) {
|
||||
emit_pending_blanks()
|
||||
print "## Unreleased"
|
||||
print ""
|
||||
emit_missing_sections()
|
||||
blanks = 0
|
||||
}
|
||||
emit_pending_blanks()
|
||||
}
|
||||
' "$changelog" > "$tmp_dir/changelog.new" || { echo "changelog-merge: awk pass failed" >&2; exit 1; }
|
||||
|
||||
mv "$tmp_dir/changelog.new" "$changelog"
|
||||
rm -f "${merged_fragments[@]}"
|
||||
echo "changelog-merge: merged ${#merged_fragments[@]} fragment(s) into $changelog"
|
||||
for fragment in "${merged_fragments[@]}"; do
|
||||
echo " - $fragment"
|
||||
done
|
||||
Executable
+364
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tiered local gate for the Native SDK.
|
||||
#
|
||||
# scripts/gate.sh fast [base-ref] # affected-only: what your diff touches
|
||||
# scripts/gate.sh full [base-ref] [--all] [--perf] # everything CI-shaped that runs locally
|
||||
#
|
||||
# fast — root `zig build test` + `zig build validate`, plus the suites for
|
||||
# the examples AFFECTED by your diff against base-ref (default: main). The
|
||||
# diff is `git diff --name-only` against `git merge-base base-ref HEAD`,
|
||||
# plus untracked files, so uncommitted work counts. Framework diffs on
|
||||
# macOS additionally run the render-benchmark budget ratchet (see below).
|
||||
#
|
||||
# Path -> step mapping (fast tier):
|
||||
# src/**, build.zig, build.zig.zon, build/**,
|
||||
# tools/**, tests/**, assets/** -> framework change: root suites
|
||||
# + ALL example suites
|
||||
# (frontends, native, mobile)
|
||||
# src/platform/macos/** -> additionally cef-host-link:
|
||||
# build the WebView example
|
||||
# with -Dweb-engine=chromium so
|
||||
# cef_host.mm actually compiles
|
||||
# and links (macOS only; skipped
|
||||
# with a loud warning when the
|
||||
# CEF layout is absent — prime
|
||||
# with `zig build &&
|
||||
# ./zig-out/bin/native cef install`)
|
||||
# examples/<name>/** -> that example's suite only
|
||||
# (test-example-<name>; the
|
||||
# mobile projects map to
|
||||
# test-examples-mobile; hello/
|
||||
# webview/browser run their
|
||||
# in-dir `zig build test`)
|
||||
# docs/** -> docs `pnpm check`
|
||||
# anything else (README, .github, packages,
|
||||
# scripts, skills, changelog.d, ...) -> root suites only
|
||||
# A docs-ONLY diff runs only the docs check. The docs check is path-gated
|
||||
# in both tiers: it never runs unless docs/ changed (or --all in full).
|
||||
#
|
||||
# full — root test + validate, every example suite (frontends, native incl.
|
||||
# canvas-preview, mobile), the Chromium host link check (cef-host-link),
|
||||
# the macOS automation smokes (gpu-surface,
|
||||
# gpu-dashboard, gpu-components, canvas-preview, writeback; skipped off-macOS), a
|
||||
# markup check over every example markup file, and the docs check if docs/ changed
|
||||
# vs base-ref or --all was passed. --perf additionally runs the percentile
|
||||
# GPU perf check (test-gpu-dashboard-perf; macOS only, slow, load-sensitive —
|
||||
# opt-in so a busy dev box doesn't fail the gate on noise).
|
||||
#
|
||||
# bench-check — the render-benchmark budget ratchet
|
||||
# (`zig build bench-render -Doptimize=ReleaseFast -- --check
|
||||
# tools/bench-render-budgets.txt`): median e2e p50 of three deterministic
|
||||
# suite passes per scenario vs committed budgets with ~1.3x+ headroom, so
|
||||
# it trips on order-of-magnitude regressions (a reintroduced O(n^2)
|
||||
# planner scan, an extra emission per input), not machine noise. Unlike
|
||||
# the --perf harness it is headless, in-process, and seconds-long once the
|
||||
# ReleaseFast build is cached — that is why it runs by DEFAULT (fast tier:
|
||||
# framework diffs only, since engine perf cannot regress from an
|
||||
# example-only or docs diff; full tier: always) instead of opt-in like
|
||||
# --perf: a ratchet that only runs when someone remembers to ask is not a
|
||||
# ratchet. macOS-only because the budgets are calibrated on Apple Silicon.
|
||||
# Set NATIVE_SDK_SKIP_BENCH_CHECK=1 to skip it on a box that is too busy
|
||||
# even for the generous budgets.
|
||||
#
|
||||
# Deliberately NOT `set -e`: every step runs even after a failure so the
|
||||
# summary shows the whole picture; the exit code is non-zero if any step
|
||||
# failed. Step output streams through; each step is timed.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo_root" || exit 1
|
||||
|
||||
usage() {
|
||||
echo "usage: scripts/gate.sh <fast|full> [base-ref] [--all] [--perf]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
tier="${1:-}"
|
||||
case "$tier" in fast|full) ;; *) usage ;; esac
|
||||
shift
|
||||
|
||||
base_ref="main"
|
||||
run_all=false
|
||||
run_perf=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--all) run_all=true ;;
|
||||
--perf) run_perf=true ;;
|
||||
-*) usage ;;
|
||||
*) base_ref="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- diff classification --------------------------------------------------
|
||||
|
||||
base_commit="$(git merge-base "$base_ref" HEAD 2>/dev/null)"
|
||||
if [ -z "$base_commit" ]; then
|
||||
echo "gate: cannot resolve merge-base of '$base_ref' and HEAD" >&2
|
||||
exit 2
|
||||
fi
|
||||
changed_files="$( (git diff --name-only "$base_commit"; git ls-files --others --exclude-standard) | sort -u)"
|
||||
|
||||
framework_changed=false
|
||||
macos_platform_changed=false
|
||||
docs_changed=false
|
||||
meta_changed=false
|
||||
affected_examples="" # space-separated example dir names
|
||||
mobile_affected=false
|
||||
|
||||
# Examples with a root `test-example-<name>` step, derived from build.zig's
|
||||
# `addExampleTestStep(...)` registrations so this script is never a second
|
||||
# registry that drifts (it did, twice in one day — soundboard/calculator/
|
||||
# notes were silently skipped). Layout/file-contains checks register other
|
||||
# `test-example-*` names via different helpers and are deliberately not
|
||||
# example suites, so the match is keyed on the helper name.
|
||||
registered_examples="$(sed -n 's/.*addExampleTestStep([^"]*"test-example-\([A-Za-z0-9-]*\)".*/\1/p' build.zig | sort -u | tr '\n' ' ')"
|
||||
if [ -z "$registered_examples" ]; then
|
||||
echo "gate: derived no registered examples from build.zig (addExampleTestStep pattern matched nothing — helper renamed?)" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Examples whose suite is their own in-dir `zig build test`.
|
||||
indir_examples="browser hello webview"
|
||||
# Mobile example projects, covered as a group by test-examples-mobile.
|
||||
mobile_examples="android ios mobile-canvas mobile-shell"
|
||||
|
||||
note_example() {
|
||||
case " $affected_examples " in *" $1 "*) ;; *) affected_examples="$affected_examples $1" ;; esac
|
||||
}
|
||||
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] || continue
|
||||
case "$file" in
|
||||
docs/*) docs_changed=true ;;
|
||||
src/platform/macos/*) framework_changed=true; macos_platform_changed=true ;;
|
||||
src/*|build.zig|build.zig.zon|build/*|tools/*|tests/*|assets/*) framework_changed=true ;;
|
||||
examples/*/*)
|
||||
example="${file#examples/}"
|
||||
example="${example%%/*}"
|
||||
case " $mobile_examples " in
|
||||
*" $example "*) mobile_affected=true ;;
|
||||
*) note_example "$example" ;;
|
||||
esac
|
||||
;;
|
||||
*) meta_changed=true ;;
|
||||
esac
|
||||
done <<EOF_FILES
|
||||
$changed_files
|
||||
EOF_FILES
|
||||
|
||||
# ---- step machinery -------------------------------------------------------
|
||||
|
||||
step_names=""
|
||||
step_status=""
|
||||
step_secs=""
|
||||
failures=0
|
||||
gate_start=$(date +%s)
|
||||
|
||||
record() { # name status seconds
|
||||
step_names="$step_names$1|"
|
||||
step_status="$step_status$2|"
|
||||
step_secs="$step_secs$3|"
|
||||
}
|
||||
|
||||
run_step() { # name command...
|
||||
name="$1"; shift
|
||||
echo ""
|
||||
echo "==> $name: $*"
|
||||
start=$(date +%s)
|
||||
"$@"
|
||||
rc=$?
|
||||
secs=$(( $(date +%s) - start ))
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
record "$name" PASS "$secs"
|
||||
else
|
||||
record "$name" FAIL "$secs"
|
||||
failures=$((failures + 1))
|
||||
echo "==> $name FAILED (exit $rc)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
skip_step() { # name reason
|
||||
echo ""
|
||||
echo "==> $1: skipped ($2)"
|
||||
record "$1" SKIP 0
|
||||
}
|
||||
|
||||
docs_check() {
|
||||
run_step "docs-install" pnpm --dir docs install --frozen-lockfile
|
||||
# Build into a gate-owned dist dir so the check never corrupts a running
|
||||
# dev server's .next cache.
|
||||
run_step "docs-check" env NEXT_DIST_DIR=.next-gate pnpm --dir docs check
|
||||
}
|
||||
|
||||
is_macos=false
|
||||
[ "$(uname -s)" = "Darwin" ] && is_macos=true
|
||||
|
||||
bench_check() {
|
||||
zig build bench-render -Doptimize=ReleaseFast -- --check "$repo_root/tools/bench-render-budgets.txt"
|
||||
}
|
||||
|
||||
run_bench_check_step() { # runs (or explains skipping) the render-benchmark ratchet
|
||||
if ! $is_macos; then
|
||||
skip_step "bench-check" "budgets calibrated on Apple Silicon macOS"
|
||||
elif [ -n "${NATIVE_SDK_SKIP_BENCH_CHECK:-}" ]; then
|
||||
skip_step "bench-check" "NATIVE_SDK_SKIP_BENCH_CHECK set"
|
||||
else
|
||||
run_step "bench-check" bench_check
|
||||
fi
|
||||
}
|
||||
|
||||
# The Chromium (CEF) host, src/platform/macos/cef_host.mm, is only compiled
|
||||
# by Chromium-engine app builds — never by `zig build test`/`validate` — so
|
||||
# without this step an edit that keeps it merely grep-identical to the
|
||||
# AppKit host can land without ever seeing a compiler. test-webview-cef-link
|
||||
# builds (and links) the WebView example against the host for real.
|
||||
cef_layout_ready() {
|
||||
[ -f third_party/cef/macos/include/cef_app.h ] &&
|
||||
[ -d "third_party/cef/macos/Release/Chromium Embedded Framework.framework" ] &&
|
||||
[ -f third_party/cef/macos/libcef_dll_wrapper/libcef_dll_wrapper.a ]
|
||||
}
|
||||
|
||||
cef_host_step() {
|
||||
if ! $is_macos; then
|
||||
skip_step "cef-host-link" "macOS only"
|
||||
return
|
||||
fi
|
||||
if ! cef_layout_ready; then
|
||||
echo "" >&2
|
||||
echo "==> cef-host-link: WARNING — src/platform/macos changed but the CEF layout is absent," >&2
|
||||
echo " so the Chromium host was NOT compiled. Blind cef_host.mm edits will not be caught." >&2
|
||||
echo " Prime it once (downloads the prepared CEF runtime into third_party/cef/macos):" >&2
|
||||
echo " zig build && ./zig-out/bin/native cef install" >&2
|
||||
skip_step "cef-host-link" "CEF layout absent at third_party/cef/macos (see warning)"
|
||||
return
|
||||
fi
|
||||
run_step "cef-host-link" zig build test-webview-cef-link
|
||||
}
|
||||
|
||||
# ---- tiers ----------------------------------------------------------------
|
||||
|
||||
if [ "$tier" = "fast" ]; then
|
||||
non_docs_change=false
|
||||
{ $framework_changed || $meta_changed || $mobile_affected || [ -n "$affected_examples" ]; } && non_docs_change=true
|
||||
|
||||
if $non_docs_change || ! $docs_changed; then
|
||||
# Root suites run for any non-docs diff, and also for an empty diff
|
||||
# (a clean tree still deserves a baseline check).
|
||||
run_step "zig-test" zig build test
|
||||
run_step "zig-validate" zig build validate
|
||||
else
|
||||
skip_step "zig-test" "docs-only diff"
|
||||
skip_step "zig-validate" "docs-only diff"
|
||||
fi
|
||||
|
||||
if $framework_changed; then
|
||||
run_step "examples-frontends" zig build test-examples-frontends
|
||||
run_step "examples-native" zig build test-examples-native
|
||||
run_step "examples-mobile" zig build test-examples-mobile
|
||||
# Engine perf can only regress through framework code, so the ratchet
|
||||
# rides the same trigger as the full example sweep.
|
||||
run_bench_check_step
|
||||
else
|
||||
for example in $affected_examples; do
|
||||
case " $registered_examples " in
|
||||
*" $example "*) run_step "example-$example" zig build "test-example-$example" ;;
|
||||
*)
|
||||
case " $indir_examples " in
|
||||
*" $example "*) run_step "example-$example" sh -c "cd 'examples/$example' && zig build test -Dplatform=null" ;;
|
||||
*) skip_step "example-$example" "no test suite registered for examples/$example" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
$mobile_affected && run_step "examples-mobile" zig build test-examples-mobile
|
||||
fi
|
||||
|
||||
if $macos_platform_changed; then
|
||||
cef_host_step
|
||||
else
|
||||
skip_step "cef-host-link" "src/platform/macos unchanged vs $base_ref"
|
||||
fi
|
||||
|
||||
if $docs_changed; then
|
||||
docs_check
|
||||
else
|
||||
skip_step "docs-check" "docs/ unchanged vs $base_ref"
|
||||
fi
|
||||
else # full
|
||||
run_step "zig-test" zig build test
|
||||
run_step "zig-validate" zig build validate
|
||||
run_step "examples-frontends" zig build test-examples-frontends
|
||||
run_step "examples-native" zig build test-examples-native
|
||||
run_step "examples-mobile" zig build test-examples-mobile
|
||||
|
||||
cef_host_step
|
||||
|
||||
if $is_macos; then
|
||||
run_step "smoke-gpu-surface" zig build test-gpu-surface-smoke
|
||||
run_step "smoke-gpu-dashboard" zig build test-gpu-dashboard-smoke
|
||||
run_step "smoke-gpu-components" zig build test-gpu-components-smoke
|
||||
run_step "smoke-webview" zig build test-webview-smoke
|
||||
run_step "smoke-native-shell" zig build test-native-shell-smoke
|
||||
run_step "smoke-canvas-preview" zig build test-canvas-preview-smoke
|
||||
run_step "smoke-writeback" zig build test-writeback-smoke
|
||||
else
|
||||
skip_step "smoke-gpu-surface" "macOS only"
|
||||
skip_step "smoke-gpu-dashboard" "macOS only"
|
||||
skip_step "smoke-gpu-components" "macOS only"
|
||||
skip_step "smoke-webview" "macOS only"
|
||||
skip_step "smoke-native-shell" "macOS only"
|
||||
skip_step "smoke-canvas-preview" "macOS only"
|
||||
skip_step "smoke-writeback" "macOS only"
|
||||
fi
|
||||
|
||||
run_bench_check_step
|
||||
|
||||
if $run_perf; then
|
||||
if $is_macos; then
|
||||
run_step "perf-gpu-dashboard" zig build test-gpu-dashboard-perf
|
||||
else
|
||||
skip_step "perf-gpu-dashboard" "macOS only"
|
||||
fi
|
||||
else
|
||||
skip_step "perf-gpu-dashboard" "opt-in: pass --perf (slow, load-sensitive)"
|
||||
fi
|
||||
|
||||
markup_check() {
|
||||
zig build || return 1
|
||||
# shellcheck disable=SC2046
|
||||
# -type f: zero-config app builds leave a .native/ cache DIRECTORY in
|
||||
# example dirs, which the name pattern would otherwise match.
|
||||
./zig-out/bin/native markup check $(find examples -name '*.native' -type f | sort)
|
||||
}
|
||||
run_step "markup-check" markup_check
|
||||
|
||||
if $docs_changed || $run_all; then
|
||||
docs_check
|
||||
else
|
||||
skip_step "docs-check" "docs/ unchanged vs $base_ref (pass --all to force)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- summary --------------------------------------------------------------
|
||||
|
||||
total_secs=$(( $(date +%s) - gate_start ))
|
||||
echo ""
|
||||
echo "==================== gate $tier summary (base: $base_ref) ===================="
|
||||
old_ifs="$IFS"; IFS='|'
|
||||
set -- $step_names
|
||||
names=("$@")
|
||||
set -- $step_status
|
||||
statuses=("$@")
|
||||
set -- $step_secs
|
||||
secs=("$@")
|
||||
IFS="$old_ifs"
|
||||
i=0
|
||||
while [ "$i" -lt "${#names[@]}" ]; do
|
||||
printf ' %-22s %-4s %4ss\n' "${names[$i]}" "${statuses[$i]}" "${secs[$i]}"
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf ' %-22s %-4s %4ss\n' "total" "$([ "$failures" -eq 0 ] && echo PASS || echo FAIL)" "$total_secs"
|
||||
if [ "$failures" -gt 0 ]; then
|
||||
echo "gate $tier: $failures step(s) failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "gate $tier: all steps green"
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/bin/sh
|
||||
# Percentile GPU performance check for examples/gpu-dashboard.
|
||||
#
|
||||
# The single-launch smoke (test-gpu-dashboard-smoke) proves correctness with one
|
||||
# load-tolerant latency sample; this harness measures a distribution:
|
||||
#
|
||||
# 1. Cold start: launches the automation build N times (NATIVE_SDK_PERF_LAUNCHES,
|
||||
# default 5), recording gpu_first_frame_latency_ns from the automation
|
||||
# snapshot for each launch.
|
||||
# 2. Steady state: on the final (warm) launch, drives M "Auto refresh" switch
|
||||
# clicks (NATIVE_SDK_PERF_INTERACTIONS, default 5), recording gpu_input_latency_ns
|
||||
# (input receipt -> first present) per interaction.
|
||||
# 3. Prints every sample plus min/median/p90/max per series, and asserts each
|
||||
# series' p90 under its budget:
|
||||
# NATIVE_SDK_PERF_BUDGET_MS first-frame p90 budget, default 300
|
||||
# NATIVE_SDK_PERF_INPUT_BUDGET_MS input-latency p90 budget, default 100
|
||||
# Defaults are deliberately generous (2x+ the smoke's 150 ms first-frame
|
||||
# budget): this check exists to catch step-function regressions on shared
|
||||
# CI/dev machines, not 10% drift on an idle box.
|
||||
#
|
||||
# p90 is nearest-rank: sorted[ceil(0.9 * n)]. At the default n=5 that IS the
|
||||
# maximum of the five samples — with five samples the nearest-rank 90th
|
||||
# percentile estimator selects the max, so "max of 5 under a generous budget"
|
||||
# is not a shortcut but the estimator itself; raising NATIVE_SDK_PERF_LAUNCHES past 9
|
||||
# starts discarding the worst outlier as expected.
|
||||
#
|
||||
# Usage: scripts/perf-gpu-dashboard.sh <native-sdk-cli>
|
||||
# Expects examples/gpu-dashboard already built with -Dautomation=true
|
||||
# (`zig build test-gpu-dashboard-perf` wires the build + this script).
|
||||
set -eu
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cli="${1:?usage: scripts/perf-gpu-dashboard.sh <native-sdk-cli>}"
|
||||
case "$cli" in /*) ;; *) cli="$repo_root/$cli" ;; esac
|
||||
cd "$repo_root/examples/gpu-dashboard"
|
||||
app="zig-out/bin/gpu-dashboard"
|
||||
[ -x "$app" ] || { echo "perf: $app is missing; build examples/gpu-dashboard with -Dautomation=true first" >&2; exit 1; }
|
||||
automation_dir=".zig-cache/native-sdk-automation"
|
||||
mkdir -p "$automation_dir"
|
||||
log=".zig-cache/native-sdk-gpu-dashboard-perf.log"
|
||||
|
||||
require_positive_int() { # name value
|
||||
case "$2" in ''|*[!0-9]*) echo "perf: $1 must be a positive integer: $2" >&2; exit 1 ;; esac
|
||||
[ "$2" -gt 0 ] || { echo "perf: $1 must be a positive integer: $2" >&2; exit 1; }
|
||||
}
|
||||
|
||||
launches="${NATIVE_SDK_PERF_LAUNCHES:-5}"
|
||||
interactions="${NATIVE_SDK_PERF_INTERACTIONS:-5}"
|
||||
first_frame_budget_ms="${NATIVE_SDK_PERF_BUDGET_MS:-300}"
|
||||
input_budget_ms="${NATIVE_SDK_PERF_INPUT_BUDGET_MS:-100}"
|
||||
require_positive_int NATIVE_SDK_PERF_LAUNCHES "$launches"
|
||||
require_positive_int NATIVE_SDK_PERF_INTERACTIONS "$interactions"
|
||||
require_positive_int NATIVE_SDK_PERF_BUDGET_MS "$first_frame_budget_ms"
|
||||
require_positive_int NATIVE_SDK_PERF_INPUT_BUDGET_MS "$input_budget_ms"
|
||||
first_frame_budget_ns=$((first_frame_budget_ms * 1000000))
|
||||
input_budget_ns=$((input_budget_ms * 1000000))
|
||||
|
||||
dump_diagnostics() { # failure forensics for shared CI runners: app log + canvas line
|
||||
echo "---- app log ($log) ----" >&2
|
||||
cat "$log" >&2 2>/dev/null || true
|
||||
echo "---- dashboard-canvas snapshot line ----" >&2
|
||||
read_snapshot
|
||||
printf '%s\n' "$snapshot" | grep 'view @w1/dashboard-canvas' >&2 || echo "(no dashboard-canvas line in snapshot)" >&2
|
||||
}
|
||||
|
||||
pid=""
|
||||
trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT
|
||||
stop_app() {
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
wait "$pid" >/dev/null 2>&1 || true
|
||||
pid=""
|
||||
}
|
||||
|
||||
read_snapshot() {
|
||||
snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
|
||||
}
|
||||
|
||||
canvas_field() { # field-name -> value of gpu_<field>=N on the dashboard-canvas line, or empty
|
||||
printf '%s\n' "$snapshot" | sed -n "s/.*view @w1\\/dashboard-canvas kind=gpu_surface.* $1=\\([0-9][0-9]*\\).*/\\1/p"
|
||||
}
|
||||
|
||||
ns_to_ms() { # ns -> ms with one decimal
|
||||
awk "BEGIN { printf \"%.1f\", $1 / 1000000 }"
|
||||
}
|
||||
|
||||
launch_and_measure_first_frame() {
|
||||
rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir/command.txt"
|
||||
"$app" > "$log" 2>&1 &
|
||||
pid=$!
|
||||
ready="$("$cli" automate wait 2>&1)"
|
||||
case "$ready" in *"ready=true"*) ;; *) echo "perf: gpu-dashboard automation snapshot was not ready" >&2; dump_diagnostics; exit 1 ;; esac
|
||||
first_frame_latency=""
|
||||
attempts=0
|
||||
while [ "$attempts" -lt 100 ]; do
|
||||
read_snapshot
|
||||
case "$snapshot" in
|
||||
*'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_nonblank=true'*)
|
||||
first_frame_latency="$(canvas_field gpu_first_frame_latency_ns)"
|
||||
case "$first_frame_latency" in ''|*[!0-9]*|0) ;; *) return 0 ;; esac
|
||||
;;
|
||||
esac
|
||||
attempts=$((attempts + 1))
|
||||
sleep 0.1
|
||||
done
|
||||
echo "perf: dashboard GPU first frame latency was not recorded within 10s" >&2
|
||||
dump_diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---- series bookkeeping -----------------------------------------------------
|
||||
|
||||
first_frame_samples=""
|
||||
input_samples=""
|
||||
|
||||
append_sample() { # list-var-name value
|
||||
eval "current=\$$1"
|
||||
if [ -z "$current" ]; then eval "$1=\$2"; else eval "$1=\"\$current
|
||||
\$2\""; fi
|
||||
}
|
||||
|
||||
p90_rank() { # n -> nearest-rank index for p90: ceil(0.9 * n)
|
||||
echo $(( (9 * $1 + 9) / 10 ))
|
||||
}
|
||||
|
||||
series_stat() { # newline-list rank -> value at 1-based rank of ascending sort
|
||||
printf '%s\n' "$1" | sort -n | sed -n "${2}p"
|
||||
}
|
||||
|
||||
report_series() { # label list budget_ns budget_ms -> prints summary, returns 1 on p90 overrun
|
||||
label="$1"; list="$2"; budget_ns="$3"; budget_ms="$4"
|
||||
n="$(printf '%s\n' "$list" | wc -l | tr -d ' ')"
|
||||
rank="$(p90_rank "$n")"
|
||||
min="$(series_stat "$list" 1)"
|
||||
median="$(series_stat "$list" $(( (n + 1) / 2 )))"
|
||||
p90="$(series_stat "$list" "$rank")"
|
||||
max="$(series_stat "$list" "$n")"
|
||||
echo "perf: $label summary over $n samples: min $(ns_to_ms "$min") ms, median $(ns_to_ms "$median") ms, p90 $(ns_to_ms "$p90") ms, max $(ns_to_ms "$max") ms (budget $budget_ms ms)"
|
||||
if [ "$p90" -gt "$budget_ns" ]; then
|
||||
echo "perf: $label p90 exceeded the $budget_ms ms budget: $p90 ns ($(ns_to_ms "$p90") ms)" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- cold start: N launches, first-frame latency ----------------------------
|
||||
|
||||
i=1
|
||||
while [ "$i" -le "$launches" ]; do
|
||||
launch_and_measure_first_frame
|
||||
append_sample first_frame_samples "$first_frame_latency"
|
||||
echo "perf: first-frame sample $i/$launches: $first_frame_latency ns ($(ns_to_ms "$first_frame_latency") ms)"
|
||||
if [ "$i" -lt "$launches" ]; then stop_app; fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# ---- steady state: M widget clicks on the final (warm) launch ---------------
|
||||
# Each click of the "Auto refresh" switch requests a GPU frame; the runtime
|
||||
# records gpu_input_latency_ns = input receipt -> first present that consumed
|
||||
# it. A sample is ready once the snapshot shows this click's input timestamp
|
||||
# AND a frame timestamp at/after it (that frame is what recorded the latency).
|
||||
|
||||
read_snapshot
|
||||
prev_input_ts="$(canvas_field gpu_input_timestamp_ns)"
|
||||
case "$prev_input_ts" in ''|*[!0-9]*) prev_input_ts=0 ;; esac
|
||||
|
||||
i=1
|
||||
while [ "$i" -le "$interactions" ]; do
|
||||
read_snapshot
|
||||
switch_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/dashboard-canvas#\([0-9][0-9]*\) role=switch name="Auto refresh".*/\1/p' | head -1)"
|
||||
case "$switch_id" in ''|*[!0-9]*) echo "perf: dashboard auto refresh switch id was missing from snapshot" >&2; exit 1 ;; esac
|
||||
"$cli" automate widget-click dashboard-canvas "$switch_id" >/dev/null 2>&1
|
||||
input_latency=""
|
||||
attempts=0
|
||||
# 200 x 50ms nominal; each iteration also spawns several subprocesses, so
|
||||
# the real window is 2-4x longer on a loaded shared runner. That headroom
|
||||
# only delays a genuine failure — a healthy click lands in a few frames.
|
||||
while [ "$attempts" -lt 200 ]; do
|
||||
read_snapshot
|
||||
input_ts="$(canvas_field gpu_input_timestamp_ns)"
|
||||
frame_ts="$(canvas_field gpu_timestamp_ns)"
|
||||
case "$input_ts" in ''|*[!0-9]*) input_ts=0 ;; esac
|
||||
case "$frame_ts" in ''|*[!0-9]*) frame_ts=0 ;; esac
|
||||
if [ "$input_ts" -gt "$prev_input_ts" ] && [ "$frame_ts" -ge "$input_ts" ]; then
|
||||
input_latency="$(canvas_field gpu_input_latency_ns)"
|
||||
case "$input_latency" in ''|*[!0-9]*|0) input_latency="" ;; *) break ;; esac
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
sleep 0.05
|
||||
done
|
||||
if [ -z "$input_latency" ]; then
|
||||
echo "perf: widget-click $i did not produce a presented frame with recorded input latency within the wait window" >&2
|
||||
dump_diagnostics
|
||||
exit 1
|
||||
fi
|
||||
prev_input_ts="$input_ts"
|
||||
append_sample input_samples "$input_latency"
|
||||
echo "perf: input-latency sample $i/$interactions: $input_latency ns ($(ns_to_ms "$input_latency") ms)"
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
stop_app
|
||||
|
||||
# ---- summary + assertions ---------------------------------------------------
|
||||
|
||||
failures=0
|
||||
report_series "first-frame latency" "$first_frame_samples" "$first_frame_budget_ns" "$first_frame_budget_ms" || failures=$((failures + 1))
|
||||
report_series "input latency" "$input_samples" "$input_budget_ns" "$input_budget_ms" || failures=$((failures + 1))
|
||||
[ "$failures" -eq 0 ] || exit 1
|
||||
echo "gpu-dashboard perf ok"
|
||||
Reference in New Issue
Block a user