From 07fd892e0f2d7459e4c0a085ee56cfd3bf6dc083 Mon Sep 17 00:00:00 2001 From: Brad Clement Date: Wed, 1 Jul 2026 19:16:34 -0700 Subject: [PATCH] Reuse EngineQuerier to reduce allocation pressure --- .../driver/engine/SimulationEngine.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index bc0e6ef1f0..a78e61fb4b 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -99,6 +99,8 @@ public final class SimulationEngine implements AutoCloseable { private final LiveCells cells; private Duration elapsedTime; + private final EngineQuerier reusableQuerier = new EngineQuerier(); + public SimulationEngine(LiveCells initialCells) { timeline = new TemporalEventSource(); referenceTimeline = new TemporalEventSource(); @@ -523,15 +525,15 @@ public void updateCondition( final Duration horizonTime ) { if (this.closed) throw new IllegalStateException("Cannot update condition on closed simulation engine"); - final var querier = new EngineQuerier(frame); + reusableQuerier.reset(frame); final var prediction = this.conditions .get(condition) - .nextSatisfied(querier, horizonTime.minus(currentTime)) + .nextSatisfied(reusableQuerier, horizonTime.minus(currentTime)) .map(currentTime::plus); - this.waitingConditions.subscribeQuery(condition, querier.referencedTopics); + this.waitingConditions.subscribeQuery(condition, reusableQuerier.referencedTopics); - final var expiry = querier.expiry.map(currentTime::plus); + final var expiry = reusableQuerier.expiry.map(currentTime::plus); if (prediction.isPresent() && (expiry.isEmpty() || prediction.get().shorterThan(expiry.get()))) { this.scheduledJobs.schedule(JobId.forSignal(condition), SubInstant.Tasks.at(prediction.get())); } else { @@ -548,16 +550,16 @@ public void updateResource( final Duration currentTime, final ResourceUpdates resourceUpdates) { if (this.closed) throw new IllegalStateException("Cannot update resource on closed simulation engine"); - final var querier = new EngineQuerier(frame); + reusableQuerier.reset(frame); resourceUpdates.add(new ResourceUpdates.ResourceUpdate<>( - querier, + reusableQuerier, currentTime, resourceId, this.resources.get(resourceId))); - this.waitingResources.subscribeQuery(resourceId, querier.referencedTopics); + this.waitingResources.subscribeQuery(resourceId, reusableQuerier.referencedTopics); - final var expiry = querier.expiry.map(currentTime::plus); + final var expiry = reusableQuerier.expiry.map(currentTime::plus); if (expiry.isPresent()) { this.scheduledJobs.schedule(JobId.forResource(resourceId), SubInstant.Resources.at(expiry.get())); } @@ -1160,15 +1162,19 @@ private static Optional trySerializeEvent( /** A handle for processing requests from a modeled resource or condition. */ private static final class EngineQuerier implements Querier { - private final TaskFrame frame; - private final Set> referencedTopics = new HashSet<>(); + private TaskFrame frame; + private Set> referencedTopics = new HashSet<>(); private Optional expiry = Optional.empty(); // Cache: query -> state. Safe because resources/conditions never emit events during getDynamics, // so cell state cannot change between repeated reads within one evaluation. private final HashMap, Object> stateCache = new HashMap<>(32); - public EngineQuerier(final TaskFrame frame) { + void reset(final TaskFrame frame) { this.frame = Objects.requireNonNull(frame); + // subscribeQuery takes ownership of the previous set, so allocate a new one + this.referencedTopics = new HashSet<>(); + this.expiry = Optional.empty(); + this.stateCache.clear(); } @Override