[Buildroot] [PATCH v4] utils/test-pkg: add --jobs option

Joseph Kogut <[email protected]>
Newsgroups net.busybox.buildroot
Message-ID <[email protected]>
Builds run by test-pkg are often not CPU limited, so running several
builds concurrently reduces the time needed to test a package across
many toolchains.

Add a -j/--jobs option to run multiple builds in parallel.

Quote "keep" in build_one(), as it is now passed in as an argument
rather than set from a literal.

Testing sdl2 on six toolchains with a 16-core Ryzen 9 9950X reduced wall
clock time from 2m32.979s with -j1 to 0m41.704s with -j0. Testing libpng
across all 35 toolchains reduced wall clock time from 23m34.593s to
2m6.510s, an approximately 11x speedup.

Signed-off-by: Joseph Kogut <[email protected]>
---
Changes in v4:
- Rewritten to use GNU parallel
- Changed -C -> -j (more idiomatic)
- Link to v3: https://lore.kernel.org/r/[email protected]

Changes in v3:
- Fix improper removal of '-T' short option during rebase
- Minor tweak to remove newline between prompt and output
- Simplify make job termination
- Link to v2: https://lore.kernel.org/r/[email protected]

Changes in v2:
- Rebase on origin/master
- Properly restore cursor on interrupt
- Properly terminate running jobs on interrupt
- Add animated spinner for running builds
- Simplify status updates
- Link to v1: https://lore.kernel.org/r/[email protected]

To: [email protected]
---
 utils/test-pkg | 84 ++++++++++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 70 insertions(+), 14 deletions(-)

