Skip to content

Eliminate heap allocations in ShotCalculator.calculateShot() hot path #449

Description

@JRTaylord

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

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
In Review

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions