diff --git a/docs/faces.md b/docs/faces.md index 56abafb..2ee00c5 100644 --- a/docs/faces.md +++ b/docs/faces.md @@ -674,6 +674,11 @@ app builds for has it, and the explicit `vfmaq` matters because LLVM will not fu add on its own. The portable loop remains the definition the others are tested against. All three produce the same 1,531,969 pairs. +The NEON kernel is the one a desktop `cargo test` never executes, so +`tools/face-tests-on-device.sh` runs the suite on an attached device: `dr-face` carries no weights +and touches no display, so its tests are a plain ARM64 binary that runs under `adb shell` with +nothing installed. Worth running whenever the kernels change. + **Where a regroup's time actually goes**, on that library, because the answer moved twice while it was being looked at: diff --git a/tools/face-tests-on-device.sh b/tools/face-tests-on-device.sh new file mode 100755 index 0000000..121b7b1 --- /dev/null +++ b/tools/face-tests-on-device.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Run dr-face's test suite on a connected Android device. +# +# ./tools/face-tests-on-device.sh [extra cargo test args…] +# +# ## Why this exists +# +# `dr-face`'s similarity scan picks its dot product per machine (docs/faces.md +# §9): AVX2 where the CPU has it, **NEON on aarch64**, and a portable loop +# otherwise. The NEON kernel is the one that runs on the phone and the tablet, +# and it is the one a desktop `cargo test` never executes — a wrong lane index +# or a mishandled tail there would be a silent wrong answer on exactly the +# devices nobody runs the test suite on. +# +# `the_fastest_kernel_agrees_with_the_portable_one` is written to catch that, +# and this is how it gets to run on the hardware it is about. Everything else +# in the suite comes along for free: `dr-face` carries no weights and touches +# no display, so its tests are a plain ARM64 binary that runs under `adb +# shell` with nothing installed. +# +# Not part of CI, which has no device attached. Run it when the kernels change. +set -euo pipefail + +TARGET=aarch64-linux-android +API=26 +DEST=/data/local/tmp/dr_face_tests + +ndk="${ANDROID_NDK_HOME:-}" +if [[ -z "$ndk" ]]; then + # The newest NDK the SDK has, which is what the app is built with. + ndk=$(find "${ANDROID_HOME:-$HOME/Android/Sdk}/ndk" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | + sort -V | tail -1) +fi +[[ -n "$ndk" && -d "$ndk" ]] || { + echo "no NDK found — set ANDROID_NDK_HOME" >&2 + exit 1 +} + +clang="$ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/$TARGET$API-clang" +[[ -x "$clang" ]] || { + echo "no $TARGET$API-clang in $ndk" >&2 + exit 1 +} + +rustup target list --installed | grep -qx "$TARGET" || rustup target add "$TARGET" + +# Told to the device before the build, so a missing tablet costs seconds rather +# than the two minutes it takes to compile for it. +adb devices | grep -qw device || { + echo "no device attached — plug the tablet in and enable USB debugging" >&2 + exit 1 +} + +echo "building dr-face tests for $TARGET…" +binary=$( + CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$clang" \ + CC_aarch64_linux_android="$clang" \ + cargo test -p dr-face --lib --release --target "$TARGET" --no-run --message-format=json "$@" | + python3 -c ' +import json, sys +for line in sys.stdin: + m = json.loads(line) + if m.get("profile", {}).get("test") and m.get("executable"): + print(m["executable"]) +' | tail -1 +) +[[ -n "$binary" ]] || { + echo "cargo produced no test binary" >&2 + exit 1 +} + +echo "pushing $(basename "$binary")…" +adb push "$binary" "$DEST" >/dev/null +adb shell chmod 755 "$DEST" +# `--test-threads` left alone: the scan spawns its own workers and the point is +# to exercise them the way the app will. +adb shell "$DEST" --color never +adb shell rm -f "$DEST"