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-07-26 - Replace LINQ GroupBy with fixed-size array in RobotMerger
**Learning:** GroupBy and ToDictionary in a per-frame vision processing loop (~100Hz) causes significant GC pressure by allocating dictionaries, closures, and enumerators on the heap.
**Action:** Use a pre-allocated fixed-size array of lists based on Robot ID + Team Color index mapping for zero-allocation grouping.
84 changes: 77 additions & 7 deletions Vision/Tracking/RobotMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,88 @@ 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 ~N allocs/frame — replacing LINQ GroupBy/ToDictionary with fixed-size arrays and loops
private readonly List<RobotTracker>[] _trackersByRobotIndex = new List<RobotTracker>[Tyr.Common.Data.CommonConfigs.MaxRobots * 2];
private readonly Dictionary<RobotId, List<RobotTracker>> _fallbackTrackers = new();

public RobotMerger()
{
for (int i = 0; i < _trackersByRobotIndex.Length; i++)
{
_trackersByRobotIndex[i] = new List<RobotTracker>(8);
}
}

private int GetRobotIndex(RobotId id)
{
if (!id.Id.HasValue || !id.Team.HasValue || id.Team == Tyr.Common.Data.TeamColor.Unknown)
return -1;

var idVal = id.Id.Value;
if (idVal >= Tyr.Common.Data.CommonConfigs.MaxRobots)
return -1;

return id.Team == Tyr.Common.Data.TeamColor.Yellow ? (int)idVal : (int)idVal + Tyr.Common.Data.CommonConfigs.MaxRobots;
}

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());
for (int i = 0; i < _trackersByRobotIndex.Length; i++)
{
_trackersByRobotIndex[i].Clear();
}

foreach (var trackers in _fallbackTrackers.Values)
{
trackers.Clear();
}

var mergedRobots = new List<FilteredRobot>();
foreach (var camera in cameras)
{
foreach (var tracker in camera.Robots.Values)
{
var index = GetRobotIndex(tracker.Id);
if (index >= 0)
{
_trackersByRobotIndex[index].Add(tracker);
}
else
{
if (!_fallbackTrackers.TryGetValue(tracker.Id, out var fallbackList))
{
fallbackList = new List<RobotTracker>(8);
_fallbackTrackers[tracker.Id] = fallbackList;
}
fallbackList.Add(tracker);
}
}
}

var mergedRobots = new List<FilteredRobot>(Tyr.Common.Data.CommonConfigs.MaxRobots * 2);

for (uint idVal = 0; idVal < Tyr.Common.Data.CommonConfigs.MaxRobots; idVal++)
{
var yellowIndex = (int)idVal;
var yellowTrackers = _trackersByRobotIndex[yellowIndex];
if (yellowTrackers.Count > 0)
{
mergedRobots.Add(Merge(new RobotId { Id = idVal, Team = Tyr.Common.Data.TeamColor.Yellow }, yellowTrackers, timestamp));
}

var blueIndex = (int)idVal + Tyr.Common.Data.CommonConfigs.MaxRobots;
var blueTrackers = _trackersByRobotIndex[blueIndex];
if (blueTrackers.Count > 0)
{
mergedRobots.Add(Merge(new RobotId { Id = idVal, Team = Tyr.Common.Data.TeamColor.Blue }, blueTrackers, timestamp));
}
}

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

return mergedRobots;
Expand Down
Loading