ZaStoGram/Tools/web_proxy_bench/bench.sh
loop-uh 78f4107005
Some checks failed
ZaStoGram source guards / guards (push) Failing after 22s
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Failing after 2m2s
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Failing after 2m7s
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Failing after 2m4s
Переписать путь данных WEB-прокси: один поток, окна по BDP
Замеры на стенде (эмулятор Android 16, настоящий WebView и страница
моста, tproxy-server в режиме websocket за nginx, netem) показали, что
канал насыщают и upstream DrKLO (8ad4ac9ba), и 12f2a119f, а теряется
не скорость, а очередь: фиксированные 1 МиБ выгрузки и 2 МиБ кредита
ограничивали скорость на длинных RTT (300 мс: 2,7 против 4,1 МБ/с), а
без окон upstream держит перед чатом секунды данных. Узкое место
Desktop (base64 eval по одному кадру) на Android отсутствует.

- WebProxyEngine: один поток на селекторе вместо потока на сокет,
  исполнителя носителя и общей блокировки. Сокет tgnet читается только
  по гранту планировщика, прямо в пакет для страницы; приём пишется в
  tgnet без блокировки, зависший читатель не держит остальные потоки.
- Все кадры прохода уходят странице одним сообщением (до 96 КиБ).
- Граница со страницей — WebMessagePort, когда WebView умеет: кадры
  страницы не разбираются в UI-потоке. Иначе, или если порт не ожил за
  15 с, — прежний слушатель; нонс и точный origin проверяются в обоих.
- Одно управление потоком: окно релея плюс адаптивные окна выгрузки и
  загрузки (WebProxyFlow.AdaptiveWindow, как у Desktop, с отличиями по
  замерам: запас очереди от 200 мс, рост как slow start, слив только
  при подтверждённой очереди, пол min(2 МиБ, 0,8 с скорости)).
- Выгрузка снова по четырём соединениям, как в upstream.
- Медиа через MTProxy/WEB: знак DC в заголовке берётся по тому же
  правилу, что и медиа-ключ (hasMediaAddress), иначе медиа-ключ мог
  уйти в обычный кластер и получить -404 сразу после создания.

Итог против upstream на 24 сценариях: скорость в пределах ±1–2 %
(загрузка 128 КиБ×4 при 50 мс +7 %, смешанный трафик в шуме),
задержка чата при полной выгрузке 2,3–6,3 с → 0,37–0,88 с, при
загрузке 1,3–3,3 с → 0,42–0,89 с. Стенд — Tools/web_proxy_bench,
JVM-тесты движка — Tools/web_proxy_flow_tests.
2026-09-24 00:47:40 +03:00

248 lines
12 KiB
Shell
Executable file

