@@ -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
2634on :
2735 push :
@@ -37,174 +45,125 @@ concurrency:
3745
3846env :
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
4449jobs :
45- # ── H0: characterise the pipe itself, both directions, both hosts ────────
46- baseline :
47- name : baseline — network + GitHub control
50+ # ── THE decisive test: does concurrency multiply the budget? ───── ────────
51+ scaling :
52+ name : scaling — 1 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