Skip to content

Commit 994dd0b

Browse files
tune: lower coalesce/settle step from 40 → 10 ms, raise tunnel-node settle max to 1 s (#674)
The batch coalesce step controls how long the client (and the tunnel-node's straggler settle) waits between checking for more ops to pack into the same batch. At 40 ms the wait was conservative — good for packing uploads but needlessly slow on the download path where the tunnel-node round-trip, not coalescing, is the bottleneck. Lowering the step to 10 ms means we fire batches almost immediately when there's nothing else queued, cutting ~30 ms of dead air on every download-dominated round-trip. When both sides DO have data in flight (uploads, bursty page loads), the adaptive reset still works: each arriving op resets the 10 ms step timer, so a rapid burst naturally coalesces up to the 1 s hard cap without wasting quota on many small batches. In short: don't wait when there's nothing to wait for; batch aggressively when there is. Client side: - DEFAULT_COALESCE_STEP_MS 40 → 10 ms - DEFAULT_COALESCE_MAX_MS unchanged at 1000 ms Tunnel-node side: - STRAGGLER_SETTLE_STEP 40 → 10 ms (matches client step) - STRAGGLER_SETTLE_MAX 500 → 1000 ms (more room to pack straggler responses when upstream targets reply at different speeds — saves Apps Script quota on the return leg) Users who prefer the old behaviour can set "coalesce_step_ms": 40 in config.json. Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d53399d commit 994dd0b

5 files changed

Lines changed: 19 additions & 8 deletions

File tree

android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ data class MhrvConfig(
9696
val verifySsl: Boolean = true,
9797
val logLevel: String = "info",
9898
val parallelRelay: Int = 1,
99-
val coalesceStepMs: Int = 40,
99+
val coalesceStepMs: Int = 10,
100100
val coalesceMaxMs: Int = 1000,
101101
val upstreamSocks5: String = "",
102102

@@ -210,7 +210,7 @@ data class MhrvConfig(
210210
put("verify_ssl", verifySsl)
211211
put("log_level", logLevel)
212212
put("parallel_relay", parallelRelay)
213-
if (coalesceStepMs != 40) put("coalesce_step_ms", coalesceStepMs)
213+
if (coalesceStepMs != 10) put("coalesce_step_ms", coalesceStepMs)
214214
if (coalesceMaxMs != 1000) put("coalesce_max_ms", coalesceMaxMs)
215215
if (upstreamSocks5.isNotBlank()) {
216216
put("upstream_socks5", upstreamSocks5.trim())
@@ -422,7 +422,7 @@ object ConfigStore {
422422
verifySsl = obj.optBoolean("verify_ssl", true),
423423
logLevel = obj.optString("log_level", "info"),
424424
parallelRelay = obj.optInt("parallel_relay", 1),
425-
coalesceStepMs = obj.optInt("coalesce_step_ms", 40),
425+
coalesceStepMs = obj.optInt("coalesce_step_ms", 10),
426426
coalesceMaxMs = obj.optInt("coalesce_max_ms", 1000),
427427
upstreamSocks5 = obj.optString("upstream_socks5", ""),
428428
passthroughHosts = obj.optJSONArray("passthrough_hosts")?.let { arr ->

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub struct Config {
104104
pub parallel_relay: u8,
105105
/// Adaptive batch coalesce: after each op arrives, wait this many ms
106106
/// for more ops before firing the batch. Resets on every arrival.
107-
/// 0 = use compiled default (40ms).
107+
/// 0 = use compiled default (10ms).
108108
#[serde(default)]
109109
pub coalesce_step_ms: u16,
110110
/// Hard cap on total coalesce wait (ms). 0 = use compiled default (1000ms).

src/proxy_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ impl ProxyServer {
518518
mitm,
519519
rewrite_ctx,
520520
tunnel_mux: None, // initialized in run() inside the tokio runtime
521-
coalesce_step_ms: if config.coalesce_step_ms > 0 { config.coalesce_step_ms as u64 } else { 40 },
521+
coalesce_step_ms: if config.coalesce_step_ms > 0 { config.coalesce_step_ms as u64 } else { 10 },
522522
coalesce_max_ms: if config.coalesce_max_ms > 0 { config.coalesce_max_ms as u64 } else { 1000 },
523523
})
524524
}

src/tunnel_client.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,18 @@ const CLIENT_FIRST_DATA_WAIT: Duration = Duration::from_millis(50);
5959
/// Adaptive coalesce defaults: after each new op arrives, wait another
6060
/// step for more ops. Resets on every arrival, up to max from the first
6161
/// op. Overridable via config `coalesce_step_ms` / `coalesce_max_ms`.
62-
const DEFAULT_COALESCE_STEP_MS: u64 = 40;
62+
///
63+
/// 10 ms is enough to catch ops that arrive in the same event-loop tick
64+
/// (e.g. a browser opening 6 parallel connections) without adding
65+
/// perceptible latency to downloads where the tunnel-node reply — not
66+
/// coalescing — is the real bottleneck. When both sides *do* have data
67+
/// in flight (uploads, bursty page loads), the adaptive reset still
68+
/// packs batches efficiently: each arriving op resets the step timer, so
69+
/// a rapid burst naturally coalesces up to `DEFAULT_COALESCE_MAX_MS`
70+
/// without an explicit upload/download distinction. The net effect is
71+
/// "don't wait when there's nothing to wait for; batch aggressively when
72+
/// there is."
73+
const DEFAULT_COALESCE_STEP_MS: u64 = 10;
6374
const DEFAULT_COALESCE_MAX_MS: u64 = 1000;
6475

6576
/// Structured error code the tunnel-node returns when it doesn't know the

tunnel-node/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ const ACTIVE_DRAIN_DEADLINE: Duration = Duration::from_millis(350);
4747
/// still arriving. Stops when no new data arrived in the last STEP (the
4848
/// burst is over) or MAX is reached. Packing more session responses into
4949
/// one batch saves quota on high-latency relays (~1.5s Apps Script overhead).
50-
const STRAGGLER_SETTLE_STEP: Duration = Duration::from_millis(40);
51-
const STRAGGLER_SETTLE_MAX: Duration = Duration::from_millis(500);
50+
const STRAGGLER_SETTLE_STEP: Duration = Duration::from_millis(10);
51+
const STRAGGLER_SETTLE_MAX: Duration = Duration::from_millis(1000);
5252

5353
/// Drain-phase deadline when the batch is a pure poll (no writes, no new
5454
/// connections — clients just asking "any push data?"). Holding the

0 commit comments

Comments
 (0)