Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cf2f49c
(#80) Add coordination test mode
aidnem Feb 27, 2026
73fb62a
(#80) Implement shot tuning mode
aidnem Feb 27, 2026
494d842
(#80) More shooter and hood tuning
aidnem Feb 28, 2026
16e9bb1
(#80) Add shot map tuning tab with video recording, NT4 telemetry, an…
godmar Feb 28, 2026
4ceaea2
(#80) Add shot tuning implementation plan
godmar Feb 28, 2026
99a2d48
(instructions given to Claude at first)
godmar Feb 28, 2026
4c1664c
Merge remote-tracking branch 'refs/remotes/origin/80-shot-tuning-mode…
godmar Feb 28, 2026
991dd99
(#80) Make NT4 connection panel collapsible with address display
godmar Feb 28, 2026
38b808f
(#80) fixed hub center coordinates
godmar Feb 28, 2026
b5ca023
(#80) Try adding different slots based on epsilon to constants of sho…
aidnem Feb 28, 2026
af2ab71
Merge remote branch '80-shot-tuning-mode' of https://github.com/team4…
aidnem Feb 28, 2026
9bf5403
(#80) Make coordination layer distance logging use shooter to hub dis…
aidnem Feb 28, 2026
3ae3865
(#80) Even more shooter tuning
aidnem Feb 28, 2026
e5c8e58
(#80) Read shooter/hood setpoints from TunableNumbers, add NT distance
godmar Feb 28, 2026
3ba3b60
Add real vision offsets and shooter to turret offset
aidnem Feb 28, 2026
c4682dd
(#80) Fix shot tuning bugs: camera dropdown, Shot Maps sync, re-export
godmar Feb 28, 2026
a98860a
output opponents inner center point
godmar Feb 28, 2026
5ae6646
(#80) Fix shot map export: save to local file instead of in-memory store
godmar Feb 28, 2026
afecaca
updated state machine dot file
godmar Feb 28, 2026
2a35baf
Merge remote-tracking branch 'refs/remotes/origin/80-shot-tuning-mode…
godmar Feb 28, 2026
2a4bbdb
(#80) Apply robotToShooter offset to pose-based hub distance
godmar Feb 28, 2026
fa5dd1c
(#80) Move NT4 connection panel inline next to page heading
godmar Feb 28, 2026
9471ceb
(#80) Fix Chrome seeking, cross-browser keyboard shortcuts, add ±10 f…
godmar Mar 1, 2026
8d50bf1
(#80) Add flight-window indicator strip below the scrub slider
godmar Mar 1, 2026
1270ac4
(#80) Move FPS selector inline with camera chooser, use dropdown
godmar Mar 1, 2026
8e1c051
(#80) Return focus to container after slider interaction
godmar Mar 1, 2026
8e924cf
(#80) Add s/e keyboard shortcuts for marking start and end timestamps
godmar Mar 1, 2026
c08849f
(#80) Reorder sidebar: Vision first, Shot Maps second
godmar Mar 1, 2026
d186953
(#80) More shop tuning and some optimization
aidnem Mar 1, 2026
4a0a7f8
Merge branch '80-shot-tuning-mode' of https://github.com/team401/2026…
aidnem Mar 1, 2026
7a7cc35
(#85) Reduce memory allocations in drive, subsystems, and coordinatio…
aidnem Mar 1, 2026
28f02e5
Simplify odometry queue copying per PR review
aidnem Mar 2, 2026
0c90c41
Revert Drive.java pre-allocated moduleDeltas per PR review
aidnem Mar 2, 2026
9f0ec5a
Revert Module.java and CoordinationLayer optimizations per PR review
aidnem Mar 2, 2026
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
13 changes: 10 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ deploy {
// getTargetTypeClass is a shortcut to get the class type using a string

frcJava(getArtifactTypeClass('FRCJavaArtifact')) {
// 422 2025 arguments
// gcType = 'Other'
// jvmArgs.add("-XX:GCTimeRatio=5")
// jvmArgs.add("-XX:+UseG1GC")
// jvmArgs.add("-XX:MaxGCPauseMillis=20")

// 401 arguments pre 2-28 shop
jvmArgs.add("-XX:+UnlockExperimentalVMOptions")
jvmArgs.add("-XX:GCTimeRatio=5")
jvmArgs.add("-XX:GCTimeRatio=0")
jvmArgs.add("-XX:+UseSerialGC")
jvmArgs.add("-XX:MaxGCPauseMillis=50")
jvmArgs.add("-XX:MaxGCPauseMillis=20")

// The options below may improve performance, but should only be enabled on the RIO 2
//
Expand Down Expand Up @@ -115,7 +122,7 @@ dependencies {
def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text)
annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version"

def coppercoreVersion = "2026.2.12"
def coppercoreVersion = "2026.2.13"

implementation "io.github.team401.coppercore:controls:$coppercoreVersion"
implementation "io.github.team401.coppercore:geometry:$coppercoreVersion"
Expand Down
314 changes: 314 additions & 0 deletions docs/claude_optimizations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
# Memory Allocation Optimizations# Memory Allocation Optimizations



**Optimized by:** Claude Opus 4.5 (Anthropic)**Optimized by:** Claude 3.5 Sonnet (Anthropic)

**Date:** March 2026**Date:** March 2026

**Goal:** Reduce garbage collection pressure on the roboRIO's constrained 100MB heap**Goal:** Reduce garbage collection pressure on the roboRIO's constrained 100MB heap



## Overview## Overview



These optimizations reduce per-cycle memory allocations to minimize garbage collection (GC) pauses on the roboRIO. On a 2-core, 32-bit ARM processor with limited heap space, frequent allocations lead to:These optimizations reduce per-cycle memory allocations to minimize garbage collection (GC) pauses on the roboRIO. On a 2-core, 32-bit ARM processor with limited heap space, frequent allocations lead to:

- More frequent GC cycles- More frequent GC cycles

- Stop-the-world pauses that can cause loop overruns- Stop-the-world pauses that can cause loop overruns

- Inconsistent loop timing- Inconsistent loop timing



The primary strategies used:The primary strategies used:

1. **Loop-based queue draining** - Replaced stream operations with simple loops1. **Pre-allocated arrays** - Reuse fixed-size arrays instead of creating new ones each cycle

2. **Mutable measures** - Use WPILib's `MutAngle`/`MutAngularVelocity` with `mut_replace()` instead of `Unit.of()`2. **Mutable measures** - Use WPILib's `MutAngle`/`MutAngularVelocity` with `mut_replace()` instead of `Unit.of()`

3. **Primitive math for shot calculations** - Compute rotations and transforms using doubles instead of allocating geometry objects in hot paths3. **Primitive math** - Compute rotations and transforms using doubles instead of allocating geometry objects

4. **Object reuse** - Cache and reuse objects like `EnhancedLine2d` via mutable setters

## Files Modified

## Files Modified

### Drive System

### Drive System

#### `ModuleIOTalonFX.java`

- **Change:** Simplified queue-to-input copying using loops#### `ModuleIOTalonFX.java`

- **Before:** Used `stream().mapToDouble().toArray()` which allocates intermediate objects- **Change:** Simplified queue-to-input copying (no intermediate arrays)

- **After:** Determine sample count from queue sizes, allocate output arrays directly, loop-copy from queues- **Before:** Used `stream().mapToDouble().toArray()` which allocates intermediate objects and a new array each cycle

- **Impact:** Eliminates stream overhead- **After:** Determine sample count from queue sizes, allocate output arrays directly, loop-copy from queues

- **Impact:** Eliminates stream overhead; arrays still allocated but at correct size with no intermediate copies

#### `GyroIOPigeon2.java`

- **Change:** Same pattern as ModuleIOTalonFX#### `GyroIOPigeon2.java`

- **Before:** Stream-based queue draining- **Change:** Same pattern as ModuleIOTalonFX

- **After:** Loop-based draining directly into input arrays- **Before:** Stream-based queue draining

- **Impact:** Eliminates stream overhead for gyro odometry data- **After:** Loop-based draining directly into input arrays

- **Impact:** Eliminates stream overhead for gyro odometry data

#### `ModuleIOSim.java`

- **Change:** Pre-allocated arrays matching real robot pattern#### `Module.java`

- **Impact:** Maintains consistency with real robot code, avoids allocations in sim- **Change:** Pre-allocated odometry positions array + cached logger key string

- **Before:** Created new `SwerveModulePosition[]` each cycle, concatenated logger key string

### Subsystems (Mutable Measures)- **After:** Reuse pre-allocated array, cache the logger key at construction

- **Impact:** Eliminates array allocation + string concatenation per module per cycle

#### `ShooterSubsystem.java`

- **Change:** `MutAngularVelocity cachedVelocity` for `getVelocity()` method#### `ModuleIOSim.java`

- **Before:** `return RadiansPerSecond.of(inputs.velocityRadPerSec)` allocates new Measure- **Change:** Pre-allocated arrays matching real robot pattern

- **After:** `return cachedVelocity.mut_replace(inputs.velocityRadPerSec, RadiansPerSecond)`- **Impact:** Maintains consistency with real robot code, avoids allocations in sim

- **Impact:** Eliminates 1 Measure allocation per cycle

### Subsystems (Mutable Measures)

#### `HopperSubsystem.java`

- **Change:** `MutAngularVelocity cachedVelocity` for `getHopperVelocity()` method#### `ShooterSubsystem.java`

- **Impact:** Eliminates 1 Measure allocation per cycle- **Change:** `MutAngularVelocity cachedVelocity` for `getVelocity()` method

- **Before:** `return RadiansPerSecond.of(inputs.velocityRadPerSec)` allocates new Measure

#### `IndexerSubsystem.java`- **After:** `return cachedVelocity.mut_replace(inputs.velocityRadPerSec, RadiansPerSecond)`

- **Change:** `MutAngularVelocity cachedVelocity` for `getVelocity()` method- **Impact:** Eliminates 1 Measure allocation per cycle

- **Impact:** Eliminates 1 Measure allocation per cycle

#### `HopperSubsystem.java`

#### `TurretSubsystem.java`- **Change:** `MutAngularVelocity cachedVelocity` for `getHopperVelocity()` method

- **Change:** `MutAngle cachedAngle` + `MutAngularVelocity cachedVelocity`- **Impact:** Eliminates 1 Measure allocation per cycle

- **Impact:** Eliminates 2 Measure allocations per cycle

#### `IndexerSubsystem.java`

#### `HoodSubsystem.java`- **Change:** `MutAngularVelocity cachedVelocity` for `getVelocity()` method

- **Change:** `MutAngle cachedAngle` + `MutAngularVelocity cachedVelocity`- **Impact:** Eliminates 1 Measure allocation per cycle

- **Impact:** Eliminates 2 Measure allocations per cycle

#### `TurretSubsystem.java`

#### `IntakeSubsystem.java`- **Change:** `MutAngle cachedAngle` + `MutAngularVelocity cachedVelocity`

- **Change:** `MutAngle cachedPivotAngle` for `getCurrentPivotAngle()` method- **Impact:** Eliminates 2 Measure allocations per cycle

- **Impact:** Eliminates 1 Measure allocation per cycle

#### `HoodSubsystem.java`

### Coordination Layer- **Change:** `MutAngle cachedAngle` + `MutAngularVelocity cachedVelocity`

- **Impact:** Eliminates 2 Measure allocations per cycle

#### `CoordinationLayer.java`

#### `IntakeSubsystem.java`

**`runShotCalculatorWithDrive()` method:**- **Change:** `MutAngle cachedPivotAngle` for `getCurrentPivotAngle()` method

- **Before:**- **Impact:** Eliminates 1 Measure allocation per cycle

- `new Pose3d(robotPose).plus(robotToShooter).getTranslation()` - 2 Pose3d + Translation3d

- `ChassisSpeeds.fromRobotRelativeSpeeds()` - new ChassisSpeeds### Coordination Layer

- `new Translation3d(0, 0, omega)` for omega vector

- `robotToShooterTranslation.rotateBy(new Rotation3d())` - Rotation3d + Translation3d#### `CoordinationLayer.java`

- `omega_vec.cross()` - Vector<N3> allocation

- `new Translation2d()` for shooter velocity**Test mode distance calculation:**

- `ShotCalculations.calculateShotFromMap()` returning new `MapBasedShotInfo` record- **Before:** `new Pose3d().plus().getTranslation().toTranslation2d().getDistance(new Translation2d())`

- **After:**- **After:** Manual math: `Math.sqrt(dx * dx + dy * dy)`

- All calculations done with primitive `double` math- **Impact:** Eliminates Pose3d, Translation3d, Translation2d allocations

- Cross product computed manually: `vRotX = -omega * ry`, `vRotY = omega * rx`

- Uses `calculateShotFromMapInPlace()` with cached `MutableMapBasedShotInfo`**`shouldStowHoodBasedOnMovement()` method:**

- **Impact:** Eliminates ~10+ geometry/math object allocations per cycle- **Before:**

- `new Translation2d().rotateBy()` for field-centric speeds

### Shot Calculations - `new Pose3d().plus().getTranslation().toTranslation2d()` for shooter position

- `new EnhancedLine2d()` each cycle

#### `ShotCalculations.java` - `new Translation2d[]{}` for logging

- **After:**

**New `MutableMapBasedShotInfo` class:** - Manual rotation using `cos`/`sin` primitives

- Mutable alternative to `MapBasedShotInfo` record - Manual transform calculation

- `set()` method to update values in place - Cached `EnhancedLine2d` reused via `set()` method

- Same getter methods as the record for API compatibility - Pre-allocated `cachedShooterTrajectory` array

- **Impact:** Eliminates ~6-8 geometry object allocations per cycle

**New `calculateShotFromMapInPlace()` method:**

- Takes primitive `double` parameters instead of `Translation3d`/`Translation2d`**`runShotCalculatorWithDrive()` method:**

- Stores output in a `MutableMapBasedShotInfo` parameter- **Before:**

- All distance calculations use `Math.sqrt(dx*dx + dy*dy)` instead of `Translation2d.getDistance()` - `new Pose3d(robotPose).plus(robotToShooter).getTranslation()` - 2 Pose3d + Translation3d

- **Impact:** Eliminates record allocation + multiple Translation2d/Translation3d allocations per cycle - `ChassisSpeeds.fromRobotRelativeSpeeds()` - new ChassisSpeeds

- `new Translation3d(0, 0, omega)` for omega vector

## Estimated Impact - `robotToShooterTranslation.rotateBy(new Rotation3d())` - Rotation3d + Translation3d

- `omega_vec.cross()` - Vector<N3> allocation

### Per-Cycle Allocation Reduction - `new Translation2d()` for shooter velocity

- `ShotCalculations.calculateShotFromMap()` returning new `MapBasedShotInfo` record

| Area | Allocations Eliminated |- **After:**

|------|----------------------| - All calculations done with primitive `double` math

| Drive odometry (gyro + modules) | Stream overhead eliminated | - Cross product computed manually: `vRotX = -omega * ry`, `vRotY = omega * rx`

| Subsystem measures | ~8 Measure objects | - Uses `calculateShotFromMapInPlace()` with cached `MutableMapBasedShotInfo`

| Shot calculations | ~10 geometry objects + 1 record |- **Impact:** Eliminates ~10+ geometry/math object allocations per cycle

| **Total** | **~20+ object allocations per cycle** |

### Shot Calculations

### Expected Results

#### `ShotCalculations.java`

- **Real robot:** Should see improvement due to:

- Smaller heap (100MB vs multi-GB)**New `MutableMapBasedShotInfo` class:**

- Less powerful GC- Mutable alternative to `MapBasedShotInfo` record

- No spare CPU cores to hide GC latency- `set()` method to update values in place

- Same getter methods as the record for API compatibility

## Notes

**New `calculateShotFromMapInPlace()` method:**

### What Was NOT Changed- Takes primitive `double` parameters instead of `Translation3d`/`Translation2d`

- Stores output in a `MutableMapBasedShotInfo` parameter

- **Optional usage:** `Optional.ifPresent()` has negligible overhead - the Optional objects are stored as fields (created once), and `ifPresent()` is just a null check- All distance calculations use `Math.sqrt(dx*dx + dy*dy)` instead of `Translation2d.getDistance()`

- **Logging allocations:** Some allocations remain for `Logger.recordOutput()` calls - these can be disabled in competition if needed- **Impact:** Eliminates record allocation + multiple Translation2d/Translation3d allocations per cycle

- **WPILib geometry classes:** `Rotation2d`, `Pose2d`, `SwerveModulePosition` are immutable by design - we use them where code clarity is more important than micro-optimization

### Utilities

### Tradeoffs

#### `EnhancedLine2d.java`

- **Mutable measures:** Callers must not store references to returned measures, as values will change. This is documented in method Javadocs.- **Change:** Made fields non-final, added `set(Translation2d start, Translation2d end)` method

- **Code complexity:** Primitive math is less readable than geometry class operations, but comments explain the equivalent operations.- **Before:** Immutable, required `new EnhancedLine2d()` each time endpoints changed

- **After:** Can reuse same instance by calling `set()`

## Testing- **Impact:** Eliminates EnhancedLine2d allocation in `shouldStowHoodBasedOnMovement()`



All changes verified to:## Estimated Impact

1. Compile successfully with `./gradlew build`

2. Pass existing tests### Per-Cycle Allocation Reduction

3. Run correctly in simulation

| Area | Allocations Eliminated |

Real robot testing pending.|------|----------------------|

| Drive odometry (4 modules + gyro) | ~15-20 arrays/objects |
| Subsystem measures | ~8 Measure objects |
| Coordination layer | ~15-20 geometry objects |
| Shot calculations | ~10 geometry objects + 1 record |
| **Total** | **~50+ object allocations per cycle** |

### Expected Results

- **Simulation:** Reduced allocation rate by ~200-500 KB/s
- **Real robot:** Should see proportionally larger improvement due to:
- Smaller heap (100MB vs multi-GB)
- Less powerful GC
- No spare CPU cores to hide GC latency

## Notes

### What Was NOT Changed

- **Optional usage:** `Optional.ifPresent()` has negligible overhead - the Optional objects are stored as fields (created once), and `ifPresent()` is just a null check
- **Logging allocations:** Some `Translation3d` allocations remain for `Logger.recordOutput()` calls - these can be disabled in competition if needed
- **WPILib geometry classes:** `Rotation2d`, `Pose2d`, `SwerveModulePosition` are immutable by design - we work around this where critical

### Tradeoffs

- **Mutable measures:** Callers must not store references to returned measures, as values will change. This is documented in method Javadocs.
- **Code complexity:** Primitive math is less readable than geometry class operations, but comments explain the equivalent operations.
- **EnhancedLine2d mutability:** Changed from immutable to mutable pattern - callers should be aware the object can change.

## Testing

All changes verified to:
1. Compile successfully with `./gradlew build`
2. Pass existing tests
3. Run correctly in simulation

Real robot testing pending (robot access unavailable at time of optimization).
Loading