All Implemented Interfaces:
Runnable

public class Tunerextends SimpleTimer2.TimedEvent
General-purpose adaptive tuner. Observes network and system stats, adjusts tunable parameters to optimize router performance.

Runs every 5 seconds via SimpleTimer2. Each tick samples transport worker-stage CPU and updates the transport thread pool params; every 15th-second tick runs the full cycle for all other params. Each parameter implements an AIMD-style feedback loop bounded within safe ranges:

  1. Read observed stat (60s rolling average)
  2. Compute target value from observed stat + cross-references
  3. Clamp target to [min, max]
  4. Apply health-based dampening when system is degraded
  5. Accelerate rollback when param diverges from default
  6. Call applyValue() to push the new value to the router

New parameters are added by extending Tuner.BaseParam and implementing:

  • applyValue(int) — push value to router subsystem
  • getRuntimeValue() — read current value from subsystem
  • getObservedStat(RouterContext) — read primary signal
  • computeTarget(double) — feedback logic with cross-refs

Params can be tuned live via the console UI or persisted to autotune.config (separate from router.config). Auto-revert to factory defaults triggers when system health drops below 0.3.

  • Field Details

    • _context

      private final RouterContext _context
      The router context supplying stats and config.
    • _log

      private final Log _log
      Logger for this class.
    • _params

      private final List<Tuner.TunableParam> _params
      The tunable parameters, in registration order.
    • _autotune

      private final Tuner.AutotuneConfig _autotune
      The autotune settings read from the config.
    • _fastParams

      private final Set<Tuner.TunableParam> _fastParams
      Transport worker thread params updated every 5s instead of the 15s cycle.
    • _lastFastCycle

      private long _lastFastCycle
      Time of the last fast tuning pass.
    • _lastSlowCycle

      private long _lastSlowCycle
      Time of the last slow tuning pass.
    • _lastHealth

      private Tuner.SystemHealth _lastHealth
      Health from the most recent slow cycle, shared with fast-cycle params.
    • STAT_PERIOD

      static final long STAT_PERIOD
      Period for short-term bandwidth stats.
      See Also:
    • STAT_PERIOD_LONG

      static final long STAT_PERIOD_LONG
      Period for long-term bandwidth stats.
      See Also:
    • MAX_HISTORY

      static final int MAX_HISTORY
      Max history samples for sparklines (120 samples @ 15s = 30min; transport thread params sample every 5s = 10min).
      See Also:
    • STAGE_STAT_PREFIX

      static final String STAGE_STAT_PREFIX
      Stat prefix for per-stage CPU usage (pct of one core, 100 = full core).
      See Also:
    • CPU_STAGES

      private static final String[] CPU_STAGES
      Transport worker thread name prefixes tracked for CPU saturation.
    • _threadMXBean

      private static final ThreadMXBean _threadMXBean
      Per-thread CPU time source; getThreadCpuTime() returns -1 on unsupported JVMs.
    • _stageCpu

      private final Map<String, Tuner.StageCpu> _stageCpu
      Per-stage CPU state, updated each cycle by sampleStageCpu().
    • _threadCpu

      private final Tuner.ThreadCpuTable _threadCpu
      Per-thread stage and previous CPU time, keyed by thread id.
    • _unseenThreadIds

      private long[] _unseenThreadIds
      Scratch buffer for the name lookups that resolve newly seen threads.
    • _lastCpuSampleMs

      private long _lastCpuSampleMs
      Wall clock of the previous CPU sample cycle.
    • internalQueueSize

      private static volatile int internalQueueSize
      I2CP internal queue size — static so ClientManager can read it without circular dep.
    • untestedMultiplier

      private static volatile int untestedMultiplier
      Untested tunnel cap multiplier, tuned in-memory only. No subsystem consumer reads i2p.tunnel.untestedMultiplier, so the value lives in this static (not router.config) purely to track the last autotuned setting across applyValue/getRuntimeValue calls. -1 = no tuned value yet; use the config-or-default path.
    • maxDispatchAgeMs

      private static volatile int maxDispatchAgeMs
      Max time (ms) a message may sit in the outbound dispatch queue before being dropped.
    • codelTargetMs

      private static volatile int codelTargetMs
      The last autotuned CoDel target and interval, in ms. The queues carry the value per instance and offer no reading, and the tuning path never writes the router.codelTarget / router.codelInterval config properties, so reading those back reported a value that never moved after an apply. Like untestedMultiplier these track the last autotuned setting only. -1 = no tuned value yet; use the config-or-default path.
    • codelIntervalMs

      private static volatile int codelIntervalMs
      Cached CoDel interval in ms, -1 until first set.
    • handlerThreadPriority

      private static volatile int handlerThreadPriority
      Priority for I/O handler threads — boosted under load, reduced when idle.
    • testClientBudget

      private static volatile int testClientBudget
      Per-pool test budget for client tunnels — high enough to never throttle. Global caps (maxQueuedTests, hardLimit) protect the job queue instead.
    • testRetestBackoff

      private static volatile int testRetestBackoff
      Retest delay backoff multiplier (percent, 100 = 1x). Increases under job queue pressure to free test capacity for UNTESTED tunnels. Range 100 (no backoff) to 800 (8x).
    • buildFailureBuffer

      private static volatile int buildFailureBuffer
      Extra tunnels to build beyond target per pool when build failure rate is high. Computed from build success rate. Range 0-10.
    • initialRtoFloor

      private static volatile int initialRtoFloor
      Minimum INITIAL_RTO target (ms) in the Tuner's computeTarget(double). Prevents the Tuner from setting RTO too low, which causes spurious retransmits and wasted bandwidth. Default 6000ms — accommodates typical I2P RTT up to ~3s without premature retransmit (coherent with the 9000ms streaming-side default); the Tuner raises from there for higher-latency paths.
    • SUB_TRANSPORT

      public static final String SUB_TRANSPORT
      Subsystem labels for grouping in the UI.
      See Also:
    • SUB_TUNNEL

      public static final String SUB_TUNNEL
      Tunnel subsystem identifier.
      See Also:
    • SUB_STREAMING

      public static final String SUB_STREAMING
      Streaming subsystem identifier.
      See Also:
    • SUB_I2CP

      public static final String SUB_I2CP
      I2CP subsystem identifier.
      See Also:
    • SUB_CONGESTION

      public static final String SUB_CONGESTION
      Congestion subsystem identifier.
      See Also:
    • SUB_CRYPTO

      public static final String SUB_CRYPTO
      Crypto subsystem identifier.
      See Also:
    • SUB_ROUTER

      public static final String SUB_ROUTER
      Router subsystem identifier.
      See Also:
    • SUB_NETDB

      public static final String SUB_NETDB
      NetDB subsystem identifier.
      See Also:
    • SUB_PEER

      public static final String SUB_PEER
      Peers subsystem identifier.
      See Also:
    • SUB_TRANSIT

      public static final String SUB_TRANSIT
      Transit subsystem identifier.
      See Also:
    • MAX_MEMORY

      private static final long MAX_MEMORY
      Maximum heap available, in bytes.
    • CORES

      private static final int CORES
      CPU cores available.
    • IS_SLOW

      private static final boolean IS_SLOW
      True on a machine too slow for the default budgets.
    • MEM_FACTOR

      private static final int MEM_FACTOR
      Memory scale factor, one per 256 MB of heap, at least 1.
    • CORE_FACTOR

      private static final int CORE_FACTOR
      Core count for scaling, at least 1.
    • MLKEM_FACTOR

      private static final int MLKEM_FACTOR
      ML-KEM precalc min/max — each pair ~3.5KB, scale with cores and memory.
    • MLKEM_PRECALC_MIN

      private static final int MLKEM_PRECALC_MIN
      Smallest ML-KEM precomputation pool allowed.
    • MLKEM_PRECALC_MAX

      private static final int MLKEM_PRECALC_MAX
      Largest ML-KEM precomputation pool allowed.
    • XDH_FACTOR

      private static final int XDH_FACTOR
      X25519/EDH precalc — each pair ~64 bytes, scale with cores and memory.
    • XDH_PRECALC_MIN

      private static final int XDH_PRECALC_MIN
      Smallest X25519 precomputation pool allowed.
    • XDH_PRECALC_MAX

      private static final int XDH_PRECALC_MAX
      Largest X25519 precomputation pool allowed.
    • EDH_PRECALC_MIN

      private static final int EDH_PRECALC_MIN
      Smallest EDH precomputation pool allowed.
    • EDH_PRECALC_MAX

      private static final int EDH_PRECALC_MAX
      Largest EDH precomputation pool allowed.
    • _paramFailureLogged

      private final Set<String> _paramFailureLogged
      Params whose update failure has already been logged at least once.
    • _lastParamFailureLog

      private final Map<String,Long> _lastParamFailureLog
      Last failure-log time per param name, for the rate limit.
    • PARAM_FAILURE_LOG_INTERVAL_MS

      private static final long PARAM_FAILURE_LOG_INTERVAL_MS
      Minimum gap between repeated failure logs for the same param, ms.
      See Also:
    • PARAM_UPDATE_FAILED_STAT

      static final String PARAM_UPDATE_FAILED_STAT
      Counts params that threw during a tuning cycle.
      See Also:
    • _lastHealthScore

      private volatile double _lastHealthScore
      Overall health score from the last computation, NaN before the first.
    • _lastSubsystemScores

      private volatile Map<String,double[]> _lastSubsystemScores
      Per-subsystem scores from the last computation.
    • PROP_PREFIX

      static final String PROP_PREFIX
      Property prefix for tuner ranges. Format: tuner.{paramName}.min, tuner.{paramName}.max, tuner.{paramName}.step.
      See Also:
    • _reflectorFailures

      private static final Set<String> _reflectorFailures
      Methods whose reflective call has already been reported, keyed class#name.
    • _reflectorTargets

      private static final Map<String,String> _reflectorTargets
      The class each reflectively-tuned static is declared on, keyed by method name.

      Streaming and i2ptunnel are not on the router's compile classpath, so the tuner reaches their statics by name. Choosing the class at the call site is what broke: three params named a real method on the wrong class, the lookup failed, the failure was swallowed, and the param tuned nothing while its getter handed back a fallback that read like a live value. The class is declared once here instead, and TunerReflectorTargetTest pins every entry against the classes as they are actually compiled.

    • _reflectorClasses

      private static final Map<String,Class<?>> _reflectorClasses
      Classes named by _reflectorTargets, loaded on first use.
    • ESTABLISH_TIMEOUT_MIN

      public static final int ESTABLISH_TIMEOUT_MIN
      Lowest value either establish-timeout param may be tuned to.

      Measured on a live router: at 2250 the outbound path abandoned roughly 117 session establishments per minute, every one expiring on the deadline rather than on an observed peer failure, and 99.6% of them inside the SSU2 token exchange, which costs one round trip more than SSU1. Raising the budget to 5000 took that to zero - but build success fell from 64.5% to 56.0% and throughput 12%, because the patience moves out of the establishment path and into the build slot. 4000 is the compromise: comfortably above the value known to be harmful, below the one where slot occupancy starts costing builds. The original floor of 1500 sat well below the harmful value, so a slider drag or a restore-defaults could silently reintroduce the failure.

      See Also:
    • STREAM_MSG_SIZE_DEFAULT

      private static final int STREAM_MSG_SIZE_DEFAULT
      Matches ConnectionOptions.DEFAULT_MAX_MESSAGE_SIZE (1730), which is what the streaming layer actually applies: i2ptunnel builds socket manager options without maxMessageSize, so connections fall back to this default. Hardcoded here because the router module cannot reference the streaming app constant.
      See Also:
    • STREAM_LOSSY_RATE

      static final double STREAM_LOSSY_RATE
      Streaming retransmission rate at or above which loss is called lossy. Package-visible so the ceiling's dead-band can be derived from it in tests rather than re-declared as a second literal that could drift from it.
      See Also:
    • STREAM_SEVERE_LOSS_RATE

      private static final double STREAM_SEVERE_LOSS_RATE
      Streaming retransmission rate at or above which loss is severe enough to take the doubled shrink step instead of the ordinary one.

      Split from STREAM_LOSSY_RATE so a mildly lossy path still backs off (single step) but is not torn down at full aggression every cycle — before this split, sendDuplicateSize > 1000 put every cycle on the aggressive tier because the threshold was a size, not a rate.

      See Also:
    • CONGESTION_GATE_MS

      private static final long CONGESTION_GATE_MS
      The gateway send-failure lifetime above which the ceiling treats the path as congested.
      See Also:
    • CEILING_CLIMB_FRACTION

      private static final double CEILING_CLIMB_FRACTION
      Fraction of the lossy threshold below which the ceiling may climb again.

      The same threshold would otherwise serve both directions, so a signal sitting on it crosses it repeatedly and the ceiling bangs between the floor and the cap without resting in between - which is what a measured 50-second limit cycle looked like before this gap existed.

      See Also:
    • CEILING_CONFIRM_WINDOWS

      static final int CEILING_CONFIRM_WINDOWS
      Windows the signal must hold one direction before the ceiling moves in it.

      A single reading is one period of a small-sample ratio, which crosses a threshold on its own. Two consecutive periods in the same direction is the cheapest debounce that still reacts within a loss episode rather than after it.

      See Also:
    • CEILING_SHRINK

      static final int CEILING_SHRINK
      The ceiling should shrink.
      See Also:
    • CEILING_CLIMB

      static final int CEILING_CLIMB
      The ceiling should climb.
      See Also:
    • CEILING_HOLD

      static final int CEILING_HOLD
      The ceiling should hold where it is.
      See Also:
    • STRONG_CLEAN_MEM_PCT

      private static final double STRONG_CLEAN_MEM_PCT
      Heap % below which the streaming ceiling takes the full four-step jump.
      See Also:
    • CLIMB_HEADROOM_MEM_PCT

      private static final double CLIMB_HEADROOM_MEM_PCT
      Heap % below which a clean path may take a two-step climb; holds at or above this until memory frees.
      See Also:
    • SHRINK_MEM_PCT

      private static final double SHRINK_MEM_PCT
      Heap % above which the streaming ceiling shrinks half a step.
      See Also:
    • TIMER_GROW_MS

      private static final int TIMER_GROW_MS
      Backlog at or above which the timer gains a worker, in ms.
      See Also:
    • TIMER_SHRINK_MS

      private static final int TIMER_SHRINK_MS
      Backlog at or below which the timer releases a worker, in ms.
      See Also:
    • MIN_SEARCH_SAMPLES

      static final int MIN_SEARCH_SAMPLES
      Completed outgoing NetDb searches that must have finished before a failure ratio is trusted. Below this the ratio is noise: one failed lookup on a quiet router would otherwise re-arm every search budget in the group.
      See Also:
    • SEARCH_FAIL_RATIO

      static final double SEARCH_FAIL_RATIO
      Outgoing-search failure ratio above which no NetDb search budget may be tightened, and above which the budgets grow instead.
      See Also:
    • MAX_CONCURRENT_IDLE_TARGET

      static final int MAX_CONCURRENT_IDLE_TARGET
      Idle target for the NetDB max concurrent searches param.

      Mirrors the untuned default in IterativeSearchJob (16, or 4 on a slow router). A quiet router settles here rather than at the hard min, so the router keeps enough search capacity to resolve a cold destination without ramping up from the floor one slot per cycle.

      See Also:
    • HEALTHY_CYCLES_TO_TIGHTEN

      private static final int HEALTHY_CYCLES_TO_TIGHTEN
      Consecutive cycles with ample peer supply required before the activity window tightens again. A single ample reading must not withdraw relief from pools that are still refilling, so the window holds for this many cycles rather than snapping to the floor immediately.
      See Also:
    • MIN_TEST_SAMPLES

      static final int MIN_TEST_SAMPLES
      Completed tunnel test rounds needed before a failure ratio is trusted.
      See Also:
    • TEST_FAIL_RATIO

      static final double TEST_FAIL_RATIO
      Tunnel-test failure ratio above which the test schedule may not be relaxed.
      See Also:
    • STALL_TIMEOUT_FLOOR_MS

      static final int STALL_TIMEOUT_FLOOR_MS
      Floor and default for the I/O stall window, ms. Sized for a bulk transfer: a large download over a congested path routinely pauses longer than a few seconds between copy steps, and a window shorter than that aborts healthy transfers rather than freeing threads.
      See Also:
    • SERVER_BACKLOG_IDLE_FLOOR

      static final int SERVER_BACKLOG_IDLE_FLOOR
      Idle autotune target for the I2PTunnel server handler queue capacity. A quiet server settles here rather than at the hard min, so a healthily sized burst buffer is always retained: the live value can never collapse to the 16-hard-floor under a merely-quiet (60s-avg queueDepth) monitoring window.
      See Also:
    • STARTUP_GRACE_MS

      private static final long STARTUP_GRACE_MS
      Minimum uptime (ms) before system health scoring begins. During this window, getScore() returns NaN (UI shows "not yet available") and subsystem rings show gray "collecting" per their own min-event checks.
      See Also:
  • Constructor Details

    • Tuner

      public Tuner(RouterContext ctx)
      The tuner with all tunable parameters.
      Parameters:
      ctx - the router context
  • Method Details

    • sampleStageCpu

      private void sampleStageCpu()
      Sample per-thread CPU time per stage, once per fast tune cycle (5s).
    • stageFor

      static int stageFor(String threadName)
      Stage a thread belongs to, by name. First match in CPU_STAGES order wins, and it is the prefix, not the full name, that names the stat and the StageCpu entry.
      Parameters:
      threadName - a live thread's name, never null
      Returns:
      index into CPU_STAGES, or Tuner.ThreadCpuTable.NO_STAGE
    • resolveThreadNames

      private void resolveThreadNames(long[] ids, int count)
      Read the names of threads this cycle has not classified yet.

      Caching a thread id's stage is sound because a running thread is never renamed: every setName call in the router sits inside a thread factory, before the thread starts. An id whose CPU counter ran backwards is a reused id and is reset to Tuner.ThreadCpuTable.UNRESOLVED by sampleStageCpu(), so its name is read again rather than trusted.

      Parameters:
      ids - thread ids sampled this cycle, in any order; ids below 1 are ignored
      count - how many entries at the start of ids are valid, never more than ids.length
    • stageGrowth

      private int stageGrowth(String stage)
      Thread growth step for a CPU-saturated stage: +1 at >= 75% of a core sustained over two cycles, +2 at >= 150%, +3 at >= 300%. Severe saturation (>= 200% total) triggers immediately after a single cycle.
      Parameters:
      stage - a CPU_STAGES prefix
      Returns:
      0 when not saturated, else the number of threads to add
    • cpuHeadroom

      private boolean cpuHeadroom()
      Whether the JVM has CPU headroom to add worker threads. Gated on 60s-smoothed process load: growth is allowed while at least one full core of capacity remains unused, so 1-core machines never grow.
      Returns:
      true if at least one core of capacity is free
    • peerScaledCap

      private int peerScaledCap(Transport t, int min, int max, int divisor)
      Cap pool size by active transport connections: one extra thread above the minimum per divisor connections. Bounds worst-case thread count without creating threads on its own — demand signals drive actual growth.
      Parameters:
      t - the transport, or null if not running (no constraint)
      min - the pool minimum
      max - the pool maximum
      divisor - connections per extra thread
      Returns:
      the effective cap
    • getUDPTransport

      private UDPTransport getUDPTransport()
      UDP transport, or null if not available.
      Returns:
      the UDP transport or null
    • getNTCPTransport

      private NTCPTransport getNTCPTransport()
      NTCP transport, or null if not available.
      Returns:
      the NTCP transport or null
    • tunedOr

      static int tunedOr(int tuned, int fallback)
      The value a param last applied, or the config-or-default reading when nothing has been applied yet.

      Some params push a value into a component that offers no reading back - the CoDel queues hold theirs per instance, with no static getter. Those params' getRuntimeValue() used to read a config property the tuning path never writes, so a successful apply was indistinguishable from a setter that refused to move, and the cycle re-applied and re-warned forever. They record what they applied and read it back through here instead.

      Parameters:
      tuned - the last applied value, or -1 when nothing has been applied
      fallback - the config-or-default reading to use until then
      Returns:
      the applied value if there is one, otherwise fallback
    • setMaxDispatchAgeMs

      static void setMaxDispatchAgeMs(int value)
      Records the tuned dispatch age. The startup tunables, the auto-tuner's apply and the console override all write it here, so the verify and the transport's GetBidsJob read one value.
      Parameters:
      value - the max time in ms a message may sit in the outbound dispatch queue
    • setHandlerThreadPriority

      static void setHandlerThreadPriority(int priority)
      Records the tuned I/O handler thread priority, so the verify and getHandlerThreadPriority() read one value.
      Parameters:
      priority - the thread priority to apply, in Thread scale
    • getHandlerThreadPriority

      public static int getHandlerThreadPriority()
      The handler thread priority.
      Returns:
      the target priority for I/O handler threads
    • adjustHandlerPriority

      public static void adjustHandlerPriority()
      Adjust the current thread's priority to the target handler priority. Call this from handler thread main loops — lightweight volatile read. Critical threads (watchdog, event pumper, job queue) should NOT call this.
    • getMaxDispatchAgeMs

      public static int getMaxDispatchAgeMs()
      The max dispatch age in ms.
      Returns:
      the age, in ms
    • getInternalQueueSize

      public static int getInternalQueueSize()
      The I2CP internal queue size.
      Returns:
      the current I2CP internal queue size
    • getTestClientBudget

      public static int getTestClientBudget()
      The test client budget.
      Returns:
      tunnels tested concurrently per client pool (default 256)
    • getTestRetestBackoff

      public static int getTestRetestBackoff()
      The test retest backoff.
      Returns:
      retest delay multiplier as a percent, 100 = no backoff, up to 800
    • getBuildFailureBuffer

      public static int getBuildFailureBuffer()
      The build failure buffer.
      Returns:
      extra tunnels built beyond target per pool, 0-10
    • setInternalQueueSize

      public static void setInternalQueueSize(int size)
      The I2CP internal queue size, clamped to a sane range (called by Tuner).
      Parameters:
      size - the queue size
    • internalQueueSizeMin

      static int internalQueueSizeMin()
      The smallest queue size setInternalQueueSize(int) accepts.
      Returns:
      half the platform default, in messages
    • internalQueueSizeMax

      static int internalQueueSizeMax()
      The largest queue size setInternalQueueSize(int) accepts.
      Returns:
      four times the platform default, in messages
    • memoryDerivedInitMsgMax

      private static int memoryDerivedInitMsgMax()
      Memory-derived ceiling for per-peer initial concurrent messages: ~1 message per 8MB of max heap, clamped to a sane band. Mirrors the scaling used by Tuner.MaxConcurrentMessagesParam so the initial value can never exceed what the JVM can reasonably hold.
      Returns:
      one initial concurrent message per 8 MB of max heap, clamped to [64, 1024]
    • initConcurrentMsgsMax

      static int initConcurrentMsgsMax()
      The ceiling the tuner may propose for udp.peer.initConcurrentMsgs.

      Capped by what PeerState.setInitConcurrentMsgs(int) itself accepts: a target above that is rewritten back to the current value and then reads forever as a param that will not take effect.

      Returns:
      the memory-derived ceiling, never past the setter's own 256
    • memoryDerivedClientThreadMax

      private static int memoryDerivedClientThreadMax()
      Memory-derived ceiling for the I2PTunnel client worker pool and its admission gate. A thread-per-connection SynchronousQueue pool holds one OS thread (JVM stack + framing) per admitted connection, so the pool (and the gate that floors it) must never be allowed past a thread count the max heap can plausibly host. ~1 thread per 2MB of max heap, floored at 256 so even a constrained router keeps a sane baseline, clamped to the 16384 hard cap. Mirrors the memory scaling pattern of memoryDerivedInitMsgMax().
      Returns:
      the memory-feasible thread ceiling for the client proxy pool
    • mlkemPreCalcMax

      static int mlkemPreCalcMax()
      The ceiling the tuner may propose for crypto.mlkem.precalcMin.

      The factory caps the pool at its byte budget rather than at its hard cap, so a floor above that is rewritten back on the way in and the param looks stuck.

      Returns:
      the declared ceiling, never past MLKEMKeyFactory.getByteCap()
    • scaleForSystem

      static int scaleForSystem(int base, int hardMin, int hardMax)
      Compute a system-scaled value: base * factor, bounded by min and max. Factor is max(memFactor, coreFactor), halved for slow systems.
      Parameters:
      base - unscaled value for the detected hardware
      hardMin - floor the result is clamped to
      hardMax - ceiling the result is clamped to
      Returns:
      the scaled value, constrained to [hardMin, hardMax]
    • getMemoryPressure

      public static double getMemoryPressure()
      Returns current heap memory pressure as a ratio from 0.0 (no pressure) to 1.0 (heap full). Used to scale cache sizes and buffer pools.

      Public because it is the router-wide reading for this signal, and tunables outside this class size themselves against it too.

      Returns:
      heap pressure, 0.0 to 1.0
    • applyStartupTunables

      private static void applyStartupTunables(Tuner.AutotuneConfig autotune, RouterContext ctx)
      Apply the configured values at startup, clamped to the tunable ranges enforced by MaxDispatchAgeParam / HandlerThreadPriorityParam. These fields feed adjustHandlerPriority() and GetBidsJob before the tunable machinery takes over; an out-of-range priority would make setPriority() throw in every handler loop, and a negative dispatch age is nonsense. Kept in a static method (called once from the constructor) so the statics are not written from instance construction.
      Parameters:
      autotune - the shared autotune config used as the BaseParam fallback
      ctx - the router context
    • purgeOldTunerProps

      private void purgeOldTunerProps(RouterContext ctx)
      Remove old tuner.* properties from router.config that were written there before autotune.config was introduced. Only purges keys we know about (tuner.PARAM.value/default/min/max/step), leaves user-configured props alone.
      Parameters:
      ctx - the context whose router.config is scanned and rewritten
    • timeReached

      public void timeReached()
      Expire entries and reschedule the next check.
      Specified by:
      timeReached in class SimpleTimer2.TimedEvent
    • fastCycle

      private void fastCycle(long now)
      Fast 5s cycle: transport worker thread pools react to CPU saturation.
      Parameters:
      now - current time in ms, compared with the last fast cycle
    • slowCycle

      private void slowCycle(long now)
      Slow 15s cycle: system health, param updates, crypto pools and derived values.
      Parameters:
      now - current time in ms, compared with the last slow cycle
    • updateSlowParams

      private void updateSlowParams(Tuner.SystemHealth health)
      Update all non-fast params on the 15s cycle.
      Parameters:
      health - the system health snapshot computed for this cycle and handed to each param
    • noteParamUpdateFailure

      private void noteParamUpdateFailure(Tuner.TunableParam param, Exception e)
      Record that a param's tuning cycle threw.

      A throwing param is invisible: the cycle handler swallows it, the param never updates, and nothing anywhere shows it as dead. That is how i2p.tunnel.build.requestTimeout sat permanently inert — it threw on every cycle for the life of the router and the only evidence was a repeated stack trace in a log file nobody reads.

      Two things make it visible now: a counter on tuner.paramUpdateFailed that appears on /stats, and a per-param rate limit so a permanently broken param logs once with its trace and then stays quiet instead of re-printing every 15 seconds. Both are needed — the counter says a param is dead, the single trace says why.

      Parameters:
      param - the param that failed
      e - the exception the cycle handler swallowed
    • refreshCryptoPoolSizes

      private void refreshCryptoPoolSizes()
      Refresh XDH, EDH and MLKEM precomputation pool sizes.
    • computeTuningValues

      private void computeTuningValues()
      Compute derived tuning values from subsystem scores and job queue stats. Updates static volatile fields read by TestJob and TunnelPool. Called once per tune cycle from timeReached().
    • getBuildSuccessRate

      private double getBuildSuccessRate()
      Build success rate from hourly stats (0.0-1.0).
      Returns:
      success rate or NaN if insufficient data
    • getJobQueuePressure

      private double getJobQueuePressure()
      Job queue pressure score (0.0 = idle, 1.0 = severe). Combines jobLag, readyJobs, and droppedJobs into a single metric.
      Returns:
      the job queue pressure
    • getAvgStat

      private double getAvgStat(String name)
      60s rolling average of the named stat, or NaN if unavailable.
      Parameters:
      name - the statManager rate name to read
      Returns:
      60s rolling average of a stat, or NaN if unavailable
    • getHourlyAvgStat

      private double getHourlyAvgStat(String name)
      Hourly rolling average of the named stat, or NaN if unavailable.
      Parameters:
      name - the rate whose hourly average is wanted
      Returns:
      hourly rolling average of a stat, or NaN if unavailable
    • getEventCountStat

      private double getEventCountStat(String name)
      Event count in the last 60s for the named stat, or NaN if unavailable.
      Parameters:
      name - the rate whose event count is wanted
      Returns:
      event count in the last 60s for a stat, or NaN if unavailable
    • shutdown

      public void shutdown()
      Flush on shutdown — unconditional write if dirty.
    • getSnapshots

      public List<Tuner.ParamSnapshot> getSnapshots()
      Snapshots of all tunable params.
      Returns:
      list of param snapshots
    • getHealthScore

      public double getHealthScore()
      Latest system health score (0.0 = worst, 1.0 = perfect). Returns NaN if health hasn't been computed yet.
      Returns:
      the health score
    • getSubsystemScores

      public List<Tuner.SubsystemScore> getSubsystemScores()
      Per-subsystem health scores computed during the last tuning cycle. Returns a list of SubsystemScore sorted by the SUBSYSTEM_ORDER used on the tuning page.
      Returns:
      list of subsystem scores
    • getAutotune

      public Tuner.AutotuneConfig getAutotune()
      Shared AutotuneConfig instance. Form handlers should use this (not new AutotuneConfig()) so writes go to the same in-memory instance the Tuner reads from.
      Returns:
      the autotune config
    • getParam

      public Tuner.TunableParam getParam(String name)
      Tunable param by name.
      Parameters:
      name - the param name
      Returns:
      the param or null
    • setOverride

      public void setOverride(String name, int value)
      An override value for a param.
      Parameters:
      name - the param name
      value - the override value
    • restoreDefaults

      public void restoreDefaults()
      Restore all params to factory defaults (code-level, not persisted).
    • findStaticMethod

      static Method findStaticMethod(Class<?> c, String name, Class<?>... args) throws NoSuchMethodException
      Find a static method reflectively and make it callable.

      Two properties of the targets break the obvious lookup, and both fail quietly. The member may be package-private, which Class.getMethod(String, Class...) does not see at all, and it may sit on a package-private class, which makes even a public member throw IllegalAccessException at invoke time. Streaming's ConnectionOptions and Connection are non-public classes with public static members, so a getMethod-based call site took the first branch and a bare invoke took the second: either way the param tuned nothing while its getter handed back a fallback that looked like a live reading.

      Superclasses are walked because the members are not all declared on the class named at the call site.

      Parameters:
      c - the class to search
      name - the static method name
      args - the parameter types, empty for a getter
      Returns:
      the method, already made accessible
      Throws:
      NoSuchMethodException - if no class in the hierarchy declares it
    • reportReflectorFailure

      static void reportReflectorFailure(Class<?> c, String name, Throwable t)
      Report a reflective failure once per method.

      A getter returning its sentinel is indistinguishable from "unknown" at the call site, where it becomes a plausible-looking fallback, so a swallowed exception here reads downstream as a healthy runtime reading.

      Parameters:
      c - the class the call was made against
      name - the method that failed
      t - what went wrong
    • reflectorFailureKey

      static String reflectorFailureKey(Class<?> c, String name)
      The identity a reflective failure is deduplicated by.
      Parameters:
      c - the class the call was made against, null when it is not loaded
      name - the static method that failed
      Returns:
      the dedup key for that class and method
    • shouldReportReflectorFailure

      static boolean shouldReportReflectorFailure(Class<?> c, String name)
      Whether a reflective failure has not been reported yet, claiming the one report it is owed.

      Every method the tuner calls through reflection is reported the first time it fails and never again, because the call site cannot tell a sentinel apart from a reading: silence after the first failure is what let a whole set of streaming params tune nothing while their getters reported plausible values.

      Parameters:
      c - the class the call was made against, null when it is not loaded
      name - the static method that failed
      Returns:
      true the first time this class and method pair fails, false afterwards
    • callStaticInt

      static int callStaticInt(Class<?> c, String name, int fallback)
      Invoke a static int getter, or return fallback if it cannot be reached.
      Parameters:
      c - the class to call, null when it is not loaded yet
      name - the static method name
      fallback - the value to return when the call is unavailable
      Returns:
      the value read, or fallback
    • callStaticSetInt

      static void callStaticSetInt(Class<?> c, String name, int value)
      Invoke a static int setter, reporting a call that cannot be made.
      Parameters:
      c - the class to call, null when it is not loaded yet
      name - the static method name
      value - the value to hand over
    • callStaticDouble

      static double callStaticDouble(Class<?> c, String name, double fallback)
      Invoke a static double getter, or return fallback if it cannot be reached.
      Parameters:
      c - the class to call, null when it is not loaded yet
      name - the static method name
      fallback - the value to return when the call is unavailable
      Returns:
      the value read, or fallback
    • callStaticLong

      static long callStaticLong(Class<?> c, String name, long fallback)
      Invoke a static long getter, or return fallback if it cannot be reached.
      Parameters:
      c - the class to call, null when it is not loaded yet
      name - the static method name
      fallback - the value to return when the call is unavailable
      Returns:
      the value read, or fallback
    • callStaticSetLong

      static void callStaticSetLong(Class<?> c, String name, long value)
      Invoke a static long setter, reporting a call that cannot be made.
      Parameters:
      c - the class to call, null when it is not loaded yet
      name - the static method name
      value - the value to hand over
    • bindReflectorTargets

      private static void bindReflectorTargets(String cls, String... names)
      Record the class that declares a group of reflectively-called statics.
      Parameters:
      cls - the declaring class, by name, so no module need be on the classpath
      names - the static method names the tuner calls on it
    • reflectorTargets

      static Map<String,String> reflectorTargets()
      The target table, for the test that pins it against the compiled classes.
      Returns:
      an unmodifiable view of method name to declaring class name
    • reflectorTargetClass

      static Class<?> reflectorTargetClass(String name)
      The class a reflectively-called static is declared on, loaded on demand.

      A name missing from _reflectorTargets is reported the way a failed lookup is, because the call site cannot tell it apart from a module that is simply not installed, and an unbound name tunes nothing.

      Parameters:
      name - the static method name a reflector is about to call
      Returns:
      the declaring class, or null if the name is unbound or its module is not installed
    • establishTimeoutTarget

      static int establishTimeoutTarget(double observed, int min)
      Target establish timeout for an observed mean handshake duration.

      Four times the observed mean, floored by the param's own minimum. The floor must come from min rather than a literal: a hardcoded floor here can sit far below the value the setter enforces, and Tuner.BaseParam.clamp(int, int, int) walks the stored value toward the target by _step without consulting min at all - so the stored value would drift down while the runtime value stayed clamped, leaving the console permanently displaying a number the router was not using.

      Parameters:
      observed - mean successful handshake duration in ms, or NaN if unknown
      min - the param's configured minimum
      Returns:
      the target in ms, never below min
    • streamingLossRate

      static double streamingLossRate(double dupEvents, double sendEvents, double rtxPerMille, double rtxBytesPerMille)
      Compute the recent streaming retransmission rate.

      The 60-second send/duplicate event counts are the freshest signal and are always available while traffic flows: PacketQueue samples both counters once per 16 events, so the ratio of their event counts is the ratio of the populations they sample. Note that PacketQueue only counts a transmission as a duplicate once numSends > 1, i.e. from the third transmission onward, so a packet resent exactly once contributes no duplicate events — the returned rate is therefore a conservative (under-counting) estimate of retransmission pressure.

      The closed-stream per-mille ratios are consulted only as a fallback: they are emitted when a stream is torn down, so they are the better signal once available but say nothing during startup or on a long-lived connection.

      Parameters:
      dupEvents - 60s event count of stream.con.sendDuplicateSize, or NaN
      sendEvents - 60s event count of stream.con.sendMessageSize, or NaN
      rtxPerMille - stream.rtxRatio (resends per 1000 messages), or NaN
      rtxBytesPerMille - stream.rtxRatioBytes (resend bytes per 1000 bytes), or NaN
      Returns:
      the retransmission rate clamped to [0.0, 1.0], or NaN when no signal is available at all
    • streamingLossRate

      static double streamingLossRate(double dupEvents, double sendEvents, double dupMessages, double messagesSent, double rtxPerMille, double rtxBytesPerMille)
      The retransmission rate, preferring the volume-weighted view.

      The primary path counts observed events. The fallback that replaced it matters when no events were observed in the window but connections did close: the per-mille message ratio is computed per connection as 1000 * dupMessagesSent / messagesSent, so a connection that sent three messages and retransmitted a few records thousands per mille. Averaged over closings it saturates the rate at 1.0 - 100% loss - while the byte-weighted ratio for the same window reads around 2%. The aggregate counters divide the same quantities as sums rather than as an average of ratios, so a three-message connection contributes three to the denominator instead of one vote of equal weight, and the result tracks the volume that was actually carried.

      Parameters:
      dupEvents - duplicate-send events observed, or NaN
      sendEvents - send events observed, or NaN
      dupMessages - duplicate messages sent over the window, or NaN
      messagesSent - messages sent over the window, or NaN
      rtxPerMille - per-connection message retransmit ratio in per-mille
      rtxBytesPerMille - byte-weighted retransmit ratio in per-mille
      Returns:
      retransmission rate in [0.0, 1.0], or NaN when there is no signal
    • isStreamLossy

      static boolean isStreamLossy(double lossRate)
      Is the streaming path losing enough that windows should stop growing?
      Parameters:
      lossRate - rate from streamingLossRate(double, double, double, double), possibly NaN
      Returns:
      true when the rate is at or above STREAM_LOSSY_RATE
    • isStreamLossSevere

      static boolean isStreamLossSevere(double lossRate)
      Is the streaming path losing enough to justify the aggressive shrink?
      Parameters:
      lossRate - rate from streamingLossRate(double, double, double, double), possibly NaN
      Returns:
      true when the rate is at or above STREAM_SEVERE_LOSS_RATE
    • exponentialClimb

      static int exponentialClimb(int current, int step)
      Clean-path climb for a window-sized parameter: max(step, current / 4).

      Near the bottom of the range this degenerates to the plain linear step so a parameter parked at min cannot take a disproportionate jump, but once it has room the growth is geometric. A healthy window reaches its cap in a handful of cycles instead of one step per cycle for as many cycles as the range spans.

      Parameters:
      current - current runtime value
      step - tuning step size
      Returns:
      the number of units to add this cycle
    • computeStreamingBdpTarget

      static int computeStreamingBdpTarget(double bwSendBps, double rttMs, int messageSize, int min, int max)
      Compute the streaming congestion-window target from the bandwidth-delay product.

      How big the pipe can be depends on the real streaming message size: the congestion window counts messages, and each message carries up to messageSize payload bytes (default 1730, two tunnel messages). Using the historical 4096 there under-sized the target by ~2.4x and capped sustained throughput below what the network could carry.

      Returned value is clamped into [min, max]. Returns -1 when there is no usable signal (no bandwidth, or an RTT too short to measure), so the caller can fall back to holding its current value.

      Parameters:
      bwSendBps - measured average send bandwidth in bytes per second
      rttMs - observed streaming RTT in milliseconds
      messageSize - streaming payload bytes per window slot, already clamped to a sane range
      min - lower bound for the window (inclusive)
      max - upper bound for the window (inclusive)
      Returns:
      the BDP window target in [min, max], or -1 if there is no usable signal; never returns a value outside [min, max]
    • computeStreamingMaxWindowTarget

      static int computeStreamingMaxWindowTarget(int current, int min, int max, int step, int defaultValue, double failLifetime, double lossRate, double memPct)
      Compute the streaming max-window ceiling target for one tuning cycle.

      The ceiling bounds in-flight messages per connection. It is an upper bound, not a reservation — in-flight data still tracks the ACK-driven congestion window — so a high ceiling never loads a slow path; it only removes a throttle from a fast one.

      The former logic additionally shrunk the ceiling whenever the bandwidth-delay product fell below current, but that bandwidth is measured through the current ceiling: on a capped connection a low average is an artifact of the cap, not evidence the cap should drop, and ratcheting the cap down on that artifact produced a down-only spiral (a sub-ceiling bandwidth read-out justifies shrinking the cap, which lowers the read-out further, ...).

      The ceiling therefore shrinks ONLY on hard negative signals (duplicate retransmits, gateway congestion, memory pressure) and climbs on a clean path, bounded upward and gated on heap headroom so a large jump can never be the event that pushes the heap into pressure.

      The clean-path climb is hyper-responsive: with strong headroom (heap below 40%) it takes four steps per cycle, with moderate headroom (below 50%) two, and at 50% or above it holds until memory frees rather than spending headroom it may need. Combined with the 5s fast cycle, the 8192-message ceiling is reachable from the factory default in four strong cycles (~20s) instead of ~4 slow minutes; every climb is still bounded by max and scaled down by health dampening in BaseParam.update().

      Below the recovery floor (max(min, defaultValue / 2)) and otherwise healthy, the ceiling climbs two steps toward the factory default; under hard signals it shrinks half a step but never below the recovery floor, so a depression cannot be torn back down to the floor trim. Shrinking is deliberately the fastest way to lose headroom: loss, congestion, or heap above 60% all pull the ceiling back within one (fast) cycle.

      Parameters:
      current - current ceiling value (Connection#getGlobalMaxWindowSize)
      min - lower bound for the ceiling (inclusive)
      max - upper bound for the ceiling (inclusive)
      step - one tuning step; clean-path climbs use four steps under strong heap headroom and two otherwise, hard-signal shrinks use half a step
      defaultValue - factory default ceiling; the recovery floor is max(min, defaultValue / 2)
      failLifetime - stat transport.sendMessageFailureLifetime in ms, or NaN if absent
      lossRate - streaming retransmission rate (0.0-1.0), or NaN if absent; see streamingLossRate(double, double, double, double)
      memPct - stat jobQueue.memoryUsedPercent, or NaN if absent
      Returns:
      the new ceiling target within [min, max]
    • maxWindowDecision

      static String maxWindowDecision(double failLifetime, double lossRate, double memPct, long dupEvents, long sendEvents, double avgDupSize, double eventRatio)
      Say why the streaming ceiling took the value it did. Pure, so the wording and the branch it names can be pinned by tests.

      The ceiling's direction is decided by three gated signals and the branch that shrinks is reached only when one of them is true. Reproducing those gates here rather than instrumenting the branch itself keeps the diagnostic next to the policy it explains, and because it is emitted only when the value actually moves, a run of it reads as a trace of the decisions the router made rather than as a per-cycle dump.

      This exists because the two disagreed on a live router: the ceiling spent three quarters of a run stepping down by half a step while every one of the three signals measured below its threshold, and no amount of reading the stats page could say which gate was open. Naming the values alongside the verdict is what makes that visible in the log.

      Parameters:
      failLifetime - stat transport.sendMessageFailureLifetime in ms, or NaN
      lossRate - the driver signal — the true byte-weighted rate, 0.0-1.0, or NaN
      memPct - stat jobQueue.memoryUsedPercent in %, or NaN
      dupEvents - resend event count behind the event-count ratio, 0 when unread
      sendEvents - send event count behind the event-count ratio, 0 when unread
      avgDupSize - mean bytes per retransmitted message, or NaN when unread
      eventRatio - event-count ratio, 0.0-1.0, or NaN — observation only, never the driver
      Returns:
      the verdict on the first line, then the values behind it on \n* evidence lines, never null
      See Also:
    • maxWindowDecisionFrom

      static String maxWindowDecisionFrom(double failLifetime, double driverRate, double eventRate, double memPct, long dupEvents, long sendEvents, double avgDupSize)
      Assemble the ceiling's decision reason from its signals, with the two loss rates named so the slot assignment cannot be transposed.

      This seam exists because maxWindowDecision(double, double, double, long, long, double, double) takes seven positional parameters and the two loss rates are the pair most easily swapped. A previous commit passed the event-count ratio where the true byte-weighted rate belonged, so the line reported a verdict the ceiling had not taken — and no test caught it, because every test called maxWindowDecision directly with the driver already in the driver slot. The function was right and the wiring was wrong. Naming the two rates here states the mapping in one place and makes it directly testable.

      Parameters:
      failLifetime - stat transport.sendMessageFailureLifetime in ms, or NaN
      driverRate - the true byte-weighted rate that decides — 0.0-1.0, or NaN
      eventRate - the event-count ratio, reported but not deciding — 0.0-1.0, or NaN
      memPct - stat jobQueue.memoryUsedPercent in %, or NaN
      dupEvents - resend event count behind eventRate, 0 when unread
      sendEvents - send event count behind eventRate, 0 when unread
      avgDupSize - mean bytes per retransmitted message, or NaN when unread
      Returns:
      a short clause naming the verdict and the values behind it, never null
    • ceilingDirection

      static int ceilingDirection(double lossRate, int riseShrink, int riseClimb)
      Which way the ceiling wants to move, with a dead-band between the two directions. Pure, so the band and the confirm count can be pinned without a router.

      The dead-band is the point. One threshold serving both directions means the signal only has to sit on it, and a small-sample ratio sits on it by construction. Shrinking at the full lossy threshold and climbing at half of it leaves a band where the honest answer is "unchanged", which is what lets the ceiling rest at an intermediate value.

      Boundaries: shrink is inclusive at STREAM_LOSSY_RATE and above; climb is inclusive at STREAM_LOSSY_RATE * CEILING_CLIMB_FRACTION and below; the open interval between them is the dead-band. A direction is returned only after CEILING_CONFIRM_WINDOWS consecutive windows in that zone — a single reading is one period of a small-sample ratio.

      Parameters:
      lossRate - event-count retransmission rate 0.0-1.0, or NaN
      riseShrink - consecutive windows already held in the shrink direction
      riseClimb - consecutive windows already held in the climb direction
      Returns:
      the direction to move, or CEILING_HOLD
    • wholePct

      private static String wholePct(double percent)
      A value already expressed in percent, as a whole-percent string.
      Parameters:
      percent - the value on a 0-100 scale
      Returns:
      the rounded percent with a % sign, or "no signal" for NaN
    • pct

      private static String pct(double rate)
      A fraction as a whole-percent string; NaN becomes "no signal".
      Parameters:
      rate - the fraction on a 0-1 scale
      Returns:
      the rate as a whole percent with a % sign
    • secs

      private static String secs(double ms)
      A millisecond duration as a seconds string with one decimal.
      Parameters:
      ms - the duration in milliseconds
      Returns:
      the duration in seconds with one decimal, or "no signal" for NaN
    • congestionAvoidanceGrowthTarget

      static int congestionAvoidanceGrowthTarget(int current, int min, int max, int step, int defaultValue, double buildSuccess, double failLifetime, double lossRate, double lifetimeRTT, double lifetimeWindowSize, double chokeSize, double congWindowSize, double rtxRatio, double rtxRatio5)
      Compute the target for CongestionAvoidanceGrowthParam.

      Extracted as a static method for unit testing, matching maxSlowStartWindow(int, int, int, int, int, double, double, double).

      Parameters:
      current - current runtime value
      min - minimum allowed value
      max - maximum allowed value
      step - tuning step size
      defaultValue - factory default value
      buildSuccess - tunnel build success rate (0.0-1.0), or NaN if unknown
      failLifetime - sendMessageFailureLifetime stat (ms), or NaN
      lossRate - streaming retransmission rate (0.0-1.0), or NaN
      lifetimeRTT - completed stream RTT (ms), or NaN
      lifetimeWindowSize - final send window of closed streams, or NaN
      chokeSize - choke-depth stat, or NaN
      congWindowSize - window size at congestion, or NaN
      rtxRatio - hourly retransmit ratio (per-mille), or NaN
      rtxRatio5 - 5-minute retransmit ratio (per-mille), or NaN
      Returns:
      target value clamped to [min, max]
    • windowsCongesting

      private static boolean windowsCongesting(double congWindowSize)
      Whether the congestion window at recent congestion events was too small to count as a real congestion signal — the shared "window collapsed" predicate for both streaming growth-factor targets, so the two cannot drift apart.
      Parameters:
      congWindowSize - window size at congestion stat, or NaN
      Returns:
      true if the window at congestion was below 5 messages
    • slowStartGrowthTarget

      static int slowStartGrowthTarget(int current, int min, int max, int step, int defaultValue, double observed, double buildSuccess, double failLifetime, double lossRate, double lifetimeWindowSize, double chokeSize, double congWindowSize, double rtxRatio, double rtxRatio5)
      Compute the target for SlowStartGrowthParam.

      Extracted as a static method for unit testing, matching congestionAvoidanceGrowthTarget(int, int, int, int, int, double, double, double, double, double, double, double, double, double).

      Parameters:
      current - current runtime value
      min - minimum allowed value
      max - maximum allowed value
      step - tuning step size
      defaultValue - factory default value
      observed - outbound RTT in ms (may be NaN)
      buildSuccess - tunnel build success rate (0.0-1.0), or NaN if unknown
      failLifetime - sendMessageFailureLifetime stat (ms), or NaN
      lossRate - streaming retransmission rate (0.0-1.0), or NaN
      lifetimeWindowSize - final send window of closed streams, or NaN
      chokeSize - choke-depth stat, or NaN
      congWindowSize - window size at congestion, or NaN
      rtxRatio - hourly retransmit ratio (per-mille), or NaN
      rtxRatio5 - 5-minute retransmit ratio (per-mille), or NaN
      Returns:
      target value clamped to [min, max]
    • streamCeilingTarget

      static int streamCeilingTarget(double active, int current, int min, int max, int step, boolean refusing, boolean distress)
      Pure decision logic for the stream ceiling.

      Demand-following elasticity: raise the ceiling as legitimate demand or cap-refusals appear so locally hosted services and the HTTP proxy keep flowing; only genuine router starvation (distress) pulls it down, and one step at a time so bursts survive. Malicious floods are handled by the refusal-layer SYN gate and 24h auto-ban, not by crippling this cap.

      Parameters:
      active - active-stream demand on the last arrival (NaN when idle)
      current - the current ceiling (never 0 here; caller maps unarmed to max)
      min - the config floor for the ceiling
      max - the config ceiling (top of working range)
      step - the Tuner step size
      refusing - true when the "dropped for conn limit" guards fired recently
      distress - true only under super-high job-lag WITH congestion/latency
      Returns:
      the target ceiling for the streaming override
    • searchFailureRatio

      static double searchFailureRatio(double successes, double failures)
      Failure ratio of this router's own outgoing NetDb searches.

      Use the per-search outcome stats: netDb.successTime and netDb.failedTime each fire once per completed iterative search in IterativeSearchJob, so their event counts are our successes and our failures.

      Do not use netDb.lookupsMatched or netDb.lookupsMatchedLeaseSet for this — those are written by HandleDatabaseLookupMessageJob when this router ANSWERS another router's lookup. They measure floodfill service, not our own searches, and sit near zero on a non-floodfill, which reads as "every lookup failed" and parks the whole budget group at its floor.

      Parameters:
      successes - event count of netDb.successTime, or NaN
      failures - event count of netDb.failedTime, or NaN
      Returns:
      the failure ratio in [0, 1], or NaN when too few searches completed
    • outcomeRatio

      static double outcomeRatio(double successes, double failures, int minSamples)
      Failure ratio from per-attempt outcome event counts.

      Both arguments are event counts of stats that fire once per completed attempt, one on the success path and one on the failure path. A NaN side is read as zero events, so a stat with no traffic in the window does not skew the ratio; it just lowers the sample count.

      Parameters:
      successes - event count of the success-path stat, or NaN
      failures - event count of the failure-path stat, or NaN
      minSamples - completed attempts needed before the ratio is trusted
      Returns:
      the failure ratio in [0, 1], or NaN when too few attempts completed
    • perPeerSearchTimeMs

      static double perPeerSearchTimeMs(double successTime, double successRetries)
      Per-peer reply budget implied by a completed successful search.

      netDb.successTime is the whole multi-round search and netDb.successRetries is (peers queried - 1), so the quotient is the mean cost of one round; doubling it leaves headroom for a slow round without letting the per-peer window run out to the total search cap.

      Parameters:
      successTime - mean successful search duration in ms, or NaN
      successRetries - mean additional queries per successful search, or NaN
      Returns:
      the per-peer budget in ms, or NaN when there is no signal
    • nextWindowMultiplier

      static int nextWindowMultiplier(int fastPeers, boolean startupGrace, int healthyCycles, int min, int max, int step)
      Next window multiplier for a tuning cycle, keyed on peer supply.

      Pure so the policy is testable without a router. The multiplier is the ceiling TunnelPeerSelector.windowMultiplierFor(int, boolean, int, int) allows for the current fast-peer count, and widening applies in one step because scarcity is not noisy the way a single build-success sample was. Tightening waits for HEALTHY_CYCLES_TO_TIGHTEN consecutive cycles with ample supply, so a momentary dip cannot withdraw relief from a pool that is still refilling.

      Parameters:
      fastPeers - peers currently classified fast
      startupGrace - true during the startup grace period
      healthyCycles - consecutive cycles observed with ample peer supply
      min - floor, e.g. 1
      max - ceiling, e.g. 4
      step - retained for call-site symmetry; the gate is continuous
      Returns:
      the multiplier to apply
    • lookupLimitTarget

      static int lookupLimitTarget(int current, int min, int max, int step, boolean lookupsSlow, boolean queueBackedUp, boolean dropsHappening, boolean lookupsFast)
      Pure starvation-first decision for next-hop lookup concurrency (see PercentLookupLimitParam):
      • queue full or overflowing while lookups succeed in time → demand exceeds the lid; raise aggressively so queued entries convert instead of expiring as silent build drops
      • lookups genuinely slow with nothing queued → netdb overload; less vetting is cheaper
      • fast and quiet → more vetting is cheap
      • anything else (mixed signals) → hold

      Static and package visible for tests.

      Parameters:
      current - current percent value
      min - minimum allowed value
      max - maximum allowed value
      step - the adjustment increment, doubled on a backed-up queue and subtracted in full on slow lookups
      lookupsSlow - true when successful lookups average > 5000ms
      queueBackedUp - true when the pending-lookup queue averages high
      dropsHappening - true when queue-full drops occurred recently
      lookupsFast - true when successful lookups average < 2000ms
      Returns:
      the target percent value, clamped to [min, max]
    • sendWindowTarget

      static int sendWindowTarget(int current, int min, int max, int step, int idleFloor, double observed, double jobLag, double memPressure, double retransmits, double congCWIN, double sendWindow, double rtxRatio, double failsafeCloses, double mtuDecrease)
      Pure CWIN growth/shrink decision for the max send window, extracted from MaxSendWindowParam#computeTarget so the branches are unit-testable without a Tuner context.

      Priority: memory-critical shrink, death-spiral growth, failsafe/MTU shrink, congestion-growth, high-usage growth, stale-ratchet recovery, clean-path pre-ramp, low-usage shrink. The shrink branch never drives the window below the anti-ratchet idle floor (the stable default) — the 64KB floor that previously froze CWIN is only reachable via memory pressure, and growth always wins under congestion. The stale-ratchet recovery climbs any value parked below the idle floor back up (a leftover low ratchet cannot permanently cap the CWIN), and the clean-path pre-ramp climbs a healthy, non-idle window toward the ceiling so a fast pipe is never capped by the transport window.

      Parameters:
      current - the current runtime CWIN in bytes
      min - hard lower clamp in bytes
      max - hard upper clamp in bytes
      step - per-cycle adjustment in bytes
      idleFloor - anti-ratchet floor for idle shrink (max of default and min)
      observed - udp.avgSendWindow usage (NaN = no data)
      jobLag - jobQueue.jobLag (NaN = unknown)
      memPressure - heap pressure ratio 0.0-1.0
      retransmits - udp.retransmitEvents recent count
      congCWIN - udp.congestionOccurred recent count
      sendWindow - udp.avgSendWindow for collapse detection
      rtxRatio - stream.rtxRatio hourly ratio
      failsafeCloses - ntcp.failsafeCloses recent count
      mtuDecrease - udp.mtuDecrease recent count
      Returns:
      the new CWIN target, already clamped to [min, max]
    • maxSlowStartWindow

      static int maxSlowStartWindow(int current, int min, int max, int step, int factoryDefault, double observed, double failLifetime, double lossRate)
      Compute the target value for MaxSlowStartWindowParam.

      This is extracted to a static method for unit testing, matching the pattern of sendWindowTarget(int, int, int, int, int, double, double, double, double, double, double, double, double, double).

      Clean-path increases are exponential: a cycle adds max(step, current / 4), so a healthy cap climbs from the recovery floor to max in ~8 cycles (~2 minutes on the 15s slow cycle) instead of one linear step per cycle (48 cycles, 12 minutes), unlocking slow start's exponential ramp on high-BDP paths much sooner. Near min the term degenerates to the linear step, and shrink branches are untouched: loss still pulls the cap back fast. Health dampening in BaseParam.update() still applies on top.

      Parameters:
      current - current runtime value of the parameter
      min - minimum allowed value
      max - maximum allowed value
      step - tuning step size
      factoryDefault - factory default value
      observed - outbound RTT in ms (may be NaN)
      failLifetime - sendMessageFailureLifetime stat (may be NaN)
      lossRate - streaming retransmission rate (0.0-1.0), or NaN if absent; see streamingLossRate(double, double, double, double)
      Returns:
      target value clamped to [min, max]
    • computeServerHandlerThreads

      static int computeServerHandlerThreads(int current, int min, int max, double queueDepth, double active, double blockingTime, double jobLag, double synExpire)
      Pure decision logic for the I2PTunnel server handler pool size. Extracted (package-visible, static) so unit tests can exercise the policy without a live RouterContext.

      Signals:

      • queueDepth — handler tasks waiting for a free pool thread.
      • active — number of handler threads currently busy (i2ptunnel.serverHandler.active). When this pins the pool ceiling while the queue backs up, handlers are starving connections. blockingHandleTime stays low in that case because it only measures the fast local socket connect to the app, not the slow inbound I2P write to the peer, so saturation is the reliable signal.
      • blockingTime — local handler connect time (not the I2P write).
      • jobLag — jobQueue.jobLag as a CPU-pressure veto: never grow a pool on a swap/busy box.
      • synExpire — router-wide SYN accept-queue expire rate percent (stream.con.synExpireRate). A high rate is direct latency evidence: SYNs are expiring in the accept queue because the inbound I2P write is slow, NOT because the handler pool is under-provisioned. Handlers stall on the same slow transport, so adding threads cannot drain faster; aggressive growth here would just pile up more blocked sockets. A high expire rate therefore moderates growth rather than vetoing it.
      Parameters:
      current - current pool size
      min - the floor the shrink branch stops at
      max - the ceiling every growth branch clamps to
      queueDepth - 60s avg of i2ptunnel.serverHandler.queueDepth, or NaN
      active - 60s avg of i2ptunnel.serverHandler.active, or NaN
      blockingTime - 60s avg of i2ptunnel.serverHandler.blockingHandleTime, or NaN
      jobLag - 60s avg of jobQueue.jobLag, or NaN
      synExpire - 60s avg of stream.con.synExpireRate (percent), or NaN
      Returns:
      the new pool size
    • computeReceiveWorkers

      static int computeReceiveWorkers(int current, int min, int max, double backlogged, double queueDepth, double rtxRatio, double jobLag)
      Pure decision logic for the Tuner-managed inbound receive-worker default (i2p.streaming.receiveWorkerThreads). Extracted (package-visible, static) so unit tests can exercise the policy without a live RouterContext.

      Signals:

      • backlogged — 60s avg of stream.receiveBacklogged (producer-blind events on a full receive shard queue), NaN when none.
      • queueDepth — 60s avg of stream.receiveQueueDepth.
      • rtxRatio — resends per 1000 sends (loss signal), NaN when unknown. Threshold 50 = 5% resends, matching InitialWindowSizeParam.
      • jobLag — 60s avg of jobQueue.jobLag (CPU pressure), NaN when unknown.

      Policy (throughput / latency / reliability):

      • Grow by one shard when producers were blind on a full queue this period and the CPU is not pegged — the dispatcher is the bottleneck, so parallelize the inflow (throughput).
      • Never shrink while the path is lossy (rtxRatio > 50): the retransmission burst still needs its dispatch capacity, and shrinking under loss risks re-backlogging a just-relieved queue (reliability).
      • Slowly reclaim a shard when the queue holds near-empty (queueDepth <= 0.25) with no growth veto active — a leaner pool keeps the same latency (latency). The probe requires real (non-NaN) observations, so a genuinely idle manager is left alone rather than churned.
      Parameters:
      current - current live worker count
      min - lower bound (2)
      max - upper bound (8)
      backlogged - 60s avg of stream.receiveBacklogged, or NaN
      queueDepth - 60s avg of stream.receiveQueueDepth, or NaN
      rtxRatio - resends per 1000 sends, or NaN
      jobLag - 60s avg of jobQueue.jobLag (CPU pressure), or NaN
      Returns:
      the new worker count, clamped to [min, max]
    • computeClientMaxConnections

      static int computeClientMaxConnections(int current, int min, int max, double shedRate, double activeThreads, int maxRunner)
      Pure decision logic for the Tuner-managed default client concurrent-connection cap (i2ptunnel.maxConnections default). Extracted (package-visible, static) so unit tests can exercise the policy without a live RouterContext.

      Signals:

      • shedRate — events/minute of i2ptunnel.clientConnectionShed. A non-zero rate means the accept loop is closing excess inbound connections (zero bytes) because the cap is too low; this is the primary "raise" signal and the direct observable of the empty-response symptom.
      • activeThreads — i2ptunnel.clientRunner.activeThreads (60s avg of true executor load). When it pins near maxRunner the executor is saturated, so raising the connection cap would only pile onto a busy pool.
      • current/min/max — the current default (per-tunnel overrides are untouched), floor, and ceiling.

      Policy: any shed (admission-gate overflow or executor-full rejection) is the unambiguous overload signal — grow the gate decisively and fast so excess connections are admitted and served instead of dropped as empty responses. The worker pool is floored at this gate in TunnelControllerGroup.setClientRunnerMax, so a decisive gate rise also pulls the executor ceiling up (no more 256-vs-27 wedge). Near-saturated executor grows moderately; healthy load grows gently; idle decays slowly so a quiet spell does not throw away burst headroom. Rise is deliberately much faster than decay to fully absorb bursts.

      Parameters:
      current - current default cap
      min - floor for the default cap
      max - ceiling for the default cap
      shedRate - 60s avg of i2ptunnel.clientConnectionShed, or NaN
      activeThreads - 60s avg of i2ptunnel.clientRunner.activeThreads, or NaN
      maxRunner - the client executor max pool size (saturation reference point)
      Returns:
      the new default cap
    • computeIOTransferTarget

      static int computeIOTransferTarget(int current, double observed, int min, int max)
      Pure growth policy for the global Server→Client I/O budget. Extracted (package-visible, static) so unit tests can exercise the policy without a live RouterContext.

      Observed is the busiest pool's utilization — active plus queued transfers divided by that pool's present maximum — not a global sum. Ten pools sharing one budget means the summed pressure divided by the budget reads low even when one pool is saturated; the per-pool ratio makes a saturated pool visible regardless of how quiet the others are. Above 90% the step doubles the current budget so a download burst cannot wait out a +2-per-cycle crawl; between 70–90% growth is still aggressive. Below 20% the budget shrinks by one so a quiet router does not hold threads it does not need.

      Parameters:
      current - current runtime budget (clamped to [min,max] first)
      observed - the busiest pool's utilization rate; NaN or negative leaves the value unchanged
      min - inclusive lower bound
      max - inclusive upper bound
      Returns:
      target budget in [min, max]
    • nextStallTimeoutMs

      static int nextStallTimeoutMs(int current, double observed, int minMs, int maxMs)
      Next I/O stall window for an observed per-cycle stall delta.

      Pure, so the policy is testable without a router.

      The window only loosens above the floor when transfers are being cut off, and relaxes back to the floor when they are not. It never goes below the floor, and there is deliberately no "no stalls, tighten" branch: a shorter window kills more legitimate transfers, which produces more stall events, which loosens the window, which then tightens again on the next quiet cycle. That limit cycle parked the window at its 5s floor, shorter than any pause in a bulk transfer, and aborted 45MB downloads partway through.

      Parameters:
      current - the window in effect, ms
      observed - stall events in this cycle
      minMs - the floor in ms; a negative value is raised to 0
      maxMs - the ceiling in ms; the returned window never exceeds it
      Returns:
      the next window, ms
    • computeServerBacklogQueueCapacity

      static int computeServerBacklogQueueCapacity(int current, int min, int max, double queueDepth, double jobLag)
      Pure decision logic for the I2PTunnel server handler queue capacity. Extracted (package-visible, static) so unit tests can exercise the policy without a live RouterContext.

      The queue is the admission buffer between the accept loop and the handler pool. queueDepth (60s avg) is how deeply it is actually used. Growing it widens the burst window; shrinking it bounds memory (each queued task holds an I2PSocket).

      Signals:

      • queueDepth — handler tasks waiting for a free pool thread.
      • jobLag — jobQueue.jobLag as a CPU-pressure veto: never grow the buffer on a swap/busy box.

      Idle handling converges the buffer to SERVER_BACKLOG_IDLE_FLOOR rather than to min: a too-large buffer shrinks, a too-small one grows, so a quiet server retains a burst buffer and can never be parked at the hard 16 floor. min remains only the external setter clamp.

      Parameters:
      current - current queue capacity
      min - the setter's lower clamp, unused here; the idle floor sets the low end instead
      max - the ceiling every growth branch clamps to
      queueDepth - 60s avg of i2ptunnel.serverHandler.queueDepth, or NaN
      jobLag - 60s avg of jobQueue.jobLag, or NaN
      Returns:
      the new queue capacity
    • clientRunnerMaxMin

      static int clientRunnerMaxMin()
      The floor the tuner may propose for i2ptunnel.clientRunner.max.

      TunnelControllerGroup.setClientRunnerMax floors the pool at the tunnel's admission gate, clientDefaultMaxConnections, whose own floor is I2PTunnelClientBase.DEFAULT_MAX_CONNECTIONS. A target below the gate is rewritten back on the way in, so the tuner must never offer one. The floor is hardcoded here because the router module cannot reference the i2ptunnel constant; TunerCalleeBoundsTest pins it against the live value.

      Returns:
      the lowest client runner pool size the setter will accept
    • isHealthyBuildRate

      static boolean isHealthyBuildRate(double observed)
      Whether a build success rate counts as healthy for a tuning walk.

      Four params gate their walk on observed > 80: the first-hop cooldown, first-hop threshold, stale-build threshold and concurrency throttle. This router runs 70-77% success, so all four saw "not healthy" permanently while their paired timeoutsHigh condition was also permanently true, and each walked to one of its bounds and stayed there -- the cooldown pinned at its 600s ceiling while the concurrency throttle sat at its 50% maximum.

      80% is a healthy-I2P target, not this network's operating point, so using it as a walk gate means the gate never opens. 50% still separates the broken case (single digits) from the working one without demanding a target this network cannot reach.

      Parameters:
      observed - build success rate as a fraction, or NaN when unavailable
      Returns:
      true when the rate is at or above the walk's healthy bound