Problem
ShotCalculator.calculateShot() is called every robot cycle (50Hz) and allocates ~38 heap objects per invocation. While these are short-lived Eden objects that G1 handles efficiently, they violate zero-allocation discipline on the hot path and contribute to unnecessary young GC pressure on the roboRIO's 100MB fixed heap.
This is the single largest remaining source of per-cycle allocations in the codebase, producing ~1,900 geometry objects/second from this single method alone.
Allocation inventory
| Allocation |
Line(s) |
Count/Cycle |
Type |
robotPosition.plus(robotToShooter) — Pose2d.transformBy internally allocates 1 Pose2d + 2 Translation2d + 1 Rotation2d |
138 |
4 |
Math |
new Translation2d(vx, vy).rotateBy(heading) — explicit new + rotateBy returns a new Translation2d |
150-152 |
2 |
Math |
new Translation2d(...) + .plus() in 10-iteration convergence loop — each iteration allocates 2 Translation2d |
183-188 |
20 |
Math |
.plus(new Translation2d(...)) for robot center lookahead |
201-207 |
2 |
Math |
target.minus(robotCenterLookahead).getAngle() — .minus() returns a Translation2d, .getAngle() returns a Rotation2d |
208 |
2 |
Math |
angleToTarget.minus(robotToShooter.getRotation()) — returns a new Rotation2d |
209 |
1 |
Math |
new Translation2d(...) for velocity offset (logging only) |
222-223 |
1 |
Logging |
target.minus(velocityOffset) (logging only) |
224 |
1 |
Logging |
new Pose2d(...) x4 for Logger.recordOutput |
225-233 |
4 |
Logging |
new ShootingParams(...) |
238-239 |
1 |
Output |
| Total |
|
38 |
31 math / 6 logging / 1 output |
Root cause
WPILib geometry classes (Translation2d, Rotation2d, Pose2d) are immutable — every arithmetic operation (plus, minus, rotateBy) returns a new object. Additionally, Pose2d.plus(Transform2d) calls transformBy internally, which chains multiple geometry operations producing 4 hidden allocations from a single call. This is safe and idiomatic Java, but incompatible with zero-allocation hot paths.
Fix
Three categories of allocations need different strategies:
Math temporaries (~31 objects/cycle)
The geometry operations — especially the 10-iteration convergence loop which alone accounts for 20 of the 38 allocations — are all straightforward 2D vector math (add, subtract, rotate, distance). These can be replaced entirely with scalar double fields and inline arithmetic (cos, sin, sqrt, atan2). Pre-allocate scratch doubles as class fields and compute in-place. The Pose2d.plus(Transform2d) on line 138 can similarly be decomposed into its underlying scalar operations.
Logging Pose2d objects (~6 objects/cycle)
Logger.recordOutput(String, Pose2d) requires a Pose2d, and Pose2d is immutable. Logging raw doubles would achieve zero-alloc but loses AdvantageScope field widget visualization, which is more valuable than the allocation savings. Note that the velocityOffset Translation2d (line 222-223) and virtualTarget (line 224) are only used for logging — they aren't part of the shot computation. Consider how often these values actually change meaningfully and whether that can be exploited to avoid recreating objects every cycle.
Output record (~1 object/cycle)
ShootingParams is already cached and cleared once per cycle. Consider whether mutable cached fields could replace the record allocation, or accept this single allocation as the method's return contract.
Scope
In scope
- Eliminate all
new Translation2d() / Rotation2d / Pose2d math temporaries in calculateShot()
- Decompose
robotPosition.plus(robotToShooter) into scalar math to eliminate 4 hidden allocations
- Replace with scalar
double fields and inline math
- Reduce logging
Pose2d allocations without sacrificing AdvantageScope visualization
- Maintain identical mathematical behavior — this is a refactor, not a logic change
Out of scope
- Changing the public
ShootingParams API (can be a follow-up)
- Modifying WPILib geometry classes upstream
- Allocations inside
InterpolatingDoubleTreeMap.get() (vendor code)
Validation
- Unit test the refactored
calculateShot() against the current implementation with known inputs to verify identical outputs
- Verify AdvantageScope field widget logging still renders correctly
Affected Files
src/main/java/frc/robot/subsystems/Shooter/ShotCalculator.java
Related Issues
Problem
ShotCalculator.calculateShot()is called every robot cycle (50Hz) and allocates ~38 heap objects per invocation. While these are short-lived Eden objects that G1 handles efficiently, they violate zero-allocation discipline on the hot path and contribute to unnecessary young GC pressure on the roboRIO's 100MB fixed heap.This is the single largest remaining source of per-cycle allocations in the codebase, producing ~1,900 geometry objects/second from this single method alone.
Allocation inventory
robotPosition.plus(robotToShooter)—Pose2d.transformByinternally allocates 1 Pose2d + 2 Translation2d + 1 Rotation2dnew Translation2d(vx, vy).rotateBy(heading)— explicitnew+rotateByreturns a new Translation2dnew Translation2d(...)+.plus()in 10-iteration convergence loop — each iteration allocates 2 Translation2d.plus(new Translation2d(...))for robot center lookaheadtarget.minus(robotCenterLookahead).getAngle()—.minus()returns a Translation2d,.getAngle()returns a Rotation2dangleToTarget.minus(robotToShooter.getRotation())— returns a new Rotation2dnew Translation2d(...)for velocity offset (logging only)target.minus(velocityOffset)(logging only)new Pose2d(...)x4 for Logger.recordOutputnew ShootingParams(...)Root cause
WPILib geometry classes (
Translation2d,Rotation2d,Pose2d) are immutable — every arithmetic operation (plus,minus,rotateBy) returns a new object. Additionally,Pose2d.plus(Transform2d)callstransformByinternally, which chains multiple geometry operations producing 4 hidden allocations from a single call. This is safe and idiomatic Java, but incompatible with zero-allocation hot paths.Fix
Three categories of allocations need different strategies:
Math temporaries (~31 objects/cycle)
The geometry operations — especially the 10-iteration convergence loop which alone accounts for 20 of the 38 allocations — are all straightforward 2D vector math (add, subtract, rotate, distance). These can be replaced entirely with scalar
doublefields and inline arithmetic (cos,sin,sqrt,atan2). Pre-allocate scratch doubles as class fields and compute in-place. ThePose2d.plus(Transform2d)on line 138 can similarly be decomposed into its underlying scalar operations.Logging Pose2d objects (~6 objects/cycle)
Logger.recordOutput(String, Pose2d)requires aPose2d, andPose2dis immutable. Logging raw doubles would achieve zero-alloc but loses AdvantageScope field widget visualization, which is more valuable than the allocation savings. Note that thevelocityOffsetTranslation2d (line 222-223) andvirtualTarget(line 224) are only used for logging — they aren't part of the shot computation. Consider how often these values actually change meaningfully and whether that can be exploited to avoid recreating objects every cycle.Output record (~1 object/cycle)
ShootingParamsis already cached and cleared once per cycle. Consider whether mutable cached fields could replace the record allocation, or accept this single allocation as the method's return contract.Scope
In scope
new Translation2d()/Rotation2d/Pose2dmath temporaries incalculateShot()robotPosition.plus(robotToShooter)into scalar math to eliminate 4 hidden allocationsdoublefields and inline mathPose2dallocations without sacrificing AdvantageScope visualizationOut of scope
ShootingParamsAPI (can be a follow-up)InterpolatingDoubleTreeMap.get()(vendor code)Validation
calculateShot()against the current implementation with known inputs to verify identical outputsAffected Files
src/main/java/frc/robot/subsystems/Shooter/ShotCalculator.javaRelated Issues