Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c7fe394
ci(rps): harden macos-15 against runner lost-communication wedges
justcoding121 Oct 2, 2026
ac107ae
fix(rps): parse workflow created_at when gh returns DateTime
justcoding121 Oct 2, 2026
feb3743
ci(rps): raise macOS ramp timeout from 40m to 55m
justcoding121 Oct 2, 2026
be4d4e8
docs(rps): note macOS ramp timeout is 55 minutes
justcoding121 Oct 2, 2026
c6b3916
docs(wiki): refresh Windows/Linux performance tables and charts @ 41f…
justcoding121 Oct 2, 2026
45112b1
docs(perf): record that a past RPS miss is not a ban on retrying
justcoding121 Oct 2, 2026
1d96d08
fix(rps): stop the HTTP/2 WebSocket probe stalling on Linux delayed ACK
justcoding121 Oct 2, 2026
5e3da4d
perf(h2): size streamed DATA frames to the credit the client has alre…
justcoding121 Oct 2, 2026
f2061c2
perf(h3): write small frames from a per-stream scratch buffer
justcoding121 Oct 2, 2026
b067fc1
docs(rps): record the f2061c27 verification run IDs
justcoding121 Oct 2, 2026
8a1b819
docs(wiki): publish the HTTP/2 WebSocket re-measure
justcoding121 Oct 2, 2026
cda92a8
docs(perf): record the H2 credit-frame and H3 scratch re-measure
justcoding121 Oct 2, 2026
050e72c
docs(wiki): publish Apple Silicon Mac RPS tables and charts
justcoding121 Oct 2, 2026
ed05ba5
docs(wiki): refresh the 7.0 vs 6.0 cross-version table
justcoding121 Oct 2, 2026
feee234
docs(wiki): restore the f2061c27 HTTP/2 WebSocket rows overwritten by…
justcoding121 Oct 2, 2026
4a19be8
docs(wiki): show gold/silver/bronze on all peer RPS tables
justcoding121 Oct 2, 2026
fcc17cc
docs(wiki): keep only gold medals on peer RPS tables
justcoding121 Oct 2, 2026
c9e0755
tools(rps): log negotiated TLS version, cipher suite and ALPN per arm…
justcoding121 Oct 2, 2026
865509c
tools(rps): add CPU-per-request analysis script from existing CSV col…
justcoding121 Oct 2, 2026
2be9a95
ci(rps): same-job paired A/B workflow (rps-ab.yml) with CI-based keep…
justcoding121 Oct 2, 2026
49ba9e8
tools(rps): Linux profile capture (perf, dotnet-trace, gc-verbose, co…
justcoding121 Oct 2, 2026
f8dae58
ci(rps): dispatch-only Linux profile workflow (rps-profile.yml)
justcoding121 Oct 2, 2026
9dfe3cd
docs(rps): document rps-ab, rps-profile, CPU-per-request and tls-pari…
justcoding121 Oct 2, 2026
2f408f6
tools(rps): --arm-contains accepts comma-separated alternatives and a…
justcoding121 Oct 3, 2026
6d150be
perf(h1): lock-free terminate-lite client shell pool (ConcurrentQueue…
justcoding121 Oct 2, 2026
e3a5573
docs(wiki): Linux C# limit ledger with per-row classification and evi…
justcoding121 Oct 2, 2026
1632b05
docs(wiki): record CI A/B and H3->H1 profile results in the Linux C# …
justcoding121 Oct 3, 2026
8107215
ci(rps): give the macOS ramp step 90 minutes so a filtered product re…
justcoding121 Oct 3, 2026
db8fa28
docs(wiki): record the H3 and H1 micro-win bundle as tried, not kept
justcoding121 Oct 3, 2026
97f1aac
docs(wiki): paste the H1 re-measure and refresh the practical charts
justcoding121 Oct 3, 2026
9fa0566
build(website): override vite and esbuild to patched releases (dev-se…
justcoding121 Oct 3, 2026
88c3aaf
docs(wiki): state the Linux C# limit conclusion and its scope
justcoding121 Oct 3, 2026
9825981
ci(rps): gate reverse on the closest peer at 0.50 and MITM at 0.25
justcoding121 Oct 3, 2026
aae68ba
docs(wiki): drop the struck-through POST note from the user page
justcoding121 Oct 3, 2026
e5a9c5f
fix(inspector): keep large session list actions off the UI thread
justcoding121 Oct 3, 2026
e9888ff
docs(wiki): Linux body, POST and new-connection rows are at the C# fl…
justcoding121 Oct 3, 2026
0386b2a
perf(rps): one wiki row per job, and retire the 7.0 vs 6.0 comparison…
justcoding121 Oct 3, 2026
47e95f2
fix(rps): list HTTP/3 rows even before libmsquic is installed (#1065)
justcoding121 Oct 3, 2026
9d2b06e
fix(rps): keep a one-row suite matrix as a JSON array (#1066)
justcoding121 Oct 3, 2026
543d6e6
docs(wiki): re-measure every Performance table at 0386b2aa (#1067)
justcoding121 Oct 3, 2026
ba81d61
chore(release): bump product version to 7.0.16 for the next beta cut.
justcoding121 Oct 4, 2026
a409490
Merge branch 'beta' into develop for the 7.0.16-beta cut.
justcoding121 Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/pr-path-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
env:
TWP_NC_MAINTAINERS: ${{ vars.TWP_NC_MAINTAINERS }}
ACTOR: ${{ github.actor }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
shell: bash
run: |
set -euo pipefail
Expand All @@ -31,7 +32,9 @@ jobs:
is_maintainer=false
for m in "${MAINTAINERS[@]}"; do
m="$(echo "$m" | xargs)"
if [[ "$ACTOR" == "$m" ]]; then
# A cloud agent push sets actor to cursor[bot] on a maintainer-owned PR.
# The PR author is the account that opened it.
if [[ "$ACTOR" == "$m" || "$PR_AUTHOR" == "$m" ]]; then
is_maintainer=true
break
fi
Expand Down
193 changes: 193 additions & 0 deletions .github/workflows/rps-ab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Same-job paired A/B for Titanium RPS work (workflow_dispatch only; Linux only).
#
# Why: two rps-saturation dispatches land on two different VMs and runner variance (~5%) is larger than
# the gains left on the Linux gap list. This job checks out a baseline and a candidate product ref on ONE
# ubuntu-latest VM, builds the SAME probe harness (tools/RpsLoadProbe from the dispatch ref) against each,
# and runs them alternately baseline, candidate, baseline, candidate ... for >= 5 pairs with a cool-down
# between runs. Paired deltas, a 95% confidence interval and a keep gate are written to the job summary.
#
# Dispatch the workflow on the harness ref (usually the candidate branch):
# gh workflow run rps-ab.yml --ref <branch-or-sha> \
# -f baseline_ref=develop -f candidate_ref=<branch-or-sha> \
# -f mode=compare-ceiling -f arm_contains=twp-reverse-http1 -f pairs=5
#
# Only product code (everything outside tools/RpsLoadProbe) differs between the two sides.
# Peers (nginx / HAProxy / Envoy) are not installed here; keep arm_contains on Titanium (twp-) or bare- arms.

name: RPS paired A/B

on:
workflow_dispatch:
inputs:
baseline_ref:
description: 'Baseline product ref (branch, tag or SHA)'
required: true
default: develop
candidate_ref:
description: 'Candidate product ref (branch, tag or SHA)'
required: false
default: ''
mode:
description: 'Probe ramp mode (see rps-saturation.yml for the list)'
required: true
default: compare-ceiling
arm_contains:
description: 'Arm-name substring filter (applied to the arms of the mode)'
required: true
default: 'twp-reverse-http1'
concurrency:
description: 'Concurrency (one value; paired runs use a fixed step)'
required: true
default: '32'
duration_sec:
description: 'Measure seconds'
required: true
default: '8'
pairs:
description: 'Number of baseline/candidate pairs (>= 5 for a keep decision)'
required: true
default: '5'
cooldown_sec:
description: 'Idle seconds between consecutive runs'
required: true
default: '15'
order:
description: 'alternate = A,B,A,B ; counterbalance = A,B,B,A,A,B (cancels linear drift)'
required: true
default: alternate
type: choice
options:
- alternate
- counterbalance
probe_args:
description: 'Extra probe arguments, space separated; later flags win (e.g. "--response-bytes 65536 --warmup-sec 3 --arm-shard 1/3")'
required: false
default: ''

permissions:
contents: read

jobs:
ab:
runs-on: ubuntu-latest
timeout-minutes: 300
env:
AB_BASELINE_REF: ${{ inputs.baseline_ref }}
AB_CANDIDATE_REF: ${{ inputs.candidate_ref }}
AB_MODE: ${{ inputs.mode }}
AB_ARM_CONTAINS: ${{ inputs.arm_contains }}
AB_CONCURRENCY: ${{ inputs.concurrency }}
AB_DURATION: ${{ inputs.duration_sec }}
AB_PAIRS: ${{ inputs.pairs }}
AB_COOLDOWN: ${{ inputs.cooldown_sec }}
AB_ORDER: ${{ inputs.order }}
AB_PROBE_ARGS: ${{ inputs.probe_args }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- uses: actions/setup-dotnet@v6
with:
dotnet-version: |
10.0.x

- name: Log runner shape
run: |
set -euo pipefail
echo "os=$(uname -s) $(uname -r)"
echo "nproc=$(nproc)"
lscpu | sed -n '1,20p'

- name: Install libmsquic (HTTP/3 arms)
run: |
set -euo pipefail
. /etc/os-release
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
"https://packages.microsoft.com/config/${ID}/${VERSION_ID}/packages-microsoft-prod.deb" \
-o packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm -f packages-microsoft-prod.deb
sudo find /etc/apt -type f \( -iname '*chrome*' -o -iname '*google*chrome*' \) -print -delete || true
sudo apt-get update || true
sudo apt-get install -y libmsquic

- name: Check out baseline and candidate worktrees
run: |
set -euo pipefail
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*' || true
resolve() {
git rev-parse --verify --quiet "$1^{commit}" \
|| git rev-parse --verify --quiet "origin/$1^{commit}"
}
base_sha="$(resolve "$AB_BASELINE_REF")" || { echo "cannot resolve baseline_ref=$AB_BASELINE_REF" >&2; exit 1; }
cand_ref="${AB_CANDIDATE_REF:-$GITHUB_SHA}"
cand_sha="$(resolve "$cand_ref")" || { echo "cannot resolve candidate_ref=$cand_ref" >&2; exit 1; }
echo "baseline $AB_BASELINE_REF -> $base_sha"
echo "candidate $cand_ref -> $cand_sha"
echo "harness $GITHUB_SHA (tools/RpsLoadProbe overlaid on both sides)"
git worktree add --detach "$RUNNER_TEMP/ab/baseline" "$base_sha"
git worktree add --detach "$RUNNER_TEMP/ab/candidate" "$cand_sha"
{
echo "AB_BASELINE_SHA=$base_sha"
echo "AB_CANDIDATE_SHA=$cand_sha"
} >> "$GITHUB_ENV"

- name: Build the identical probe against each product ref
run: |
set -euo pipefail
for side in baseline candidate; do
wt="$RUNNER_TEMP/ab/$side"
rm -rf "$wt/tools/RpsLoadProbe"
cp -r tools/RpsLoadProbe "$wt/tools/RpsLoadProbe"
dotnet build -c Release "$wt/tools/RpsLoadProbe/RpsLoadProbe.csproj" --warnaserror
mkdir -p "$RUNNER_TEMP/ab-bin"
cp -r "$wt/tools/RpsLoadProbe/bin/Release/net10.0" "$RUNNER_TEMP/ab-bin/$side"
done
ls -la "$RUNNER_TEMP/ab-bin/baseline/RpsLoadProbe" "$RUNNER_TEMP/ab-bin/candidate/RpsLoadProbe"

- name: Paired A/B ramp
run: |
set -euo pipefail
ulimit -n 65535 || true
extra=()
if [ -n "${AB_PROBE_ARGS:-}" ]; then
# shellcheck disable=SC2206
for a in $AB_PROBE_ARGS; do extra+=(--probe-arg "$a"); done
fi
python3 tools/RpsLoadProbe/rps-ab-run.py \
--baseline-probe "$RUNNER_TEMP/ab-bin/baseline/RpsLoadProbe" \
--candidate-probe "$RUNNER_TEMP/ab-bin/candidate/RpsLoadProbe" \
--mode "$AB_MODE" --arm-contains "$AB_ARM_CONTAINS" \
--concurrency "$AB_CONCURRENCY" --duration-sec "$AB_DURATION" \
--pairs "$AB_PAIRS" --cooldown-sec "$AB_COOLDOWN" --order "$AB_ORDER" \
"${extra[@]}" --out "$RUNNER_TEMP/ab-out"

- name: Analyze paired deltas
if: always()
run: |
set -euo pipefail
test -f "$RUNNER_TEMP/ab-out/manifest.json" || exit 0
{
echo "baseline \`$AB_BASELINE_REF\` = \`$AB_BASELINE_SHA\` "
echo "candidate \`${AB_CANDIDATE_REF:-$GITHUB_SHA}\` = \`$AB_CANDIDATE_SHA\` "
echo "harness (identical on both sides) = \`$GITHUB_SHA\`"
echo
} > "$RUNNER_TEMP/ab-out/summary.md"
python3 tools/RpsLoadProbe/rps-ab-analyze.py "$RUNNER_TEMP/ab-out" --markdown "$RUNNER_TEMP/ab-out/analysis.md" > /dev/null
cat "$RUNNER_TEMP/ab-out/analysis.md" >> "$RUNNER_TEMP/ab-out/summary.md"
cat "$RUNNER_TEMP/ab-out/summary.md" >> "$GITHUB_STEP_SUMMARY"
cat "$RUNNER_TEMP/ab-out/summary.md"

- name: Upload A/B artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: rps-ab-${{ github.run_id }}
path: |
${{ runner.temp }}/ab-out/**/*.csv
${{ runner.temp }}/ab-out/**/*.tsv
${{ runner.temp }}/ab-out/**/run.log
${{ runner.temp }}/ab-out/manifest.json
${{ runner.temp }}/ab-out/summary.md
if-no-files-found: error
165 changes: 165 additions & 0 deletions .github/workflows/rps-profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Linux CPU / allocation / perf capture for a few RPS arms (workflow_dispatch only; diagnosis, not publishable RPS).
#
# One arm at a time, fixed concurrency, ~60 s measure per arm. Per arm the capture is
# - perf record -g on the whole proxy process tree (kernel + OpenSSL + runtime + JIT frames named
# via DOTNET_PerfMapEnabled and libcoreclr / libclrjit symbols from the Microsoft symbol server)
# - dotnet-trace dotnet-sampled-thread-time (managed stacks)
# - dotnet-trace gc-verbose -> GCAllocationTick -> bytes allocated per request by type
# - dotnet-counters System.Runtime (alloc rate, GC counts, lock contentions, thread-pool queue)
# Native peers (nginx / HAProxy) get the perf window only: that is the native floor for the same wire.
# summarize-profile.py turns the captures into a markdown table (job summary + artifact).
#
# gh workflow run rps-profile.yml --ref <branch> \
# -f arms='reverse-http1-tls,bare-reverse-http1-tls,nginx-reverse-http1-tls,haproxy-reverse-http1-tls'
#
# Peers use the distro nginx / HAProxy builds (H1 + TLS terminate only); use rps-saturation.yml for
# publishable peer numbers.

name: RPS profile (Linux)

on:
workflow_dispatch:
inputs:
arms:
description: 'Comma-separated single-arm probe modes (e.g. reverse-http1,bare-reverse-http1,nginx-reverse-http1)'
required: true
default: 'reverse-http1-tls,bare-reverse-http1-tls,nginx-reverse-http1-tls,haproxy-reverse-http1-tls'
concurrency:
description: 'Fixed concurrency for every captured arm'
required: true
default: '32'
capture_sec:
description: 'Seconds per capture window (perf, sampled-thread-time, gc-verbose); measure step = 3 windows + slack'
required: true
default: '20'
probe_args:
description: 'Extra probe arguments, space separated (e.g. "--response-bytes 65536")'
required: false
default: ''

permissions:
contents: read

jobs:
profile:
runs-on: ubuntu-latest
timeout-minutes: 120
env:
PF_ARMS: ${{ inputs.arms }}
PF_CONCURRENCY: ${{ inputs.concurrency }}
PF_CAPTURE_SEC: ${{ inputs.capture_sec }}
PF_PROBE_ARGS: ${{ inputs.probe_args }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: |
10.0.x

- name: Log runner shape
run: |
set -euo pipefail
echo "os=$(uname -s) $(uname -r)"
echo "nproc=$(nproc)"
lscpu | sed -n '1,20p'

- name: Install profiling tools, nginx, HAProxy and libmsquic
run: |
set -euo pipefail
sudo find /etc/apt -type f \( -iname '*chrome*' -o -iname '*google*chrome*' \) -print -delete || true
sudo apt-get update || true
sudo apt-get install -y linux-tools-common "linux-tools-$(uname -r)" || sudo apt-get install -y linux-tools-generic || true
# linux-tools-generic ships perf under /usr/lib/linux-tools/<version>/ ; expose one that runs.
perf_bin="$(command -v perf || true)"
if [ -z "$perf_bin" ] || ! "$perf_bin" --version >/dev/null 2>&1; then
cand="$(ls -d /usr/lib/linux-tools/*/perf 2>/dev/null | tail -n 1 || true)"
[ -n "$cand" ] && sudo ln -sf "$cand" /usr/local/bin/perf
fi
perf --version
sudo apt-get install -y nginx haproxy || true
sudo systemctl stop nginx haproxy || true
. /etc/os-release
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
"https://packages.microsoft.com/config/${ID}/${VERSION_ID}/packages-microsoft-prod.deb" \
-o packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm -f packages-microsoft-prod.deb
sudo apt-get update || true
sudo apt-get install -y libmsquic || true
dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-counters
dotnet tool install -g dotnet-symbol
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
nginx -v || true
haproxy -v | head -n 1 || true

- name: Install runtime native symbols (libcoreclr, libclrjit) for perf
run: |
set -euo pipefail
export PATH="$PATH:$HOME/.dotnet/tools"
rt="$(dirname "$(dotnet --list-runtimes | awk '/Microsoft.NETCore.App 10\./ {gsub(/\[|\]/,"",$3); print $3 "/" $2 "/x"}' | tail -n 1)")"
echo "runtime dir: $rt"
mkdir -p "$RUNNER_TEMP/sym"
dotnet-symbol --symbols --output "$RUNNER_TEMP/sym" "$rt/libcoreclr.so" "$rt/libclrjit.so"
for f in libcoreclr libclrjit; do
bid="$(readelf -n "$rt/$f.so" | awk '/Build ID/ {print $3}')"
d="/usr/lib/debug/.build-id/${bid:0:2}"
sudo mkdir -p "$d"
sudo cp "$RUNNER_TEMP/sym/$f.so.dbg" "$d/${bid:2}.debug"
echo "$f build-id $bid"
done

- name: Build probe and allocation-tick reader
run: |
set -euo pipefail
dotnet build -c Release tools/RpsLoadProbe/RpsLoadProbe.csproj --warnaserror
dotnet build -c Release tools/RpsAllocTicks/RpsAllocTicks.csproj

- name: Capture arms
run: |
set -euo pipefail
export PATH="$PATH:$HOME/.dotnet/tools"
ulimit -n 65535 || true
export RPS_ALLOC_TICKS="$PWD/tools/RpsAllocTicks/bin/Release/net10.0/RpsAllocTicks"
extra=()
if [ -n "${PF_PROBE_ARGS:-}" ]; then
# shellcheck disable=SC2206
for a in $PF_PROBE_ARGS; do extra+=(--probe-arg "$a"); done
fi
IFS=',' read -ra arms <<< "$PF_ARMS"
for arm in "${arms[@]}"; do
arm="$(echo "$arm" | xargs)"
[ -z "$arm" ] && continue
echo "::group::profile $arm"
bash tools/RpsLoadProbe/profile-arm.sh --probe "$PWD/tools/RpsLoadProbe/bin/Release/net10.0/RpsLoadProbe" \
--mode "$arm" --out "$RUNNER_TEMP/profile/$arm" --concurrency "$PF_CONCURRENCY" \
--capture-sec "$PF_CAPTURE_SEC" "${extra[@]}" || echo "::warning::profile of $arm failed"
echo "::endgroup::"
sleep 10
done

- name: Summarise
if: always()
run: |
set -euo pipefail
test -d "$RUNNER_TEMP/profile" || exit 0
python3 tools/RpsLoadProbe/summarize-profile.py "$RUNNER_TEMP/profile" --cpus "$(nproc)" \
--markdown "$RUNNER_TEMP/profile/summary.md" > /dev/null
cat "$RUNNER_TEMP/profile/summary.md" >> "$GITHUB_STEP_SUMMARY"

- name: Upload captures
if: always()
uses: actions/upload-artifact@v7
with:
name: rps-profile-${{ github.run_id }}
path: |
${{ runner.temp }}/profile/summary.md
${{ runner.temp }}/profile/**/*.txt
${{ runner.temp }}/profile/**/*.csv
${{ runner.temp }}/profile/**/*.log
${{ runner.temp }}/profile/**/*.tsv
${{ runner.temp }}/profile/**/*.nettrace
${{ runner.temp }}/profile/**/*.speedscope.json
${{ runner.temp }}/profile/**/perf.data.gz
if-no-files-found: error
Loading
Loading