Skip to content

Commit 5420962

Browse files
committed
probe round 2: is the 12 KB/s budget per-connection or per-host?
Round 1 (run 30374882109) settled the why: US-East runner -> file.gitcode.com is a flat ~0.012 MB/s regardless of size and regardless of transport (curl matches urllib exactly), while the SAME runner downloads from gitcode.com at 3.87 MB/s and uploads to github.com at 16 MB/s, and a mainland-CN host reaches 1.84 MB/s to the same endpoint. So it is inbound rate limiting on the GitCode side for this egress, not the client and not cross-border bandwidth per se. At 12 KB/s the 34.8MB asset needs ~48 minutes, which is why four rounds of tuning MIRROR_UPLOAD_TIMEOUT never worked. Round 1 ran out of wall-clock (4x8MB serial alone took 2618s) before reaching the parallel comparison — and that is the measurement that picks the fix: per-connection budget -> N concurrent uploads scale, and parallelising mirror_res.sh's serial per-asset loop helps per-host budget -> parallelism buys nothing and the gitcode leg has to leave GitHub-hosted runners entirely scaling/ measures N=1 vs 4 vs 8 on 1MB files so it finishes either way, and also dumps the upload_url contract to see whether multipart/resumable upload is even offered. variance/ bounds the spread, which decides whether any fixed timeout can be safe.
1 parent 3da87a8 commit 5420962

1 file changed

Lines changed: 97 additions & 138 deletions

File tree

.github/workflows/probe-gitcode-upload.yml

Lines changed: 97 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,34 @@ name: probe-gitcode-upload
22

33
# TEMPORARY diagnostic workflow — delete with this branch.
44
#
5-
# WHY. `publish-ecosystem`'s GitCode leg has now blown its 180s per-asset cap
6-
# on four separate releases (0.0.94 / 0.0.97 / 0.0.105 / 2026.7.28.2), each
7-
# time on the two biggest tarballs, each time costing ~5min of manual
8-
# re-upload. Every previous fix was a guess about WHY it is slow (raise the
9-
# cap, skip-don't-retry, batch verify). This workflow measures instead.
5+
# ROUND 1 (run 30374882109) already settled the "why":
106
#
11-
# Each job isolates ONE hypothesis, and they run in parallel so a single push
12-
# answers all of them:
7+
# US-East runner -> file.gitcode.com upload is a FLAT ~0.012 MB/s (12 KB/s),
8+
# independent of file size AND of transport:
9+
# urllib 1MB 88.8s | 4MB failed | 16MB 1529.9s (0.010-0.011 MB/s)
10+
# curl 1MB 83.1s | 4MB 330.7s | 16MB 1218.4s (0.012-0.013 MB/s)
11+
# 4x8MB serial: 672/650/675/599s, wall 2618s
12+
# Controls from the SAME runner:
13+
# download from gitcode.com 3.87 MB/s (~320x the upload rate)
14+
# upload to github.com 16 MB/s
15+
# Control from a mainland-CN host: 1.84 MB/s (~150x the runner's rate).
1316
#
14-
# baseline Is it the network, the direction, or GitCode specifically?
15-
# Down/up control against GitHub from the same runner.
16-
# sizes-urllib Does throughput collapse with size, or is it ~constant?
17-
# (constant MB/s => a pure bandwidth wall, not a stall.)
18-
# sizes-curl Is Python's read-it-all-then-PUT the bottleneck, or the wire?
19-
# concurrency Is bandwidth per-connection or per-host? If per-connection,
20-
# uploading assets in parallel is the whole fix — mirror_res.sh
21-
# currently uploads them SERIALLY within a host leg.
17+
# So it is neither the Python client (curl matches it) nor cross-border
18+
# bandwidth in general (the same runner downloads from the same host at
19+
# 3.87 MB/s). It is inbound-upload rate limiting on file.gitcode.com for
20+
# this egress. At 12 KB/s the 34.8MB asset needs ~48 MINUTES; the 180s cap
21+
# never had a chance, which is why "raise/loosen the cap" failed four times.
2222
#
23-
# Every job writes NDJSON to the step summary so results are comparable
24-
# without opening logs.
23+
# ROUND 2 (this file) answers the one question that decides the FIX, which
24+
# round 1 ran out of wall-clock before reaching: is the 12 KB/s budget
25+
# PER-CONNECTION or PER-HOST?
26+
# per-connection -> N parallel uploads give ~N x aggregate, and
27+
# parallelising mirror_res.sh's serial per-asset loop is
28+
# a real (if partial) fix.
29+
# per-host -> parallelism buys nothing and the mirror must move off
30+
# GitHub-hosted runners entirely.
31+
# `scaling` measures exactly that. `variance` bounds how much the rate moves
32+
# over time, which decides whether ANY fixed timeout can be safe.
2533

