|
| 1 | +name: JIT Hang Bisect |
| 2 | +# Manually-triggered bisect for the Symfony Console OutputFormatter tracing-JIT |
| 3 | +# hang seen in the nightly COMMUNITY_asan job. Runs on a native x86-64 runner |
| 4 | +# (the bug does not reproduce on ARM or under qemu emulation). |
| 5 | +on: |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + good: |
| 9 | + description: 'Known-GOOD commit/tag (bug absent)' |
| 10 | + default: 'php-8.3.0' |
| 11 | + bad: |
| 12 | + description: 'Known-BAD commit/tag (hang)' |
| 13 | + default: '27c10ddce6302912a46d72a4dcd47c89f1209544' |
| 14 | + symfony_commit: |
| 15 | + description: 'Symfony commit to test against (CI nightly pin)' |
| 16 | + default: 'dce1b899ab0c0e4cef9159c083ef535387a8db12' |
| 17 | + test_timeout: |
| 18 | + description: 'Per-step test timeout in seconds (hang detector)' |
| 19 | + default: '300' |
| 20 | + use_asan: |
| 21 | + description: 'Build with ASAN+UBSAN (matches COMMUNITY_asan exactly)' |
| 22 | + default: 'true' |
| 23 | + |
| 24 | +permissions: |
| 25 | + contents: read |
| 26 | + |
| 27 | +jobs: |
| 28 | + bisect: |
| 29 | + runs-on: ubuntu-24.04 |
| 30 | + timeout-minutes: 350 |
| 31 | + env: |
| 32 | + SYMFONY_DIR: ${{ runner.temp }}/symfony |
| 33 | + TEST_TIMEOUT: ${{ inputs.test_timeout }} |
| 34 | + USE_ASAN: ${{ inputs.use_asan }} |
| 35 | + # Mirror the COMMUNITY_asan runtime environment |
| 36 | + USE_ZEND_ALLOC: '0' |
| 37 | + USE_TRACKED_ALLOC: '1' |
| 38 | + ASAN_OPTIONS: 'exitcode=139' |
| 39 | + UBSAN_OPTIONS: 'print_stacktrace=1' |
| 40 | + SYMFONY_DEPRECATIONS_HELPER: 'max[total]=999' |
| 41 | + steps: |
| 42 | + - name: Checkout php-src (full history) |
| 43 | + uses: actions/checkout@v6 |
| 44 | + with: |
| 45 | + fetch-depth: 0 |
| 46 | + |
| 47 | + - name: Ensure bisect endpoints & tags are present |
| 48 | + run: | |
| 49 | + git remote add upstream https://github.com/php/php-src.git || true |
| 50 | + git fetch --tags --quiet upstream |
| 51 | + git rev-parse "${{ inputs.good }}^{commit}" |
| 52 | + git rev-parse "${{ inputs.bad }}^{commit}" |
| 53 | +
|
| 54 | + - name: Install build dependencies |
| 55 | + run: | |
| 56 | + sudo apt-get update |
| 57 | + sudo apt-get install -y --no-install-recommends \ |
| 58 | + build-essential autoconf automake libtool bison re2c pkg-config ccache \ |
| 59 | + libxml2-dev libsqlite3-dev libonig-dev libssl-dev zlib1g-dev \ |
| 60 | + php-cli php-xml php-mbstring composer |
| 61 | +
|
| 62 | + - name: Set up Symfony test bed (pinned commit) |
| 63 | + run: | |
| 64 | + git clone --no-checkout --filter=blob:none https://github.com/symfony/symfony.git "$SYMFONY_DIR" |
| 65 | + git -C "$SYMFONY_DIR" fetch --depth=1 origin "${{ inputs.symfony_commit }}" |
| 66 | + git -C "$SYMFONY_DIR" checkout "${{ inputs.symfony_commit }}" |
| 67 | + ( cd "$SYMFONY_DIR" && composer install --no-progress --ignore-platform-req=php+ && php ./phpunit install ) |
| 68 | +
|
| 69 | + - name: Write bisect predicate |
| 70 | + run: | |
| 71 | + cat > "${{ runner.temp }}/bisect_step.sh" <<'STEP' |
| 72 | + #!/usr/bin/env bash |
| 73 | + # exit 0=GOOD, 1=BAD(hang), 125=SKIP(untestable) |
| 74 | + set -u |
| 75 | + NCPU="$(nproc)" |
| 76 | + WS="$PWD" |
| 77 | + rev="$(git rev-parse --short HEAD)" |
| 78 | + export CC="ccache gcc" CXX="ccache g++" |
| 79 | +
|
| 80 | + make distclean >/dev/null 2>&1 |
| 81 | + ./buildconf --force >/tmp/bc.log 2>&1 || { echo "[$rev] buildconf fail -> skip"; exit 125; } |
| 82 | +
|
| 83 | + CONF=( --enable-debug --enable-zts --enable-opcache |
| 84 | + --enable-mbstring --enable-tokenizer |
| 85 | + --with-libxml --enable-dom --enable-xml --enable-xmlwriter --enable-xmlreader --enable-simplexml |
| 86 | + --enable-ctype --enable-filter --enable-fileinfo --enable-phar --enable-posix --enable-pcntl |
| 87 | + --disable-cgi ) |
| 88 | + if [ "${USE_ASAN:-true}" = "true" ]; then |
| 89 | + CONF+=( "CFLAGS=-fsanitize=undefined,address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" |
| 90 | + "LDFLAGS=-fsanitize=undefined,address" ) |
| 91 | + fi |
| 92 | + ./configure "${CONF[@]}" >/tmp/conf.log 2>&1 || { echo "[$rev] configure fail -> skip"; tail -5 /tmp/conf.log; exit 125; } |
| 93 | + make -j"$NCPU" >/tmp/make.log 2>&1 || { echo "[$rev] make fail -> skip"; tail -15 /tmp/make.log; exit 125; } |
| 94 | +
|
| 95 | + PHP_BIN="$WS/sapi/cli/php" |
| 96 | + OC="$(find "$WS" -name opcache.so -path '*/modules/*' | head -1)" |
| 97 | + [ -x "$PHP_BIN" ] && [ -n "$OC" ] || { echo "[$rev] no binary -> skip"; exit 125; } |
| 98 | + JIT=( -dzend_extension="$OC" -dopcache.enable_cli=1 -dopcache.jit=tracing -dopcache.jit_buffer_size=1G |
| 99 | + -dopcache.jit_hot_loop=1 -dopcache.jit_hot_func=1 -dopcache.jit_hot_return=1 |
| 100 | + -dopcache.jit_hot_side_exit=1 -dmemory_limit=-1 ) |
| 101 | + "$PHP_BIN" "${JIT[@]}" -r 'exit((opcache_get_status(false)["jit"]["enabled"]??false)?0:3);' \ |
| 102 | + || { echo "[$rev] JIT not enabled -> skip"; exit 125; } |
| 103 | +
|
| 104 | + cd "$SYMFONY_DIR" || { echo "[$rev] no symfony -> skip"; exit 125; } |
| 105 | + echo "[$rev] running Console suite (timeout ${TEST_TIMEOUT}s)..." |
| 106 | + timeout "${TEST_TIMEOUT}" "$PHP_BIN" "${JIT[@]}" ./phpunit src/Symfony/Component/Console \ |
| 107 | + --exclude-group tty --exclude-group benchmark --exclude-group intl-data \ |
| 108 | + --exclude-group transient --exclude-group skip >/tmp/test.log 2>&1 |
| 109 | + rc=$? |
| 110 | + case "$rc" in |
| 111 | + 124) echo "[$rev] HANG (timeout) -> BAD"; exit 1 ;; |
| 112 | + 0|1|2) echo "[$rev] completed rc=$rc -> GOOD"; exit 0 ;; |
| 113 | + *) echo "[$rev] abnormal rc=$rc (asan abort / crash) -> SKIP"; tail -5 /tmp/test.log; exit 125 ;; |
| 114 | + esac |
| 115 | + STEP |
| 116 | + chmod +x "${{ runner.temp }}/bisect_step.sh" |
| 117 | +
|
| 118 | + - name: Run git bisect |
| 119 | + run: | |
| 120 | + git bisect reset >/dev/null 2>&1 || true |
| 121 | + git bisect start |
| 122 | + git bisect bad "${{ inputs.bad }}" |
| 123 | + git bisect good "${{ inputs.good }}" |
| 124 | + set +e |
| 125 | + git bisect run "${{ runner.temp }}/bisect_step.sh" | tee "${{ runner.temp }}/bisect_out.txt" |
| 126 | + set -e |
| 127 | +
|
| 128 | + - name: Report first-bad commit |
| 129 | + if: always() |
| 130 | + run: | |
| 131 | + { |
| 132 | + echo "## JIT hang bisect result" |
| 133 | + echo '```' |
| 134 | + grep -E "is the first bad commit" -A0 "${{ runner.temp }}/bisect_out.txt" || echo "(no clear first-bad — see log below)" |
| 135 | + echo '```' |
| 136 | + echo "### Full bisect log" |
| 137 | + echo '```' |
| 138 | + git bisect log 2>/dev/null || true |
| 139 | + echo '```' |
| 140 | + } >> "$GITHUB_STEP_SUMMARY" |
| 141 | + git bisect reset >/dev/null 2>&1 || true |
0 commit comments