#!/bin/bash
# WEB proxy bench: the app's org.telegram.proxy classes in a small harness APK
# on an Android emulator, a real WebView loading the real bridge page from a
# real tproxy-server (websocket carrier) behind nginx, all traffic shaped by
# netem. Only tgnet (Bench.java, a loopback workload) and Telegram
# (backend.py, a pipelined RPC server) are stand-ins. No Telegram account.
#
# TPROXY_SERVER=/path/to/tproxy-server ./bench.sh up # netns relay, backend, CONNECT proxy
# WPB_AVD=<avd> ./bench.sh emulator # headless emulator (KVM)
# ./bench.sh shape 150 50mbit # RTT ms, rate (or "none")
# ./bench.sh build <dir-with-WebProxy*.java | git-rev> out.apk
# ./bench.sh install out.apk # install + AOT compile
# ./bench.sh run label up=32 upPart=524288 upParallel=64 # one run -> JSON line
# ./bench.sh matrix OUT REPS apkA apkB ... # conditions x workloads
# ./bench.sh summary OUT
# ./bench.sh down
#
# Run parameters (--es key value): mode=java|page, idle, dc (backend delay
# ms), up/down (MiB), upPart/upParallel/upConns, downPart/downParallel/
# downConns, ping (ms), uiload (busy ms per 16 ms frame), stuck (MiB for a
# connection whose reader stops), recover (seconds of reconnecting pings).
# Requires: sudo, ip/tc, nginx, openssl, python3, adb; Android SDK
# at $ANDROID_HOME (build-tools 36, platform 36) and androidx.webkit 1.14 in
# the Gradle cache.
set -euo pipefail
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO=$(cd "$HERE/../.." && pwd)
WORK=${WPB_WORK:-/tmp/wpbench}
NS=wpbench
PROXY_PORT=${WPB_PROXY_PORT:-28444}
SERIAL=emulator-${WPB_EMU_PORT:-5580}
SDK=${ANDROID_HOME:-/opt/android-sdk}
BT=$SDK/build-tools/36.0.0
AJ=$SDK/platforms/android-36/android.jar
export ANDROID_SERIAL=$SERIAL
mkdir -p "$WORK"
libs() {
local L=$WORK/libs
[ -f "$L/webkit.jar" ] && { echo "$L"; return; }
mkdir -p "$L"
local aar
aar=$(find ~/.gradle/caches/modules-2/files-2.1/androidx.webkit/webkit/1.14.0 -name 'webkit-1.14.0.aar' | head -1)
(cd "$L" && unzip -o -q "$aar" classes.jar && mv classes.jar webkit.jar)
cp "$(find ~/.gradle/caches/modules-2/files-2.1/androidx.annotation/annotation-jvm -name 'annotation-jvm-*.jar' ! -name '*sources*' | head -1)" "$L/annotation.jar"
echo "$L"
}
cert() {
[ -f "$WORK/cert.pem" ] && return
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=w.bench.test \
-addext subjectAltName=DNS:w.bench.test -addext basicConstraints=critical,CA:TRUE \
-keyout "$WORK/key.pem" -out "$WORK/cert.pem" 2>/dev/null
}
cmd_up() {
: "${TPROXY_SERVER:?set TPROXY_SERVER to a tproxy-server binary}"
cert
mkdir -p "$WORK/site" "$WORK/nginx/logs" "$WORK/nginx/tmp"
echo "bench" > "$WORK/site/index.html"
head -c 32 /dev/urandom > "$WORK/token.key"
cat > "$WORK/profiles.json" <<EOF
{"profiles":[{"name":"p-websocket","secret":"000000000000000000000000000000a2","backend":"127.0.0.1:2398","carrier_mode":"websocket"}]}
EOF
cat > "$WORK/config.json" <<EOF
{"public_hostname":"w.bench.test","base_path":"","listen":"127.0.0.1:8080","admin_listen":"127.0.0.1:8081",
"public_dir":"$WORK/site","static_routes":"exact","token_key_file":"$WORK/token.key","profiles_file":"$WORK/profiles.json",
"limits":{"max_profiles":8,"max_sessions_global":256,"max_streams_global":4096,"max_backend_dials_in_flight":256,
"new_sessions_per_minute":3000,"new_sessions_burst":512,"new_streams_per_minute":30000,"new_streams_burst":2048,
"max_bootstraps_global":256,"new_bootstraps_per_minute":6000,"new_bootstraps_burst":1024,"max_pending_items_global":1048576}}
EOF
chmod 600 "$WORK"/*.json "$WORK/token.key" "$WORK/key.pem"
# Same vhost as the deployed nodes: body buffers raised, request and
# response buffering off, WebSocket upgrade.
cat > "$WORK/nginx/nginx.conf" <<EOF
worker_processes 2;
pid $WORK/nginx/nginx.pid;
error_log $WORK/nginx/logs/error.log warn;
events { worker_connections 1024; }
http {
access_log off;
client_body_temp_path $WORK/nginx/tmp/body; proxy_temp_path $WORK/nginx/tmp/proxy;
fastcgi_temp_path $WORK/nginx/tmp/f; uwsgi_temp_path $WORK/nginx/tmp/u; scgi_temp_path $WORK/nginx/tmp/s;
map \$http_upgrade \$conn_upgrade { default upgrade; '' close; }
server {
listen 10.78.0.1:443 ssl; http2 on; server_name w.bench.test;
ssl_certificate $WORK/cert.pem; ssl_certificate_key $WORK/key.pem; ssl_protocols TLSv1.2 TLSv1.3;
http2_body_preread_size 1m;
location / {
proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1;
proxy_set_header Host \$host; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$conn_upgrade;
proxy_read_timeout 180s; proxy_send_timeout 180s;
client_max_body_size 4m; client_body_buffer_size 4m;
proxy_request_buffering off; proxy_buffering off;
}
}
}
EOF
sudo ip netns add $NS
sudo ip link add wpbh type veth peer name wpbn
sudo ip link set wpbn netns $NS
sudo ip addr add 10.78.0.2/24 dev wpbh; sudo ip link set wpbh up
sudo ip netns exec $NS ip addr add 10.78.0.1/24 dev wpbn
sudo ip netns exec $NS ip link set wpbn up
sudo ip netns exec $NS ip link set lo up
sudo ip netns exec $NS sysctl -qw net.ipv4.ip_unprivileged_port_start=0
sudo ip netns exec $NS sudo -u "$(id -un)" bash -c "cd '$WORK'; \
nohup python3 '$HERE/backend.py' 2398 > backend.out 2>&1 & \
nohup '$TPROXY_SERVER' -config '$WORK/config.json' > tproxy.out 2>&1 & \
/usr/sbin/nginx -p '$WORK/nginx' -c '$WORK/nginx/nginx.conf'"
(setsid nohup python3 "$HERE/connect_proxy.py" "$PROXY_PORT" > "$WORK/connect_proxy.out" 2>&1 &)
cmd_shape 50 50mbit
}
cmd_down() {
for p in $(pgrep -f "connect_proxy.py $PROXY_PORT"); do kill "$p"; done
sudo ip netns pids $NS 2>/dev/null | xargs -r sudo kill
sleep 0.5
sudo ip netns del $NS 2>/dev/null || true
sudo ip link del wpbh 2>/dev/null || true
}
cmd_shape() {
local half extra=()
half="$(python3 -c "print($1/2)")ms"
[ "$2" != none ] && extra=(rate "$2")
sudo tc qdisc replace dev wpbh root netem delay "$half" "${extra[@]}" limit 100000
sudo ip netns exec $NS tc qdisc replace dev wpbn root netem delay "$half" "${extra[@]}" limit 100000
}
cmd_emulator() {
: "${WPB_AVD:?set WPB_AVD to an x86_64 google_apis AVD}"
(setsid nohup "$SDK/emulator/emulator" -avd "$WPB_AVD" -read-only -no-snapshot -no-window -no-audio \
-no-boot-anim -gpu swiftshader_indirect -port "${WPB_EMU_PORT:-5580}" -cores 4 -memory 3072 \
> "$WORK/emulator.log" 2>&1 &)
adb wait-for-device
until [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do sleep 2; done
adb root >/dev/null; sleep 2
}
# build <dir with WebProxy*.java | git rev> <out.apk>
cmd_build() {
local src=$1 out=$2 L W
L=$(libs); W=$(mktemp -d); cert
mkdir -p "$W/src/org/telegram/proxy" "$W/classes" "$W/dex" "$W/res/raw" "$W/res/xml"
if [ -d "$src" ]; then
cp "$src"/WebProxy*.java "$W/src/org/telegram/proxy/"
else
for f in $(git -C "$REPO" ls-tree --name-only "$src" TMessagesProj/src/main/java/org/telegram/proxy/ | grep '/WebProxy'); do
git -C "$REPO" show "$src:$f" > "$W/src/org/telegram/proxy/$(basename "$f")"
done
fi
rm -f "$W/src/org/telegram/proxy/WebProxyConnectionTester.java"
cp "$WORK/cert.pem" "$W/res/raw/benchca.pem"
cp "$HERE/android/res/xml/nsc.xml" "$W/res/xml/"
javac -nowarn --release 8 -cp "$AJ:$L/webkit.jar:$L/annotation.jar" -d "$W/classes" \
$(find "$W/src" "$HERE/android/src" "$HERE/android/stubs" -name '*.java') 2>&1 | grep -v '^Note:' || true
[ -f "$W/classes/org/zastogram/wpbench/BenchActivity.class" ] || { echo "compile failed" >&2; exit 1; }
"$BT/d8" --min-api 24 --lib "$AJ" --output "$W/dex" $(find "$W/classes" -name '*.class') "$L/webkit.jar" 2>&1 | grep -iv warning || true
"$BT/aapt2" compile --dir "$W/res" -o "$W/res.zip"
"$BT/aapt2" link -o "$W/base.apk" --manifest "$HERE/android/AndroidManifest.xml" -I "$AJ" -A "$HERE/android/assets" "$W/res.zip"
(cd "$W/dex" && zip -q "$W/base.apk" classes*.dex)
"$BT/zipalign" -f 4 "$W/base.apk" "$W/aligned.apk"
[ -f "$WORK/debug.keystore" ] || keytool -genkeypair -keystore "$WORK/debug.keystore" -storepass android -keypass android \
-alias d -keyalg RSA -dname CN=bench -validity 3650 >/dev/null 2>&1
"$BT/apksigner" sign --ks "$WORK/debug.keystore" --ks-pass pass:android --out "$out" "$W/aligned.apk"
rm -rf "$W"
echo "built $out"
}
cmd_install() {
if ! adb install -r "$1" >/dev/null 2>&1; then
# Signed by another bench work dir: replace it.
adb uninstall org.zastogram.wpbench >/dev/null 2>&1 || true
adb install "$1" >/dev/null
fi
adb shell cmd package compile -m speed -f org.zastogram.wpbench >/dev/null
}
# run label key=value ...: one run, prints {"label","args","renderer_cpu_ms","r":{...}}
cmd_run() {
local label=$1 extras=() line=""
shift
for kv in "$@"; do extras+=(--es "${kv%%=*}" "${kv#*=}"); done
adb logcat -c
adb shell am start -S -W -n org.zastogram.wpbench/.BenchActivity "${extras[@]}" >/dev/null
for _ in $(seq 1 400); do
line=$(adb logcat -d -s WPBENCH:I | grep RESULT | tail -1)
[ -n "$line" ] && break
sleep 1
done
local rcpu
rcpu=$(adb shell 'for p in $(pgrep -f sandboxed); do cat /proc/$p/stat; done' | awk '{s+=($14+$15)*10} END {print s+0}')
echo "{\"label\":\"$label\",\"args\":\"$*\",\"renderer_cpu_ms\":$rcpu,\"r\":${line#*RESULT }}"
}
# matrix OUT REPS apk...: RTT/rate conditions x app-like and raw workloads
cmd_matrix() {
local out=$1 reps=$2 cond c apk name
shift 2
for cond in "50 50mbit" "150 50mbit" "300 50mbit" "100 20mbit"; do
cmd_shape $cond
c=${cond// /-}
for apk in "$@"; do
name=$(basename "$apk" .apk)
cmd_install "$apk"
# app*: FileUploadOperation (128 KiB x 16 over 4 upload
# connections) and FileLoadOperation (128 KiB x 4, or 512 KiB x 8
# with experimental params); raw*: far more in flight than any
# window, the transport's own ceiling.
for _ in $(seq 1 "$reps"); do
cmd_run "$name-$c-appup" idle=5 up=32 upConns=4 >> "$out"
cmd_run "$name-$c-rawup" idle=3 up=32 upPart=524288 upParallel=64 upConns=4 >> "$out"
cmd_run "$name-$c-appdown" idle=3 down=32 >> "$out"
cmd_run "$name-$c-appdown8" idle=3 down=32 downPart=524288 downParallel=8 >> "$out"
cmd_run "$name-$c-rawdown" idle=3 down=32 downPart=524288 downParallel=32 >> "$out"
cmd_run "$name-$c-mixed" idle=3 up=16 upConns=4 down=16 downPart=524288 downParallel=8 >> "$out"
done
done
done
}
cmd_summary() {
python3 - "$1" <<'EOF'
import collections, json, statistics, sys
rows = collections.defaultdict(list)
for line in open(sys.argv[1]):
if line.startswith("{"):
d = json.loads(line)
rows[d["label"]].append(d["r"])
def med(values):
values = [v for v in values if v is not None]
return round(statistics.median(values), 2) if values else None
for label, runs in rows.items():
get = lambda f: med([f(r) for r in runs])
print(f"{label:28} n={len(runs)} up={get(lambda r: r.get('up_MBps'))} down={get(lambda r: r.get('down_MBps'))}"
f" idle={get(lambda r: (r.get('idle_ms') or {}).get('p50'))}"
f" loaded_p50={get(lambda r: (r.get('loaded_ms') or {}).get('p50'))}"
f" loaded_p90={get(lambda r: (r.get('loaded_ms') or {}).get('p90'))}"
f" cpu/MB={get(lambda r: r.get('cpu_ms_per_MB'))} main/MB={get(lambda r: r.get('main_ms_per_MB'))}"
+ ("" if all(r.get("ok") for r in runs) else " FAILED"))
EOF
}
case "${1:-}" in
up|down|shape|emulator|build|install|run|matrix|summary) c=$1; shift; "cmd_$c" "$@" ;;
*) sed -n '2,24p' "$0"; exit 2 ;;
esac