Skip to content

Commit 57dc546

Browse files
feat(tunnel): pipelined polls with adaptive depth and overlapped client reads
Three improvements to full-tunnel throughput and latency: 1. **Overlapped client reads**: tunnel_loop reads from the client socket concurrently with the batch reply wait via tokio::select!, buffering upload data for the next op instead of blocking on a fresh read timeout. 2. **Pipelined polls with seq echo**: add a per-op sequence number echoed by the tunnel-node so the client can reorder out-of-order replies. Sessions with sustained data flow (consecutive_data >= 2) ramp up to MAX_INFLIGHT_PER_SESSION polls in flight, with 1s stagger between sends so they land in separate batches. Drops to serial on first empty reply. 3. **Adaptive pipeline depth**: idle sessions stay at depth 1 (no extra polls). Data-bearing sessions gradually ramp 1→2→3→...→10. At most MAX_ELEVATED_PER_DEPLOYMENT (6) sessions per deployment can be elevated simultaneously, preventing semaphore exhaustion. Elevation slots are released immediately on first empty reply or session close. Wire protocol: BatchOp and TunnelResponse gain an optional `seq` field. Fully backward compatible — old tunnel-nodes ignore the field, new clients fall back to serial (depth 1) when resp.seq is None. Tunnel-node: LONGPOLL_DEADLINE reduced from 15s to 4s for faster poll turnaround while keeping persistent connections (Telegram) stable. Includes bench-pipeline.sh for comparing serial vs pipelined throughput. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ab05c53 commit 57dc546

4 files changed

Lines changed: 666 additions & 176 deletions

File tree

