Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions Soccer/Ai.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion Soccer/Knowledge/Knowledge.AttackerDecision.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public partial class Knowledge

private readonly Dictionary<int, AttackerDecision> _attackerDecisions = [];
private readonly Dictionary<int, int> _passShootHysteresisByRobot = [];
private readonly List<int> _staleHysteresisKeys = []; // Bolt: pre-allocated list to avoid per-frame allocations

public AttackerDecision GetAttackerDecision(int robotId)
{
Expand All @@ -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);
}
Expand Down
21 changes: 14 additions & 7 deletions Soccer/TeamRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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))
Expand Down
Loading