2634
on:
2735
push:
@@ -37,174 +45,125 @@ concurrency:
3745

3846
env:
3947
GTC_REPO: xlings-res/mcpp
40-
# A real, already-mirrored asset used as the download reference (34.8MB).
41-
REF_ASSET: mcpp-2026.7.28.2-linux-x86_64.tar.gz
42-
REF_TAG: 2026.7.28.2
4348

4449
jobs:
45-
# ── H0: characterise the pipe itself, both directions, both hosts ────────
46-
baseline:
47-
name: baselinenetwork + GitHub control
50+
# ── THE decisive test: does concurrency multiply the budget? ─────────────
51+
scaling:
52+
name: scaling1 vs 4 vs 8 concurrent 1MB uploads
4853
runs-on: ubuntu-latest
49-
timeout-minutes: 25
54+
timeout-minutes: 50
5055
env:
5156
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
52-
GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }}
57+
TAG: probe-${{ github.run_id }}-scale
5358
steps:
5459
- uses: actions/checkout@v4
55-
- name: Runner egress identity
56-
run: |
57-
echo "runner public IP / region:"
58-
curl -s --max-time 20 https://ipinfo.io/json || echo "(ipinfo unavailable)"
59-
60-
- name: Connect/TLS timing to each host
61-
run: |
62-
fmt=' dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s code=%{http_code}\n'
63-
for h in https://api.gitcode.com https://gitcode.com https://api.github.com https://github.com; do
64-
echo "$h"
65-
curl -sS -o /dev/null --max-time 60 -w "$fmt" "$h" || echo " (failed)"
66-
done
60+
- name: Create probe release
61+
run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG"
6762

