Skip to content

Commit b95b9cb

Browse files
committed
Adaptive SQM: GPON/XGS-PON profiles, congestion tuning, non-linear latency response (#528)
* Split FTTH into separate GPON and XGS-PON Adaptive SQM profiles Rename Fiber (enum value 2) to Gpon for backwards compatibility with existing DB values. Add XgsPon (enum value 6) at end of enum. GPON schedule: 1.00 overnight, 0.99 morning taper, 0.975-0.97 workday, 0.965 streaming prime time, smooth taper back up through evening. XGS-PON schedule: compressed version - 1.00 overnight, 0.995 morning, 0.9875-0.985 workday, 0.98 streaming. 10G headroom means minimal dip. Both profiles uniform across all 7 days. Auto-detection maps "GPON", "Fiber", "FTTH", "FTTP" to GPON and "XGS-PON"/"XGSPON" to XGS-PON. * Expand XGS-PON auto-detection for multi-gig fiber names Detect 10G, 5 Gig, 3 Gig, 2 Gig, XG-PON, and variants as XGS-PON since any multi-gig fiber service implies XGS-PON infrastructure. * Make ping script safety cap baseline-proportional The flat safety cap (ABSOLUTE_MAX * 0.95) was clamping all hours to the same ceiling, making the GPON/XGS-PON congestion schedule invisible. Now scales with the baseline: cap = ABSOLUTE_MAX * SAFETY_CAP * (baseline / nominal). At night (1.00): 1003 * 0.95 * 1.0 = 952. Streaming (0.965): 1003 * 0.95 * 0.9648 = 919. Adds NOMINAL_SPEED to the ping script for the ratio calculation. * Gate baseline-proportional safety cap to fiber types only Variable connections (DOCSIS, Starlink, cellular) have wide baseline swings (0.75-0.87) and the flat safety cap was never binding - making it proportional would incorrectly clamp rates below actual capacity. Only GPON and XGS-PON use the baseline ratio since their tight range (0.965-1.00) means the flat cap was always the binding constraint. * Remove unreachable '5G Fiber' auto-detection match The cellular check (name.Contains("5G")) comes earlier in the if/else chain, so '5G Fiber' would always match cellular first. * Add user-tunable congestion severity slider Congestion severity (0.90-1.10, default 1.0) scales the magnitude of baseline schedule dips while keeping 1.0 hours unchanged: effective = 1.0 - (1.0 - schedule_multiplier) * severity UI shows a Congestion Schedule section with the effective speed range and a severity slider. Range updates live as the slider moves. Includes DB migration with default 1.0 for existing configs. * Rename Speed Range to Latency Adjust Range for clarity Distinguishes the latency-based floor/ceiling from the congestion schedule range shown below it. * Style severity slider to match app sliders, expand range to +/- 25% Use wan-time-slider class for consistent blue gradient track and white thumb. Range now 0.75-1.25 (was 0.90-1.10). Slider spans full width of the param grid. * Add user-tunable latency threshold, style severity slider Latency threshold is now an editable input (like baseline latency), persisted to DB, and restored on load. Only resets when connection type changes, not when nominal speed changes. Severity slider uses wan-time-slider class for consistent styling, range expanded to 0.75-1.25 (+/- 25%). * Fix migration split, center severity slider in param card The LatencyThresholdMs column was added to an already-applied migration, so it never ran on NAS. Split into separate migration (20260402110000). Severity slider now fits within its param card (100px width, centered) instead of spanning the full grid width. * Consolidate migrations, fix DOCSIS congestion range display Single migration for both CongestionSeverity and LatencyThresholdMs. Fix congestion range for non-fiber types to show baseline * overhead (capped at flat safety cap) instead of just the flat cap value. * Apply safety cap before latency adjustment, not after Moving the cap before latency logic eliminates the dead zone where mild latency spikes were detected but produced no visible rate change. Before: cap after latency = latency decrease from 958 to 920, then capped back to 913. No visible effect. After: cap MAX_DOWNLOAD_SPEED to 913 first, then latency decreases from 913 to 876. Every deviation produces a visible change. Post-latency ceiling still prevents the increase branch from exceeding the schedule-derived cap. * Change ping adjustment interval from 5 min to 1 min * Reduce ping count to 10 at 0.5s interval for better spread * Fix hardcoded 6 AM/6 PM in speedtest trigger description * Add log rotation on boot: keep last 2000 lines * Only persist latency threshold when user overrides it Track the auto-calculated default and only save to DB when the user's value differs. Null in DB = use profile default, so future profile changes to the default threshold will be picked up automatically. * Smooth GPON hour 22 from 0.97 to 0.975 for even taper * Skip tc update when ping rate unchanged * Non-linear latency response (n^0.7) and lower fiber thresholds Latency decrease now uses n^0.7 exponent instead of linear n, so mild spikes (1-2 deviations) get gentle response while sustained congestion (4+ deviations) still triggers aggressive shaping. GPON and XGS-PON default latency threshold lowered from 2.0/1.5 to 1.0ms - fiber latency is clean enough to detect mild congestion. * Offset non-linear curve: (n+1)^0.7 - 1 for gentler first step
1 parent 73c8b9c commit b95b9cb

10 files changed

Lines changed: 2279 additions & 54 deletions

File tree

src/NetworkOptimizer.Sqm/Models/ConnectionProfile.cs

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ public enum ConnectionType
1111
/// <summary>Starlink Satellite - Variable speeds, weather-sensitive, higher latency</summary>
1212
Starlink,
1313

14-
/// <summary>Fiber (FTTH/FTTP) - Very stable, low latency, high speed</summary>
15-
Fiber,
14+
/// <summary>GPON Fiber - Shared splitter, slight peak-hour congestion</summary>
15+
Gpon,
1616

1717
/// <summary>DSL (ADSL/VDSL) - Stable, lower speeds, distance-dependent</summary>
1818
Dsl,
@@ -21,7 +21,10 @@ public enum ConnectionType
2121
FixedWireless,
2222

2323
/// <summary>Fixed LTE/5G - Variable, cell congestion-sensitive</summary>
24-
CellularHome
24+
CellularHome,
25+
26+
/// <summary>XGS-PON Fiber - Near line-rate, minimal congestion</summary>
27+
XgsPon
2528
}
2629

