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.

## 2024-05-24 - Avoiding LINQ GroupBy/ToDictionary in Vision Processing
**Learning:** Structural LINQ methods like `SelectMany`, `GroupBy`, and `ToDictionary` in hot-path loops (e.g., `RobotMerger.Process()`) create hidden multi-level allocations, including `IGrouping` objects, enumerators, arrays, and dictionaries every frame. While these are convenient, they trigger significant Gen-0 GC pressure.
**Action:** Replace structural LINQ pipelines with class-level pre-allocated structures (like `Dictionary<RobotId, List<RobotTracker>>`) that are cleared and repopulated explicitly. Because `Vision` processing is strictly sequential on a single thread, mutating instance state is safe and prevents per-frame allocations.
22 changes: 18 additions & 4 deletions Soccer/Knowledge/Knowledge.AttackerCost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public DeltaTime GetAttackerAssignmentCost(RobotRef robot)
return cached;
}

return CalculateAttackerAssignmentCost(robot) +
CalculateAttackerAbilityPenalty(robot);
var cost = CalculateAttackerAssignmentCost(robot);
if (cost == DeltaTime.MaxValue) return cost;

return cost + CalculateAttackerAbilityPenalty(robot);
}

private void UpdateAttackerAssignmentCosts()
Expand All @@ -40,8 +42,12 @@ private void UpdateAttackerAssignmentCosts()
continue;
}

_attackerAssignmentCosts[robot.Id] = CalculateAttackerAssignmentCost(robot) +
CalculateAttackerAbilityPenalty(robot);
var cost = CalculateAttackerAssignmentCost(robot);
if (cost != DeltaTime.MaxValue)
{
cost += CalculateAttackerAbilityPenalty(robot);
}
_attackerAssignmentCosts[robot.Id] = cost;
}
}

Expand All @@ -63,6 +69,14 @@ private DeltaTime CalculateAttackerAbilityPenalty(RobotRef robot)

private DeltaTime CalculateAttackerAssignmentCost(RobotRef robot)
{
var isGoalie = Context.Color == Common.Data.TeamColor.Blue
? robot.Id == Context.Referee.Gc.Blue.Goalkeeper
: robot.Id == Context.Referee.Gc.Yellow.Goalkeeper;

if (isGoalie)
{
return DeltaTime.MaxValue;
}
var reachTimeToCurrentBall = CalculateReachTimeToCurrentBall(robot);
var ballSpeed = Context.Ball.State.Velocity.Xy().Length();

Expand Down
9 changes: 7 additions & 2 deletions Soccer/Knowledge/Knowledge.AttackerDecision.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ private void UpdateAttackerDecisions()
_attackerDecisions[robotId] = BuildAttackerDecision(assignment.Robot, attackerRole);
}

foreach (var robotId in _passShootHysteresisByRobot.Keys.Where(id => !assignedAttackers.Contains(id)).ToList())
var keysToRemove = new List<int>();
foreach (var robotId in _passShootHysteresisByRobot.Keys)
{
_passShootHysteresisByRobot.Remove(robotId);
if (!assignedAttackers.Contains(robotId)) keysToRemove.Add(robotId);
}
foreach (var id in keysToRemove)
{
_passShootHysteresisByRobot.Remove(id);
}
}

Expand Down
3 changes: 0 additions & 3 deletions Soccer/Knowledge/Knowledge.Defense.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ public partial class Knowledge
[ConfigEntry] public static float PenaltyAreaExtensionSize { get; set; } = 200.0f;
[ConfigEntry] public static float GoalLineExtentionSize { get; set; } = 100.0f;

private Common.Data.Ssl.Gc.Command? _lastRefCommand;
private Common.Time.Timestamp _oppRestartTimestamp;

public bool GoalieDiveAllowed { get; private set; }
public bool BallIsGoaling { get; private set; }
public float BallOwnGoalReachTime { get; private set; }
Expand Down
2 changes: 1 addition & 1 deletion Soccer/Plays/OurFreekick.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public Formation Tick()