scripts/bench-pipeline.sh

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env bash
2+
#
3+
# bench-pipeline.sh — compare throughput: serial (depth=1) vs pipelined (depth=10)
4+
#
5+
# Builds mhrv-rs twice (patching the INFLIGHT_ACTIVE constant), runs each
6+
# as a local SOCKS5 proxy, downloads through the full tunnel, reports.
7+
#
8+
# Usage:
9+
# ./scripts/bench-pipeline.sh [CONFIG_FILE]
10+
#
11+
# Default: config.json
12+
13+
set -euo pipefail
14+
15+
CONFIG="${1:-config.json}"
16+
RUNS=3
17+
SOCKS_PORT=18088
18+
HTTP_PORT=18087
19+
TEST_URL="https://speed.cloudflare.com/__down?bytes=5000000"
20+
SRC="src/tunnel_client.rs"
21+
TMPDIR_BENCH=$(mktemp -d)
22+
23+
cleanup() {
24+
rm -rf "$TMPDIR_BENCH"
25+
kill $PROXY_PID 2>/dev/null || true
26+
# Restore original constant
27+
sed -i '' "s/^const INFLIGHT_ACTIVE: usize = [0-9]*/const INFLIGHT_ACTIVE: usize = 10/" "$SRC" 2>/dev/null || true
28+
}
29+
trap cleanup EXIT
30+
31+
if [ ! -f "$CONFIG" ]; then
32+
echo "ERROR: Config not found: $CONFIG"
33+
exit 1
34+
fi
35+
36+
echo "╔══════════════════════════════════════════════╗"
37+
echo "║ Pipeline Throughput Benchmark ║"
38+
echo "╠══════════════════════════════════════════════╣"
39+
echo "║ Config: $CONFIG"
40+
echo "║ Test URL: $TEST_URL"
41+
echo "║ Runs: $RUNS per mode"
42+
echo "╚══════════════════════════════════════════════╝"
43+
echo ""
44+
45+
# Write a temp config with our ports
46+
TEMP_CONFIG="$TMPDIR_BENCH/config.json"
47+
python3 -c "
48+
import json
49+
with open('$CONFIG') as f:
50+
c = json.load(f)
51+
c['listen_port'] = $HTTP_PORT
52+
c['socks5_port'] = $SOCKS_PORT
53+
c['log_level'] = 'warn'
54+
with open('$TEMP_CONFIG', 'w') as f:
55+
json.dump(c, f)
56+
"
57+
58+
run_test() {
59+
local label="$1"
60+
local binary="$2"
61+
echo "━━━ $label ━━━"
62+
63+
$binary -c "$TEMP_CONFIG" &
64+
PROXY_PID=$!
65+
sleep 3
66+
67+
if ! kill -0 $PROXY_PID 2>/dev/null; then
68+
echo " ERROR: Proxy failed to start"
69+
return
70+
fi
71+
72+
# Wait for proxy
73+
for attempt in $(seq 1 15); do
74+
if curl -s --socks5-hostname localhost:$SOCKS_PORT --connect-timeout 5 -o /dev/null https://www.google.com 2>/dev/null; then
75+
break
76+
fi
77+
sleep 1
78+
done
79+
80+
local total_bytes=0
81+
local total_time=0
82+
83+
for i in $(seq 1 $RUNS); do
84+
local result
85+
result=$(curl -s --socks5-hostname localhost:$SOCKS_PORT \
86+
-o /dev/null \
87+
-w '%{size_download} %{time_total} %{speed_download}' \
88+
--connect-timeout 30 \
89+
--max-time 90 \
90+
"$TEST_URL" 2>/dev/null || echo "0 999 0")
91+
92+
local bytes time_s speed
93+
bytes=$(echo "$result" | awk '{print $1}')
94+
time_s=$(echo "$result" | awk '{print $2}')
95+
speed=$(echo "$result" | awk '{printf "%.0f", $3/1024}')
96+
97+
total_bytes=$((total_bytes + ${bytes%.*}))
98+
total_time=$(echo "$total_time + $time_s" | bc)
99+
100+
printf " Run %d: %.1fs %s KB/s\n" "$i" "$time_s" "$speed"
101+
done
102+
103+
local avg_speed avg_time
104+
avg_speed=$(echo "scale=1; $total_bytes / $total_time / 1024" | bc 2>/dev/null || echo "0")
105+
avg_time=$(echo "scale=1; $total_time / $RUNS" | bc 2>/dev/null || echo "0")
106+
printf " ➜ Average: %s KB/s (%.1fs per download)\n\n" "$avg_speed" "$avg_time"
107+
108+
kill $PROXY_PID 2>/dev/null || true
109+
wait $PROXY_PID 2>/dev/null || true
110+
sleep 1
111+
112+
echo "$label|$avg_speed|$avg_time" >> "$TMPDIR_BENCH/results.txt"
113+
}
114+
115+
# Build serial (depth=1)
116+
echo "Building serial mode (INFLIGHT_ACTIVE=1)..."
117+
sed -i '' "s/^const INFLIGHT_ACTIVE: usize = [0-9]*/const INFLIGHT_ACTIVE: usize = 1/" "$SRC"
118+
cargo build --release 2>&1 | tail -1
119+
cp target/release/mhrv-rs "$TMPDIR_BENCH/mhrv-serial"
120+
121+
# Build pipelined (depth=10)
122+
echo "Building pipelined mode (INFLIGHT_ACTIVE=10)..."
123+
sed -i '' "s/^const INFLIGHT_ACTIVE: usize = [0-9]*/const INFLIGHT_ACTIVE: usize = 10/" "$SRC"
124+
cargo build --release 2>&1 | tail -1
125+
cp target/release/mhrv-rs "$TMPDIR_BENCH/mhrv-pipelined"
126+
127+
echo ""
128+
129+
# Run tests
130+
run_test "Serial (depth=1)" "$TMPDIR_BENCH/mhrv-serial"
131+
run_test "Pipelined (depth=10)" "$TMPDIR_BENCH/mhrv-pipelined"
132+
133+
# Summary
134+
echo "╔══════════════════════════════════════════════╗"
135+
echo "║ RESULTS ║"
136+
echo "╠══════════════════════════════════════════════╣"
137+
while IFS='|' read -r label speed time; do
138+
printf "║ %-25s %6s KB/s %5ss\n" "$label" "$speed" "$time"
139+
done < "$TMPDIR_BENCH/results.txt"
140+
echo "╚══════════════════════════════════════════════╝"

src/domain_fronter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,8 @@ pub struct TunnelResponse {
514514
/// `e` only when this is `None` and compatibility is needed.
515515
#[serde(default)]
516516
pub code: Option<String>,
517+
#[serde(default)]
518+
pub seq: Option<u64>,
517519
}
518520

519521
/// A single op in a batch tunnel request.
@@ -528,6 +530,8 @@ pub struct BatchOp {
528530
pub port: Option<u16>,
529531
#[serde(skip_serializing_if = "Option::is_none")]
530532
pub d: Option<String>,
533+
#[serde(skip_serializing_if = "Option::is_none")]
534+
pub seq: Option<u64>,
531535
}
532536

533537
/// Batch tunnel response from Apps Script / tunnel node.

0 commit comments

Comments
 (0)