2730
/// <summary>
@@ -142,8 +145,11 @@ private int CalculateMaxSpeed(int nominalSpeed)
142145
{
143146
return Type switch
144147
{
145-
// Fiber often exceeds advertised speeds
146-
ConnectionType.Fiber => (int)(nominalSpeed * 1.05),
148+
// GPON: often exceeds advertised speeds
149+
ConnectionType.Gpon => (int)(nominalSpeed * 1.05),
150+
151+
// XGS-PON: 10G headroom, easily exceeds advertised
152+
ConnectionType.XgsPon => (int)(nominalSpeed * 1.05),
147153

148154
// DOCSIS cable typically hits 95% of advertised
149155
ConnectionType.DocsisCable => (int)(nominalSpeed * 0.95),
@@ -171,8 +177,11 @@ private int CalculateMinSpeed(int nominalSpeed)
171177
{
172178
return Type switch
173179
{
174-
// Fiber is very consistent
175-
ConnectionType.Fiber => (int)(nominalSpeed * 0.90),
180+
// GPON: consistent, splitter contention is minor
181+
ConnectionType.Gpon => (int)(nominalSpeed * 0.90),
182+
183+
// XGS-PON: very consistent
184+
ConnectionType.XgsPon => (int)(nominalSpeed * 0.92),
176185

177186
// DOCSIS can drop during peak congestion
178187
ConnectionType.DocsisCable => (int)(nominalSpeed * 0.65),
@@ -200,7 +209,8 @@ private int CalculateAbsoluteMax(int nominalSpeed)
200209
{
201210
return Type switch
202211
{
203-
ConnectionType.Fiber => (int)(nominalSpeed * 1.07),
212+
ConnectionType.Gpon => (int)(nominalSpeed * 1.07),
213+
ConnectionType.XgsPon => (int)(nominalSpeed * 1.07),
204214
ConnectionType.DocsisCable => (int)(nominalSpeed * 0.98),
205215
ConnectionType.Starlink => (int)(nominalSpeed * 1.15),
206216
ConnectionType.Dsl => (int)(nominalSpeed * 0.98),
@@ -217,8 +227,11 @@ private double GetOverheadMultiplier()
217227
{
218228
return Type switch
219229
{
220-
// Fiber: stable, can run close to line rate
221-
ConnectionType.Fiber => 1.08,
230+
// GPON: stable, can run close to line rate
231+
ConnectionType.Gpon => 1.08,
232+
233+
// XGS-PON: very stable, near line rate
234+
ConnectionType.XgsPon => 1.08,
222235

223236
// DOCSIS: needs buffer for peak-hour congestion
224237
ConnectionType.DocsisCable => 1.05,
@@ -246,7 +259,8 @@ private double GetBaselineLatency()
246259
{
247260
return Type switch
248261
{
249-
ConnectionType.Fiber => 5.0,
262+
ConnectionType.Gpon => 5.0,
263+
ConnectionType.XgsPon => 4.0,
250264
ConnectionType.DocsisCable => 18.0,
251265
ConnectionType.Starlink => 25.0,
252266
ConnectionType.Dsl => 20.0,
@@ -263,8 +277,11 @@ private double GetLatencyThreshold()
263277
{
264278
return Type switch
265279
{
266-
// Fiber: tight threshold, consistent connection
267-
ConnectionType.Fiber => 2.0,
280+
// GPON: tight threshold, fiber latency is clean enough to detect mild congestion
281+
ConnectionType.Gpon => 1.0,
282+
283+
// XGS-PON: tight threshold, 10G headroom means very stable latency
284+
ConnectionType.XgsPon => 1.0,
268285

269286
// DOCSIS: moderate threshold
270287
ConnectionType.DocsisCable => 2.5,
@@ -292,7 +309,8 @@ private double GetLatencyDecrease()
292309
{
293310
return Type switch
294311
{
295-
ConnectionType.Fiber => 0.98,
312+
ConnectionType.Gpon => 0.98,
313+
ConnectionType.XgsPon => 0.99,
296314
ConnectionType.DocsisCable => 0.97,
297315
ConnectionType.Starlink => 0.97,
298316
ConnectionType.Dsl => 0.97,
@@ -309,7 +327,8 @@ private double GetLatencyIncrease()
309327
{
310328
return Type switch
311329
{
312-
ConnectionType.Fiber => 1.03,
330+
ConnectionType.Gpon => 1.03,
331+
ConnectionType.XgsPon => 1.02,
313332
ConnectionType.DocsisCable => 1.04,
314333
ConnectionType.Starlink => 1.04,
315334
ConnectionType.Dsl => 1.03,
@@ -327,10 +346,13 @@ private double GetSafetyCapPercent()
327346
{
328347
return Type switch
329348
{
330-
// Fiber: 95% cap gives htb headroom to shape properly at near-line-rate
349+
// GPON: 95% cap gives htb headroom to shape properly at near-line-rate
331350
// At 98% (980 Mbps on 1G), fq_codel memory fills before AQM can react
332351
// At 95% (950 Mbps on 1G), htb absorbs bursts and fq_codel manages flows cleanly
333-
ConnectionType.Fiber => 0.95,
352+
ConnectionType.Gpon => 0.95,
353+
354+
// XGS-PON: same 95% cap for htb headroom
355+
ConnectionType.XgsPon => 0.95,
334356

335357
// All other types: 95% safety margin below the bottleneck
336358
_ => 0.95
@@ -341,7 +363,7 @@ private double GetSafetyCapPercent()
341363
/// Get 168-hour baseline dictionary scaled to nominal speed.
342364
/// Keys are "day_hour" format (0=Mon, 6=Sun), values are speeds in Mbps.
343365
/// </summary>
344-
public Dictionary<string, string> GetHourlyBaseline()
366+
public Dictionary<string, string> GetHourlyBaseline(double congestionSeverity = 1.0)
345367
{
346368
var baseline = new Dictionary<string, string>();
347369
var pattern = GetBaselinePattern();
@@ -351,8 +373,15 @@ public Dictionary<string, string> GetHourlyBaseline()
351373
for (int hour = 0; hour < 24; hour++)
352374
{
353375
var key = $"{day}_{hour}";
354-
// Scale the pattern percentage by nominal speed
355-
var speed = (int)(pattern[day, hour] * NominalDownloadMbps);
376+
var multiplier = pattern[day, hour];
377+
378+
// Scale the dip magnitude: effective = 1.0 - (1.0 - multiplier) * severity
379+
// At severity 1.0: unchanged. At 1.1: 10% deeper dips. At 0.9: 10% shallower.
380+
// Hours at 1.0 stay at 1.0 regardless of severity.
381+
if (congestionSeverity != 1.0)
382+
multiplier = 1.0 - (1.0 - multiplier) * congestionSeverity;
383+
384+
var speed = (int)(multiplier * NominalDownloadMbps);
356385
baseline[key] = speed.ToString();
357386
}
358387
}
@@ -378,11 +407,16 @@ public Dictionary<string, string> GetHourlyBaseline()
378407
? (0.60, 0.40) // 60/40 favor baseline when close
379408
: (0.80, 0.20), // 80/20 heavily favor baseline when below
380409

381-
// Fiber: very stable, trust baseline heavily
382-
ConnectionType.Fiber => withinThreshold
410+
// GPON: stable, trust baseline heavily
411+
ConnectionType.Gpon => withinThreshold
383412
? (0.70, 0.30)
384413
: (0.85, 0.15),
385414

415+
// XGS-PON: very stable, trust baseline most
416+
ConnectionType.XgsPon => withinThreshold
417+
? (0.75, 0.25)
418+
: (0.90, 0.10),
419+
386420
// DSL: stable once synced
387421
ConnectionType.Dsl => withinThreshold
388422
? (0.65, 0.35)
@@ -397,6 +431,11 @@ public Dictionary<string, string> GetHourlyBaseline()
397431
};
398432
}
399433

434+
/// <summary>
435+
/// Get the raw baseline pattern for UI display (e.g., congestion range preview).
436+
/// </summary>
437+
public double[,] GetBaselinePatternPublic() => GetBaselinePattern();
438+
400439
/// <summary>
401440
/// Get 168-hour baseline pattern as percentage of nominal speed (7 days × 24 hours).
402441
/// Based on real-world data from DOCSIS and Starlink connections.
@@ -446,9 +485,17 @@ public Dictionary<string, string> GetHourlyBaseline()
446485
{ 0.77, 0.75, 0.79, 0.67, 0.49, 0.44, 0.41, 0.43, 0.52, 0.87, 0.71, 0.55, 0.60, 0.51, 0.66, 0.77, 0.72, 0.71, 0.71, 0.70, 0.70, 0.48, 0.41, 0.62 }
447486
},
448487

449-
// Fiber: very stable, minimal variation (same pattern all week)
450-
ConnectionType.Fiber => CreateUniformWeekPattern(new double[]
451-
{ 0.98, 0.98, 0.98, 0.98, 0.98, 0.98, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.95, 0.95, 0.95, 0.95, 0.98, 0.98 }),
488+
// GPON: shared splitter means noticeable congestion at peak hours.
489+
// Smooth curve: 1.00 overnight → 0.99 morning → 0.98/0.975 → 0.97 workday → 0.965 streaming → taper back up
490+
// Hour: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
491+
ConnectionType.Gpon => CreateUniformWeekPattern(new double[]
492+
{ 0.995, 1.00, 1.00, 1.00, 1.00, 1.00, 0.99, 0.99, 0.98, 0.975, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.965, 0.965, 0.965, 0.965, 0.975, 0.985 }),
493+
494+
// XGS-PON: 10G headroom, minimal congestion even at peak.
495+
// Compressed version of GPON curve: 1.00 overnight → 0.995 → 0.99/0.9875 → 0.985 workday → 0.98 streaming
496+
// Hour: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
497+
ConnectionType.XgsPon => CreateUniformWeekPattern(new double[]
498+
{ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.995, 0.995, 0.99, 0.9875, 0.985, 0.985, 0.985, 0.985, 0.985, 0.985, 0.985, 0.985, 0.98, 0.98, 0.98, 0.98, 0.985, 0.99 }),
452499

453500
// DSL: stable but may have minor peak-hour drops (same pattern all week)
454501
ConnectionType.Dsl => CreateUniformWeekPattern(new double[]
@@ -532,7 +579,8 @@ public static string GetConnectionTypeName(ConnectionType type)
532579
{
533580
ConnectionType.DocsisCable => "DOCSIS Cable",
534581
ConnectionType.Starlink => "Starlink",
535-
ConnectionType.Fiber => "Fiber (FTTH)",
582+
ConnectionType.Gpon => "Fiber (GPON)",
583+
ConnectionType.XgsPon => "Fiber (XGS-PON)",
536584
ConnectionType.Dsl => "DSL",
537585
ConnectionType.FixedWireless => "Fixed Wireless (WISP)",
538586
ConnectionType.CellularHome => "Fixed LTE/5G",
@@ -549,7 +597,8 @@ public static string GetConnectionTypeDescription(ConnectionType type)
549597
{
550598
ConnectionType.DocsisCable => "Slows during prime time",
551599
ConnectionType.Starlink => "Weather and congestion dependent",
552-
ConnectionType.Fiber => "Fast and reliable",
600+
ConnectionType.Gpon => "Shared splitter, slight peak-hour dip",
601+
ConnectionType.XgsPon => "Near line-rate, minimal congestion",
553602
ConnectionType.Dsl => "Line quality varies by distance",
554603
ConnectionType.FixedWireless => "Weather and interference sensitive",
555604
ConnectionType.CellularHome => "Varies by tower load",

src/NetworkOptimizer.Sqm/Models/SqmConfiguration.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public class SqmConfiguration
115115
/// <summary>
116116
/// Ping adjustment interval in minutes (default: 5)
117117
/// </summary>
118-
public int PingAdjustmentInterval { get; set; } = 5;
118+
public int PingAdjustmentInterval { get; set; } = 1;
119119

120120
/// <summary>
121121
/// Learning mode enabled - collect baseline data without aggressive adjustments
@@ -148,6 +148,13 @@ public class SqmConfiguration
148148
/// </summary>
149149
public double SafetyCapPercent { get; set; } = 0.95;
150150

151+
/// <summary>
152+
/// Congestion severity multiplier (0.9-1.1, default 1.0).
153+
/// Scales the magnitude of baseline schedule dips while keeping 1.0 hours unchanged.
154+
/// effective = 1.0 - (1.0 - schedule_multiplier) * severity
155+
/// </summary>
156+
public double CongestionSeverity { get; set; } = 1.0;
157+
151158
/// <summary>
152159
/// Get the ConnectionProfile for this configuration
153160
/// </summary>

src/NetworkOptimizer.Sqm/ScriptGenerator.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ public string GenerateBootScript(Dictionary<string, string> baseline)
7676
sb.AppendLine("RESULT_FILE=\"$SQM_DIR/${SQM_NAME}-result.txt\"");
7777
sb.AppendLine("LOG_FILE=\"/var/log/sqm-${SQM_NAME}.log\"");
7878
sb.AppendLine();
79+
// Rotate log on boot/deploy: keep last 2000 lines (~1.5 days at 1 min ping interval)
80+
sb.AppendLine("# Rotate log to prevent unbounded growth");
81+
sb.AppendLine("if [ -f \"$LOG_FILE\" ] && [ $(wc -l < \"$LOG_FILE\") -gt 2000 ]; then");
82+
sb.AppendLine(" tail -n 2000 \"$LOG_FILE\" > \"${LOG_FILE}.tmp\" && mv \"${LOG_FILE}.tmp\" \"$LOG_FILE\"");
83+
sb.AppendLine("fi");
84+
sb.AppendLine();
7985
sb.AppendLine("echo \"[$(date)] SQM boot script starting for $SQM_NAME ($INTERFACE)...\" >> $LOG_FILE");
8086
sb.AppendLine();
8187

@@ -361,6 +367,7 @@ private string GeneratePingScript(Dictionary<string, string> baseline)
361367
sb.AppendLine($"UPLOAD_SPEED=\"{_config.NominalUploadSpeed}\"");
362368
sb.AppendLine($"SHAPE_UPLOAD={(_config.ShapeUpload ? "1" : "0")}");
363369
sb.AppendLine($"SAFETY_CAP=\"{Inv(_config.SafetyCapPercent)}\"");
370+
sb.AppendLine($"NOMINAL_SPEED=\"{_config.NominalDownloadSpeed}\"");
364371
sb.AppendLine($"RESULT_FILE=\"/data/sqm/{_name}-result.txt\"");
365372
sb.AppendLine($"LOG_FILE=\"/var/log/sqm-{_name}.log\"");
366373
sb.AppendLine();
@@ -418,9 +425,34 @@ private string GeneratePingScript(Dictionary<string, string> baseline)
418425
sb.AppendLine(GetBaselineBlendingLogicForPing());
419426
sb.AppendLine();
420427

428+
// Apply safety cap to MAX_DOWNLOAD_SPEED BEFORE latency adjustment.
429+
// This sets the schedule-derived ceiling as the starting point, then latency
430+
// can freely decrease below it. Without this, the cap creates a dead zone where
431+
// mild latency spikes are detected but produce no visible rate change.
432+
var useBaselineRatio = _config.ConnectionType is ConnectionType.Gpon or ConnectionType.XgsPon;
433+
if (useBaselineRatio)
434+
{
435+
sb.AppendLine("# Apply baseline-proportional safety cap before latency adjustment (fiber)");
436+
sb.AppendLine("if [ -n \"$baseline_speed\" ] && [ \"$NOMINAL_SPEED\" -gt 0 ]; then");
437+
sb.AppendLine(" baseline_ratio=$(echo \"scale=4; $baseline_speed / $NOMINAL_SPEED\" | bc)");
438+
sb.AppendLine(" max_adjusted_rate=$(echo \"scale=0; $ABSOLUTE_MAX_DOWNLOAD_SPEED * $SAFETY_CAP * $baseline_ratio / 1\" | bc)");
439+
sb.AppendLine("else");
440+
sb.AppendLine(" max_adjusted_rate=$(echo \"$ABSOLUTE_MAX_DOWNLOAD_SPEED * $SAFETY_CAP\" | bc)");
441+
sb.AppendLine("fi");
442+
}
443+
else
444+
{
445+
sb.AppendLine("# Apply flat safety cap before latency adjustment");
446+
sb.AppendLine("max_adjusted_rate=$(echo \"$ABSOLUTE_MAX_DOWNLOAD_SPEED * $SAFETY_CAP\" | bc)");
447+
}
448+
sb.AppendLine("if (( $(echo \"$MAX_DOWNLOAD_SPEED > $max_adjusted_rate\" | bc) )); then");
449+
sb.AppendLine(" MAX_DOWNLOAD_SPEED=$(echo \"scale=0; $max_adjusted_rate / 1\" | bc)");
450+
sb.AppendLine("fi");
451+
sb.AppendLine();
452+
421453
// Measure latency with validation
422454
sb.AppendLine("# Measure latency");
423-
sb.AppendLine($"latency=$(ping -I $INTERFACE -c 20 -i 0.25 -q \"$PING_HOST\" 2>/dev/null | tail -n 1 | awk -F '/' '{{print $5}}')");
455+
sb.AppendLine($"latency=$(ping -I $INTERFACE -c 10 -i 0.5 -q \"$PING_HOST\" 2>/dev/null | tail -n 1 | awk -F '/' '{{print $5}}')");
424456
sb.AppendLine();
425457
sb.AppendLine("# Validate latency result");
426458
sb.AppendLine("if [ -z \"$latency\" ]; then");
@@ -437,13 +469,11 @@ private string GeneratePingScript(Dictionary<string, string> baseline)
437469
sb.AppendLine("deviation_count=$(echo \"($latency - $BASELINE_LATENCY) / $LATENCY_THRESHOLD\" | bc)");
438470
sb.AppendLine();
439471

440-
// Latency adjustment logic
472+
// Latency adjustment logic (operates on capped MAX_DOWNLOAD_SPEED, can decrease freely)
441473
sb.AppendLine(GetLatencyAdjustmentLogic());
442474
sb.AppendLine();
443475

444-
// Apply limits
445-
sb.AppendLine("# Apply limits");
446-
sb.AppendLine("max_adjusted_rate=$(echo \"$ABSOLUTE_MAX_DOWNLOAD_SPEED * $SAFETY_CAP\" | bc)");
476+
// Post-latency ceiling: prevent increase branch from exceeding schedule cap
447477
sb.AppendLine("if (( $(echo \"$new_rate > $max_adjusted_rate\" | bc) )); then");
448478
sb.AppendLine(" new_rate=$max_adjusted_rate");
449479
sb.AppendLine("fi");
@@ -479,6 +509,15 @@ private string GeneratePingScript(Dictionary<string, string> baseline)
479509
// TC update function and apply
480510
sb.AppendLine(GetTcUpdateFunction());
481511
sb.AppendLine();
512+
513+
// Skip tc update if rate hasn't changed (avoids no-op tc rewrites every minute)
514+
sb.AppendLine("# Skip tc update if rate unchanged");
515+
sb.AppendLine("current_rate=$(tc class show dev $IFB_DEVICE 2>/dev/null | grep \"class htb 1:1 root\" | grep -o \"rate [0-9]*Mbit\" | grep -o \"[0-9]*\")");
516+
sb.AppendLine("if [ \"$new_rate_int\" = \"$current_rate\" ]; then");
517+
sb.AppendLine(" exit 0");
518+
sb.AppendLine("fi");
519+
sb.AppendLine();
520+
482521
sb.AppendLine("update_all_tc_classes $IFB_DEVICE $new_rate_int");
483522
sb.AppendLine("# Upstream: shape rate if enabled, otherwise just tune performance params");
484523
sb.AppendLine("if [ \"$SHAPE_UPLOAD\" = \"1\" ]; then");
@@ -696,8 +735,10 @@ private string GetLatencyAdjustmentLogic()
696735
{
697736
return @"# Latency-based adjustment
698737
if (( $(echo ""$latency >= $BASELINE_LATENCY + $LATENCY_THRESHOLD"" | bc -l) )); then
699-
# High latency: decrease rate
700-
decrease_multiplier=$(echo ""$LATENCY_DECREASE^$deviation_count"" | bc -l)
738+
# High latency: decrease rate with non-linear response ((n+1)^0.7 - 1)
739+
# Gentle at low deviations (transient spikes), aggressive at high (real congestion)
740+
effective_count=$(echo ""scale=4; e(0.7 * l($deviation_count + 1)) - 1"" | bc -l)
741+
decrease_multiplier=$(echo ""e($effective_count * l($LATENCY_DECREASE))"" | bc -l)
701742
new_rate=$(echo ""$MAX_DOWNLOAD_SPEED * $decrease_multiplier"" | bc)
702743
if (( $(echo ""$new_rate < $MIN_DOWNLOAD_SPEED"" | bc) )); then
703744
new_rate=$MIN_DOWNLOAD_SPEED

0 commit comments

Comments
 (0)