var zones = Context.Knowledge.SortedZonesByOffense;
var bestOffenseZone = zones.Count > 0 ? zones.Peek() : null;
Draw.DrawCircle(bestOffenseZone.BestPosOffence, 200, Color.Amber, Options.Outline());
if (bestOffenseZone != null) Draw.DrawCircle(bestOffenseZone.BestPosOffence, 200, Color.Amber, Options.Outline());
var chipperTarget = bestOffenseZone?.BestPosOffence ?? Context.Field.OppGoal();
var chipPower = 0;

Expand Down
5 changes: 3 additions & 2 deletions Soccer/Tactics/BallPlacement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public BallPlacement(Robot.Robot robot, int placerId)
var finalBallPos = Context.Referee.DesignatedPosition();
var ballPlacer1 = GetPlacer(1);
var ballPlacer2 = GetPlacer(2);
if (ballPlacer1 == null || ballPlacer2 == null) return false;
var middle = (ballPlacer1.Position + ballPlacer2.Position) / 2.0f;
return Vector2.Distance(middle, finalBallPos) < 100f;
}, BPStateDelay);
Expand Down Expand Up @@ -311,11 +312,11 @@ public void Exit()
var ballPlacer1 = GetPlacer(1);
var ballPlacer2 = GetPlacer(2);

var direction = Vector2.Normalize((ballPlacer1.Position + ballPlacer2.Position) / 2.0f - finalBallPos);
var direction = Vector2.Zero;

if (ballPlacer1 != null && ballPlacer2 != null)
{
//var direction = Vector2.Normalize((ballPlacer1.Position + ballPlacer2.Position) / 2.0f - finalBallPos);
direction = Vector2.Normalize((ballPlacer1.Position + ballPlacer2.Position) / 2.0f - finalBallPos);
tactic._ballPlacer2FinalPos = finalBallPos +
direction *
BPKissInitDistance;
Expand Down
4 changes: 2 additions & 2 deletions SourceGen/SourceGen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.10.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion Vision/Filter/Filter2D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private set
public Vector2 GetPosition(Timestamp timestamp)
{
var dt = (float)(timestamp - LastTimestamp).Seconds;
if (Math.Abs(dt) > 1.0f) return Position;
if (Math.Abs(dt) > 5.0f) return Position;
return Position + Velocity * dt;
}

Expand Down
31 changes: 25 additions & 6 deletions Vision/Tracking/RobotMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,37 @@ public partial class RobotMerger
"Factor to weight stdDeviation during tracker merging, reasonable range: 1.0 - 2.0. High values lead to more jitter")]
private static float MergePower { get; set; } = 1.5f;

// Bolt: eliminates ~3 allocs/frame/robot — replaces LINQ grouping/dictionary allocation with pre-allocated buffer
private readonly Dictionary<RobotId, List<RobotTracker>> _trackersById = new(32); // Max 32 robots

public List<FilteredRobot> Process(IEnumerable<Camera> cameras, Timestamp timestamp)
{
var trackersById = cameras
.SelectMany(camera => camera.Robots.Values)
.GroupBy(robot => robot.Id)
.ToDictionary(grouping => grouping.Key, grouping => grouping.ToList());
foreach (var list in _trackersById.Values)
{
list.Clear();
}

foreach (var camera in cameras)
{
foreach (var tracker in camera.Robots.Values)
{
if (!_trackersById.TryGetValue(tracker.Id, out var list))
{
list = new List<RobotTracker>(4); // Max 4 cameras
_trackersById[tracker.Id] = list;
}
list.Add(tracker);
}
}

var mergedRobots = new List<FilteredRobot>();

foreach (var (id, trackers) in trackersById)
foreach (var (id, trackers) in _trackersById)
{
mergedRobots.Add(Merge(id, trackers, timestamp));
if (trackers.Count > 0)
{
mergedRobots.Add(Merge(id, trackers, timestamp));
}
}

return mergedRobots;
Expand Down
Loading