68-
- name: Download throughput — gitcode vs github (same 34.8MB asset)
63+
- name: Inspect the upload_url contract (multipart/resumable available?)
6964
run: |
70-
fmt=' code=%{http_code} bytes=%{size_download} speed=%{speed_download} B/s total=%{time_total}s\n'
71-
echo "gitcode.com:"
72-
curl -sSL -o /dev/null --max-time 900 -w "$fmt" \
73-
"https://gitcode.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true
74-
echo "github.com:"
75-
curl -sSL -o /dev/null --max-time 900 -w "$fmt" \
76-
"https://github.com/${GTC_REPO}/releases/download/${REF_TAG}/${REF_ASSET}" || true
65+
# If the response carries multipart/part-size fields, a resumable
66+
# chunked upload is possible and a stalled transfer could be resumed
67+
# instead of restarted from byte zero.
68+
python3 - <<'PY'
69+
import json, os, urllib.request, urllib.parse
70+
repo, tag = os.environ["GTC_REPO"], os.environ["TAG"]
71+
url = (f"https://api.gitcode.com/api/v5/repos/{repo}/releases/{tag}"
72+
f"/upload_url?file_name=probe-contract.bin")
73+
req = urllib.request.Request(url, headers={
74+
"PRIVATE-TOKEN": os.environ["GITCODE_TOKEN"], "Accept": "application/json"})
75+
info = json.loads(urllib.request.urlopen(req, timeout=60).read())
76+
# Redact the signature so the log stays shareable.
77+
u = urllib.parse.urlparse(info["url"])
78+
print("keys: ", sorted(info.keys()))
79+
print("obs host: ", u.netloc)
80+
print("obs path: ", u.path)
81+
print("query params:", sorted(urllib.parse.parse_qs(u.query).keys()))
82+
print("headers: ", {k: ("<redacted>" if "auth" in k.lower() else v)
83+
for k, v in (info.get("headers") or {}).items()})
84+
PY
7785
78-
- name: Upload control — 32MB to GitHub from this same runner
86+
- name: N=1 (baseline for this run)
7987
run: |
80-
set -x
81-
head -c 33554432 /dev/urandom > probe-github-32m.bin
82-
gh release view "probe-${{ github.run_id }}" -R "$GTC_REPO" >/dev/null 2>&1 \
83-
|| gh release create "probe-${{ github.run_id }}" -R "$GTC_REPO" \
84-
--title "probe ${{ github.run_id }}" --notes "temporary upload probe; safe to delete"
88+
head -c 1048576 /dev/urandom > s1-1.bin
8589
start=$SECONDS
86-
gh release upload "probe-${{ github.run_id }}" probe-github-32m.bin -R "$GTC_REPO" --clobber
87-
echo "GITHUB_UPLOAD_32MB_SECONDS=$((SECONDS - start))"
90+
python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \
91+
--file s1-1.bin --method curl --label "n1" >> results.ndjson
92+
echo "N1_WALL=$((SECONDS - start))" | tee -a wall.txt
8893
89-
# ── H1: is throughput size-dependent (stall) or flat (bandwidth wall)? ───
90-
sizes-urllib:
91-
name: sizes — current urllib transport
92-
runs-on: ubuntu-latest
93-
timeout-minutes: 45
94-
env:
95-
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
96-
TAG: probe-${{ github.run_id }}-urllib
97-
steps:
98-
- uses: actions/checkout@v4
99-
- name: Create probe release
100-
run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG"
101-
- name: Upload 1 / 4 / 16 / 32 / 32 MB (serial)
94+
- name: N=4 concurrent
10295
run: |
103-
# 32MB twice: run-to-run variance matters as much as the mean when
104-
# deciding whether a fixed 180s cap can ever be safe.
105-
for spec in 1 4 16 32 32; do
106-
f="probe-urllib-${spec}m-$RANDOM.bin"
107-
head -c $((spec * 1048576)) /dev/urandom > "$f"
108-
python3 .github/tools/probe_gtc_upload.py \
109-
--repo "$GTC_REPO" --tag "$TAG" --file "$f" \
110-
--method urllib --label "urllib-${spec}MB" >> results.ndjson
111-
rm -f "$f"
96+
for i in 1 2 3 4; do head -c 1048576 /dev/urandom > "s4-$i.bin"; done
97+
start=$SECONDS
98+
for i in 1 2 3 4; do
99+
python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \
100+
--file "s4-$i.bin" --method curl --label "n4-$i" > "s4-$i.json" &
112101
done
113-
- name: Summary
114-
if: always()
115-
run: |
116-
{ echo '### sizes — urllib'; echo '```json'; cat results.ndjson; echo '```'; } \
117-
>> "$GITHUB_STEP_SUMMARY"
102+
wait
103+
echo "N4_WALL=$((SECONDS - start))" | tee -a wall.txt
104+
cat s4-*.json >> results.ndjson
118105
119-
# ── H2: is the Python client the bottleneck, or the wire? ────────────────
120-
sizes-curl:
121-
name: sizes — curl streaming transport
122-
runs-on: ubuntu-latest
123-
timeout-minutes: 45
124-
env:
125-
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
126-
TAG: probe-${{ github.run_id }}-curl
127-
steps:
128-
- uses: actions/checkout@v4
129-
- name: Create probe release
130-
run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG"
131-
- name: Upload 1 / 4 / 16 / 32 / 32 MB (serial)
106+
- name: N=8 concurrent
132107
run: |
133-
for spec in 1 4 16 32 32; do
134-
f="probe-curl-${spec}m-$RANDOM.bin"
135-
head -c $((spec * 1048576)) /dev/urandom > "$f"
136-
python3 .github/tools/probe_gtc_upload.py \
137-
--repo "$GTC_REPO" --tag "$TAG" --file "$f" \
138-
--method curl --label "curl-${spec}MB" >> results.ndjson
139-
rm -f "$f"
108+
for i in 1 2 3 4 5 6 7 8; do head -c 1048576 /dev/urandom > "s8-$i.bin"; done
109+
start=$SECONDS
110+
for i in 1 2 3 4 5 6 7 8; do
111+
python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \
112+
--file "s8-$i.bin" --method curl --label "n8-$i" > "s8-$i.json" &
140113
done
141-
- name: Summary
114+
wait
115+
echo "N8_WALL=$((SECONDS - start))" | tee -a wall.txt
116+
cat s8-*.json >> results.ndjson
117+
118+
- name: Verdict
142119
if: always()
143120
run: |
144-
{ echo '### sizes — curl'; echo '```json'; cat results.ndjson; echo '```'; } \
145-
>> "$GITHUB_STEP_SUMMARY"
121+
# N4_WALL ~= N1_WALL -> per-connection budget: parallelism is the fix.
122+
# N4_WALL ~= 4*N1_WALL -> per-host budget: parallelism buys nothing.
123+
{ echo '### scaling'; echo '```'; cat wall.txt; echo '```';
124+
echo '```json'; cat results.ndjson; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
125+
cat wall.txt
146126
147-
# ── H3: per-connection cap or per-host cap? This decides whether simply
148-
# parallelising mirror_res.sh's serial per-asset loop is the fix. ──
149-
concurrency:
150-
name: concurrency — 4x8MB serial vs parallel
127+
# ── Can ANY fixed timeout be safe? Depends on the spread, not the mean. ──
128+
variance:
129+
name: variance — 8 sequential 1MB uploads
151130
runs-on: ubuntu-latest
152-
timeout-minutes: 45
131+
timeout-minutes: 50
153132
env:
154133
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
155-
TAG: probe-${{ github.run_id }}-conc
134+
TAG: probe-${{ github.run_id }}-var
156135
steps:
157136
- uses: actions/checkout@v4
158137
- name: Create probe release
159138
run: python3 .github/tools/gtc release create "$GTC_REPO" --tag "$TAG" --name "$TAG"
160-
- name: Serial 4x8MB
139+
- name: 8 sequential 1MB uploads
161140
run: |
162-
for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "ser-$i.bin"; done
163-
start=$SECONDS
164-
for i in 1 2 3 4; do
141+
for i in 1 2 3 4 5 6 7 8; do
142+
head -c 1048576 /dev/urandom > "v-$i.bin"
165143
python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \
166-
--file "ser-$i.bin" --method curl --label "serial-$i" >> results.ndjson
144+
--file "v-$i.bin" --method curl --label "v$i" >> results.ndjson
145+
rm -f "v-$i.bin"
167146
done
168-
echo "SERIAL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt
169-
- name: Parallel 4x8MB
170-
run: |
171-
for i in 1 2 3 4; do head -c 8388608 /dev/urandom > "par-$i.bin"; done
172-
start=$SECONDS
173-
for i in 1 2 3 4; do
174-
python3 .github/tools/probe_gtc_upload.py --repo "$GTC_REPO" --tag "$TAG" \
175-
--file "par-$i.bin" --method curl --label "parallel-$i" >> "par-$i.json" &
176-
done
177-
wait
178-
echo "PARALLEL_WALL_SECONDS=$((SECONDS - start))" | tee -a wall.txt
179-
cat par-*.json >> results.ndjson
180147
- name: Summary
181148
if: always()
182149
run: |
183-
{ echo '### concurrency'; echo '```'; cat wall.txt; echo '```';
184-
echo '```json'; cat results.ndjson; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
150+
{ echo '### variance'; echo '```json'; cat results.ndjson; echo '```'; } \
151+
>> "$GITHUB_STEP_SUMMARY"
185152
186-
# ── Leave no garbage on the resource repo ───────────────────────────────
187153
cleanup:
188154
name: cleanup probe tags
189-
needs: [baseline, sizes-urllib, sizes-curl, concurrency]
155+
needs: [scaling, variance]
190156
if: always()
191157
runs-on: ubuntu-latest
192158
timeout-minutes: 10
193159
env:
194160
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
195-
GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }}
196161
steps:
197162
- name: Delete GitCode probe tags (deleting the tag deletes the release)
198163
run: |
199-
for t in "probe-${{ github.run_id }}-urllib" \
200-
"probe-${{ github.run_id }}-curl" \
201-
"probe-${{ github.run_id }}-conc"; do
164+
for t in "probe-${{ github.run_id }}-scale" "probe-${{ github.run_id }}-var"; do
202165
code=$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE \
203166
-H "PRIVATE-TOKEN: $GITCODE_TOKEN" \
204167
"https://api.gitcode.com/api/v5/repos/${GTC_REPO}/tags/${t}" || echo ERR)
205168
echo "delete $t -> $code"
206169
done
207-
- name: Delete GitHub probe release
208-
run: |
209-
gh release delete "probe-${{ github.run_id }}" -R "$GTC_REPO" --yes --cleanup-tag \
210-
|| echo "(nothing to delete)"

0 commit comments

Comments
 (0)