diff --git a/utils/test-pkg b/utils/test-pkg
index cea7ace7cb..887cebb4d2 100755
--- a/utils/test-pkg
+++ b/utils/test-pkg
@@ -17,13 +17,13 @@ do_clean() {
 
 main() {
     local o O opts
-    local cfg dir pkg random toolchains_csv toolchain all number mode prepare_only
-    local ret nb nb_skip nb_fail nb_legal nb_show nb_tc build_dir keep
+    local cfg dir pkg random toolchains_csv toolchain all number mode prepare_only jobs
+    local ret nb nb_skip nb_fail nb_legal nb_show nb_tc keep
     local -a toolchains
     local pkg_br_name
 
-    o='hakc:d:n:p:r:t:T:'
-    O='help,all,keep,prepare-only,config-snippet:,build-dir:,number:,package:,random:,toolchains-csv:,toolchain-name:'
+    o='hakc:d:j:n:p:r:t:T:'
+    O='help,all,keep,prepare-only,config-snippet:,build-dir:,jobs:,number:,package:,random:,toolchains-csv:,toolchain-name:'
     opts="$(getopt -n "${my_name}" -o "${o}" -l "${O}" -- "${@}")"
     eval set -- "${opts}"
 
@@ -33,6 +33,7 @@ main() {
     number=0
     mode=0
     prepare_only=0
+    jobs=1
     toolchains_csv="${TOOLCHAINS_CSV}"
     while [ ${#} -gt 0 ]; do
         case "${1}" in
@@ -54,6 +55,9 @@ main() {
         (-d|--build-dir)
             dir="${2}"; shift 2
             ;;
+        (-j|--jobs)
+            jobs="${2}"; shift 2
+            ;;
         (-n|--number)
             number="${2}"; shift 2
             ;;
@@ -88,6 +92,12 @@ main() {
     if [ ! -e "${cfg}" ]; then
         printf "error: %s: no such file\n" "${cfg}" >&2; exit 1
     fi
+    case "${jobs}" in
+    (''|*[!0-9]*) printf "error: invalid number of jobs: %s\n" "${jobs}" >&2; exit 1;;
+    esac
+    if [ "${jobs}" -eq 0 ]; then
+        jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)"
+    fi
     if [ -z "${dir}" ]; then
         dir="${HOME}/br-test-pkg"
     fi
@@ -137,12 +147,9 @@ main() {
     nb_fail=0
     nb_legal=0
     nb_show=0
-    for toolchainconfig in "${toolchains[@]}"; do
+    while read -r ret toolchain; do
         : $((nb++))
-        toolchain="$(basename "${toolchainconfig}" .config)"
-        build_dir="${dir}/${toolchain}"
         printf "%40s [%*d/%d]: " "${toolchain}" ${#nb_tc} "${nb}" "${nb_tc}"
-        build_one "${build_dir}" "${toolchainconfig}" "${cfg}" "${pkg}" "${prepare_only}" && ret=0 || ret=${?}
         case ${ret} in
         (0) printf "OK\n";;
         (1) : $((nb_skip++)); printf "SKIPPED\n";;
@@ -150,11 +157,15 @@ main() {
         (3) : $((nb_legal++)); printf "FAILED\n";;
         (4) : $((nb_show++)); printf "FAILED\n";;
         esac
+    done < <(run_builds)
 
-        if [ "${abort}" -eq 1 ]; then
-            return 1
-        fi
-    done
+    if [ "${abort}" -eq 1 ]; then
+        return 1
+    fi
+    if [ "${nb}" -ne "${nb_tc}" ]; then
+        printf "error: only %d of the %d builds reported a status\n" "${nb}" "${nb_tc}" >&2
+        return 1
+    fi
 
     printf "%d builds, %d skipped, %d build failed, %d legal-info failed, %d show-info failed\n" \
         "${nb}" "${nb_skip}" "${nb_fail}" "${nb_legal}" "${nb_show}"
@@ -162,6 +173,47 @@ main() {
     return $((nb_fail + nb_legal))
 }
 
+run_builds() {
+    local toolchainconfig
+
+    if [ "${jobs}" -gt 1 ]; then
+        if parallel --version 2>/dev/null | grep -q '^GNU parallel'; then
+            export -f build_one build_one_status
+            # --quote, so that empty and spaced arguments survive.
+            PARALLEL_SHELL=bash parallel --will-cite --quote --jobs "${jobs}" \
+                --ungroup build_one_status {} "${dir}" "${cfg}" "${pkg}" \
+                "${prepare_only}" "${keep}" ::: "${toolchains[@]}" || true
+            return
+        fi
+        printf "warning: GNU parallel not available; running builds serially\n" >&2
+    fi
+    # We run in a sub-shell, which does not inherit the trap set in
+    # main(), so re-arm it to keep stopping after the current build.
+    trap do_abort INT
+    for toolchainconfig in "${toolchains[@]}"; do
+        build_one_status "${toolchainconfig}" "${dir}" "${cfg}" "${pkg}" \
+            "${prepare_only}" "${keep}"
+
+        if [ "${abort}" -eq 1 ]; then
+            break
+        fi
+    done
+}
+
+build_one_status() {
+    local toolchainconfig="${1}"
+    local dir="${2}"
+    local cfg="${3}"
+    local pkg="${4}"
+    local prepare_only="${5}"
+    local keep="${6}"
+    local toolchain ret
+
+    toolchain="$(basename "${toolchainconfig}" .config)"
+    build_one "${dir}/${toolchain}" "${toolchainconfig}" "${cfg}" "${pkg}" "${prepare_only}" && ret=0 || ret=${?}
+    printf "%d %s\n" "${ret}" "${toolchain}"
+}
+
 build_one() {
     local dir="${1}"
     local toolchainconfig="${2}"
@@ -182,7 +234,7 @@ build_one() {
     # done in the same locale.
     comm -23 <(sort "${cfg}") <(sort "${dir}/.config") >"${dir}/missing.config"
     if [ -s "${dir}/missing.config" ]; then
-        if [ ${keep} -ne 1 ]; then
+        if [ "${keep}" -ne 1 ]; then
             # Invalid configuration, drop it
             rm -f "${dir}/.config"
         fi
@@ -229,7 +281,7 @@ build_one() {
 
     # If we get here, the build was successful. Clean up the build/host
     # directories to save disk space, unless 'keep' was set.
-    if [ ${keep} -ne 1 ]; then
+    if [ "${keep}" -ne 1 ]; then
         make O="${dir}" clean >> "${dir}/logfile" 2>&1
     fi
 }
@@ -299,6 +351,10 @@ Options:
         is specified, then a single build will be performed on that
         specific toolchain only.
 
+    -j N, --jobs N
+        Run N builds concurrently using GNU parallel. If N is 0, match the
+        number of logical CPUs. Builds run serially if GNU parallel is missing.
+
     -k, --keep
         Keep the build directories even if the build succeeds.
         Note: the logfile and configuration is always retained, even without

---
base-commit: 6144b0f4b73bea810809f09d23bbe76b4979bc13
change-id: 20250619-concurrent-test-pkg-f3ad6d3c01b4

Best regards,
--  
Joseph Kogut <[email protected]>

_______________________________________________
buildroot mailing list
[email protected]
https://lists.buildroot.org/mailman/listinfo/buildroot
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.