From fcc9f2f02db7b38953b64db6211fd5d4d9798e88 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:15:16 -0700 Subject: [PATCH 1/7] playTests: run cover shrink tests with steps=7 Each of the 4 `--train-cover=steps=256,shrink*` commands trained 558 candidate dictionaries. Under MSan they took ~90 s each, 6 of the 26 min of clang-msan-testzstd. Shrinking is applied to every candidate, so a few candidates exercise it just as well: steps=7 trains 16 of them, and runs ~20x faster. The fastcover shrink tests were reduced the same way in 85a016ed9. --- tests/playTests.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/playTests.sh b/tests/playTests.sh index d8ea0c3dad3..44bc82b5610 100755 --- a/tests/playTests.sh +++ b/tests/playTests.sh @@ -1909,10 +1909,10 @@ println "- Create second (different) dictionary" zstd -T0 --train-cover=k=56,d=8 "$TESTDIR"/*.c "$PRGDIR"/*.c "$PRGDIR"/*.h -o tmpDictC zstd -d tmp.zst -D tmpDictC -fo result && die "wrong dictionary not detected!" println "- Create dictionary using shrink-dict flag" -zstd -T0 --train-cover=steps=256,shrink "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict -zstd -T0 --train-cover=steps=256,shrink=1 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict1 -zstd -T0 --train-cover=steps=256,shrink=5 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict2 -zstd -T0 --train-cover=shrink=5,steps=256 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict3 +zstd -T0 --train-cover=steps=7,shrink "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict +zstd -T0 --train-cover=steps=7,shrink=1 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict1 +zstd -T0 --train-cover=steps=7,shrink=5 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict2 +zstd -T0 --train-cover=shrink=5,steps=7 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpShrinkDict3 println "- Create dictionary with short dictID" zstd -T0 --train-cover=k=46,d=8,split=80 "$TESTDIR"/*.c "$PRGDIR"/*.c --dictID=1 -o tmpDict1 cmp tmpDict tmpDict1 && die "dictionaries should have different ID !" From 9500387660d8e1fa653aaf364ec7cbdd2924192b Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:15:16 -0700 Subject: [PATCH 2/7] fuzz.py: run regression targets in parallel `fuzz.py regression` ran the 21 corpora one after another: msan-regression took 9 to 24 min on 4-CPU runners, depending on runner speed. Run one target per CPU, largest corpora first, since they take longest. Each target's output is printed with its command line when it completes. All targets run even if one fails, and failures are listed at the end. Locally, 4 CPUs, no sanitizer: 290 s -> 135 s. --- tests/fuzz/README.md | 2 +- tests/fuzz/fuzz.py | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/tests/fuzz/README.md b/tests/fuzz/README.md index e2196e832c6..8732c8c3978 100644 --- a/tests/fuzz/README.md +++ b/tests/fuzz/README.md @@ -105,7 +105,7 @@ CC=afl-clang CXX=afl-clang++ ./fuzz.py build all --enable-asan --enable-ubsan ## Regression Testing The regression test supports the `all` target to run all the fuzzers in one -command. +command. Targets run in parallel, one per CPU. ``` CC=clang CXX=clang++ ./fuzz.py build all --enable-asan --enable-ubsan diff --git a/tests/fuzz/fuzz.py b/tests/fuzz/fuzz.py index 492be3486d0..22a1ee3f925 100755 --- a/tests/fuzz/fuzz.py +++ b/tests/fuzz/fuzz.py @@ -11,6 +11,7 @@ # ########################################################################## import argparse +import concurrent.futures import contextlib import os import re @@ -648,6 +649,13 @@ def afl(args): return 0 +def corpus_size(target): + size = 0 + for root, _, files in os.walk(abs_join(CORPORA_DIR, target)): + size += sum(os.path.getsize(os.path.join(root, f)) for f in files) + return size + + def regression(args): try: description = """ @@ -655,18 +663,34 @@ def regression(args): The fuzzer should have been built with LIB_FUZZING_ENGINE='libregression.a'. Takes input from CORPORA. + Targets run in parallel, one per CPU. """ args = targets_parser(args, description) except Exception as e: print(e) return 1 - for target in args.TARGET: + + def run(target): corpora = create(abs_join(CORPORA_DIR, target)) - target = abs_join(FUZZ_DIR, target) - cmd = [target, corpora] - print(' '.join(cmd)) - subprocess.check_call(cmd) - return 0 + cmd = [abs_join(FUZZ_DIR, target), corpora] + result = subprocess.run(cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + return cmd, result + + # Largest corpora take longest: start them first + targets = sorted(args.TARGET, key=corpus_size, reverse=True) + failed = [] + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as pool: + runs = [pool.submit(run, target) for target in targets] + for done in concurrent.futures.as_completed(runs): + cmd, result = done.result() + print(' '.join(cmd)) + print(result.stdout.decode(errors='replace'), end='', flush=True) + if result.returncode != 0: + failed.append(cmd[0]) + for target in failed: + print('FAILED: {}'.format(target)) + return 1 if failed else 0 def gen_parser(args): From e3e3ce5c19dcedb8ff8e8fd13ee387c9dac32dc4 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:15:23 -0700 Subject: [PATCH 3/7] ci: run make-test and make-test-macos with make -j `make test` ran its 9 test targets one after another, taking 14-20 min, about 7 of which are time-boxed fuzzers. make-test-32bit already runs `make -j test`, and takes 10-11 min. --- .github/workflows/dev-long-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-long-tests.yml b/.github/workflows/dev-long-tests.yml index 4b0f6c2acee..41e4fca5c29 100644 --- a/.github/workflows/dev-long-tests.yml +++ b/.github/workflows/dev-long-tests.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: make test run: | - make test + make -j test make -j zstd ./tests/test_process_substitution.bash ./zstd @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: make test on macos - run: make test + run: make -j test # lasts ~10mn make-test-32bit: From c7b994bb11dac312e2a556bc80278c60fbb664ba Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:15:23 -0700 Subject: [PATCH 4/7] ci: skip large data tests in clang-msan-testzstd They took ~20 of the job's 22-26 min, making it the longest job of dev-long-tests. They still run in the make-test* jobs, and in the 3 ASan + UBSan testzstd jobs. --- .github/workflows/dev-long-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev-long-tests.yml b/.github/workflows/dev-long-tests.yml index 41e4fca5c29..39794d550be 100644 --- a/.github/workflows/dev-long-tests.yml +++ b/.github/workflows/dev-long-tests.yml @@ -251,8 +251,10 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: clang + MSan + Test Zstd + # ZSTDRTTEST= : skip the large data tests of playTests.sh, too slow under MSan. + # They still run in the make-test* and ASan + UBSan testzstd jobs. run: | - CC=clang make -j msan-test-zstd HAVE_ZLIB=0 HAVE_LZ4=0 HAVE_LZMA=0 V=1 + CC=clang make -j msan-test-zstd HAVE_ZLIB=0 HAVE_LZ4=0 HAVE_LZMA=0 ZSTDRTTEST= V=1 armfuzz: runs-on: ubuntu-latest From 11eb217c39b9bb5fa0f5e8f01e2136f2f98dd446 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 16:43:49 -0700 Subject: [PATCH 5/7] gitignore: ignore .claude/ Claude Code keeps per-checkout state there (local settings, agent worktrees). Nothing under .claude/ is tracked; ignoring it keeps it out of git status and out of accidental commits. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 03ecbde6af8..6868bc7245d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ googletest/ *.d *.vscode *.code-workspace +.claude/ compile_commands.json .clangd perf.data From 1145633b68aeb0cd80312390d15f4952011a35c6 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:53:48 -0700 Subject: [PATCH 6/7] ci: run the fuzz test jobs with make -j The uasan-%, asan-% and tsan-% rules don't pass -j, so these jobs built their testers serially, then ran fuzzer, zstreamtest and decodecorpus one after another. gcc-asan-ubsan-fuzz32 took 16 min: 10.3 for fuzzer, 3.1 for zstreamtest, 2.9 for builds. With -j, the testers build and run concurrently, as in the msan-fuzztest jobs, whose msan-% rule already passes -j. --- .github/workflows/dev-long-tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dev-long-tests.yml b/.github/workflows/dev-long-tests.yml index 39794d550be..753085fc752 100644 --- a/.github/workflows/dev-long-tests.yml +++ b/.github/workflows/dev-long-tests.yml @@ -72,7 +72,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: no intrinsics fuzztest - run: MOREFLAGS="-DZSTD_NO_INTRINSICS" make -C tests fuzztest + run: MOREFLAGS="-DZSTD_NO_INTRINSICS" make -j -C tests fuzztest # lasts ~8mn tsan-zstreamtest: @@ -95,7 +95,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: thread sanitizer fuzztest - run: CC=clang make tsan-fuzztest + run: CC=clang make -j tsan-fuzztest big-tests-zstreamtest32: runs-on: ubuntu-latest @@ -151,14 +151,14 @@ jobs: echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal main universe" | sudo tee -a /etc/apt/sources.list sudo apt-get -qqq update make gcc8install - CC=gcc-8 FUZZER_FLAGS="--long-tests" make clean uasan-fuzztest + CC=gcc-8 FUZZER_FLAGS="--long-tests" make -j clean uasan-fuzztest clang-asan-ubsan-fuzz: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: clang + ASan + UBSan + Fuzz Test - run: CC=clang FUZZER_FLAGS="--long-tests" make clean uasan-fuzztest + run: CC=clang FUZZER_FLAGS="--long-tests" make -j clean uasan-fuzztest # the only job decoding real legacy frames under sanitizers. # fuzzers do build these decoders, but only reach them if an input @@ -178,7 +178,7 @@ jobs: run: | sudo apt-get -qqq update make libc6install - CFLAGS="-O3 -m32" FUZZER_FLAGS="--long-tests" make uasan-fuzztest + CFLAGS="-O3 -m32" FUZZER_FLAGS="--long-tests" make -j uasan-fuzztest clang-asan-fuzz32: runs-on: ubuntu-latest @@ -188,7 +188,7 @@ jobs: run: | sudo apt-get -qqq update make libc6install - CC=clang CFLAGS="-O3 -m32" FUZZER_FLAGS="--long-tests" make asan-fuzztest + CC=clang CFLAGS="-O3 -m32" FUZZER_FLAGS="--long-tests" make -j asan-fuzztest # The following test seems to have issues on github CI specifically, # it does not provide the `__mulodi4` instruction emulation From 72c3d1c003be70c763ee1555cb8973f64db32567 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 5 Oct 2026 17:54:31 -0700 Subject: [PATCH 7/7] ci: shorter random fuzzing in clang-msan-fuzz-unoptimized The fuzzer unit tests take 4 to 12 min at -O0 under MSan, depending on the runner, and are what this job is for. The random fuzzing that follows (fuzzer 200 s, zstreamtest 2x 90 s) duplicates clang-msan-fuzz, which runs it longer, with --long-tests, on optimized code. Cut it to 60 s and 2x 30 s: ~2.3 min less on this job, the longest of dev-long-tests after the previous changes. --- .github/workflows/dev-long-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev-long-tests.yml b/.github/workflows/dev-long-tests.yml index 753085fc752..fab0b96203f 100644 --- a/.github/workflows/dev-long-tests.yml +++ b/.github/workflows/dev-long-tests.yml @@ -231,10 +231,12 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: clang + MSan + Fuzz Test + # -O0 targets the unit tests on unoptimized code: random fuzzing is kept short. + # clang-msan-fuzz runs the long random fuzzing. run: | sudo apt-get -qqq update sudo apt-get install clang - CC=clang MOREFLAGS="-O0" make clean msan-fuzztest + CC=clang MOREFLAGS="-O0" FUZZERTEST=-T60s ZSTREAM_TESTTIME=-T30s make clean msan-fuzztest clang-msan-fuzz: runs-on: ubuntu-latest