diff --git a/.jules/bolt.md b/.jules/bolt.md index 2773078..bc7536a 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-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. diff --git a/Vision/Tracking/RobotMerger.cs b/Vision/Tracking/RobotMerger.cs index b217fb7..a0e8ffb 100644 --- a/Vision/Tracking/RobotMerger.cs +++ b/Vision/Tracking/RobotMerger.cs @@ -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[] _trackersByRobotIndex = new List[Tyr.Common.Data.CommonConfigs.MaxRobots * 2]; + private readonly Dictionary> _fallbackTrackers = new(); + + public RobotMerger() + { + for (int i = 0; i < _trackersByRobotIndex.Length; i++) + { + _trackersByRobotIndex[i] = new List(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 Process(IEnumerable 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(); + 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(8); + _fallbackTrackers[tracker.Id] = fallbackList; + } + fallbackList.Add(tracker); + } + } + } + + var mergedRobots = new List(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;