diff --git a/.jules/bolt.md b/.jules/bolt.md index 2773078b..817241b9 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2026-04-12 - Avoid LINQ in per-frame hot path **Learning:** LINQ methods like `Where` and `FirstOrDefault` implicitly allocate enumerators and closures when capturing state (e.g., `Context.Color` or lambda expressions). In a 100Hz real-time loop like `Ai.UpdateContext()` and `Ai.Process()`, these allocations stack up quickly, causing significant GC pressure and potential micro-stutters. **Action:** Replace `LINQ` operations with manual `foreach` or `for` loops in the per-frame hot path to achieve zero-allocation data iteration. + +## 2026-05-28 - String Interpolation and LINQ inside Log calls +**Learning:** `Log.ZLogDebug` calls that include string interpolation (using `$""`) combined with complex expressions like `string.Join` and `.Select()` will evaluate these expressions and allocate memory (strings, closures, arrays) *before* the logger's internal log level filter is checked. If debug logging is disabled, these allocations are completely wasted. +**Action:** Always wrap expensive log messages (those doing LINQ, allocations, or complex string concatenations) inside an explicit `if (Log.IsEnabled(LogLevel.Debug))` block to prevent evaluating the arguments when the log level is not active. diff --git a/Soccer/Ai.cs b/Soccer/Ai.cs index 5ab045b0..7d3130cb 100644 --- a/Soccer/Ai.cs +++ b/Soccer/Ai.cs @@ -174,9 +174,13 @@ public void Process() var newRoleMapping = assignmentResult.RoleMapping; Log.ZLogDebug($"Role assignment total cost: {assignmentResult.TotalCost:F3}"); - foreach (var unfilledRole in assignmentResult.UnfilledRoles.Where(r => r.IsRequired)) + // Bolt: eliminates LINQ .Where() closure and enumerator allocs per frame + foreach (var unfilledRole in assignmentResult.UnfilledRoles) { - Log.ZLogWarning($"Required role left unfilled: {unfilledRole.Role}"); + if (unfilledRole.IsRequired) + { + Log.ZLogWarning($"Required role left unfilled: {unfilledRole.Role}"); + } } Context.Data.Value = Context.Data.Value! with diff --git a/Soccer/Knowledge/Knowledge.AttackerDecision.cs b/Soccer/Knowledge/Knowledge.AttackerDecision.cs index 576980fa..55d7b5d2 100644 --- a/Soccer/Knowledge/Knowledge.AttackerDecision.cs +++ b/Soccer/Knowledge/Knowledge.AttackerDecision.cs @@ -25,6 +25,7 @@ public partial class Knowledge private readonly Dictionary _attackerDecisions = []; private readonly Dictionary _passShootHysteresisByRobot = []; + private readonly List _staleHysteresisKeys = []; // Bolt: pre-allocated list to avoid per-frame allocations public AttackerDecision GetAttackerDecision(int robotId) { @@ -49,7 +50,17 @@ private void UpdateAttackerDecisions() _attackerDecisions[robotId] = BuildAttackerDecision(assignment.Robot, attackerRole); } - foreach (var robotId in _passShootHysteresisByRobot.Keys.Where(id => !assignedAttackers.Contains(id)).ToList()) + // Bolt: eliminates LINQ .Where(...).ToList() allocation per frame + _staleHysteresisKeys.Clear(); + foreach (var robotId in _passShootHysteresisByRobot.Keys) + { + if (!assignedAttackers.Contains(robotId)) + { + _staleHysteresisKeys.Add(robotId); + } + } + + foreach (var robotId in _staleHysteresisKeys) { _passShootHysteresisByRobot.Remove(robotId); } diff --git a/Soccer/TeamRunner.cs b/Soccer/TeamRunner.cs index beb536b4..2c2ea0fe 100644 --- a/Soccer/TeamRunner.cs +++ b/Soccer/TeamRunner.cs @@ -100,8 +100,11 @@ private bool Tick() ApplySimFeedback(simFeedback); - foreach (var robot in Context.OwnRobots.Where(r => r.HardwareStatus.IsValid())) + // Bolt: eliminates LINQ .Where() closure and enumerator allocs per frame + foreach (var robot in Context.OwnRobots) { + if (!robot.HardwareStatus.IsValid()) continue; + var now = Timestamp.Now; if (now - robot.HardwareStatus.LastUpdate > HardwareStatusValidTime) { @@ -126,12 +129,16 @@ private bool Tick() } } - Log.ZLogDebug( - $"Robot {status.Info!.RobotId}: " + - $"battery={status.Power?.V24Voltage:F2}V " + - $"temp={status.Diag?.ImuTemp:F1}°C " + - $"ball={robot.HasBallContact} " + - $"motors=[{string.Join(", ", status.Motors?.Motors.Select(m => $"{m.Target:F0}/{m.Actual:F0}") ?? [])}]"); + // Bolt: gates expensive LINQ and string interpolation inside the hot path when debug logs are off + if (Log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + { + Log.ZLogDebug( + $"Robot {status.Info!.RobotId}: " + + $"battery={status.Power?.V24Voltage:F2}V " + + $"temp={status.Diag?.ImuTemp:F1}°C " + + $"ball={robot.HasBallContact} " + + $"motors=[{string.Join(", ", status.Motors?.Motors.Select(m => $"{m.Target:F0}/{m.Actual:F0}") ?? [])}]"); + } } if (_refereeSubscriber.Reader.TryRead(out var referee))