Skip to content

Commit a1e08a0

Browse files
authored
Cache WarningActions (#1226)
1 parent 857ddc4 commit a1e08a0

4 files changed

Lines changed: 87 additions & 3 deletions

File tree

src/main/java/io/jenkins/plugins/pipelinegraphview/analysis/StatusAndTiming.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3232
import hudson.model.Action;
3333
import hudson.model.Result;
34+
import io.jenkins.plugins.pipelinegraphview.livestate.LiveGraphRegistry;
35+
import io.jenkins.plugins.pipelinegraphview.livestate.WarningActionCache;
3436
import java.util.ArrayList;
3537
import java.util.Collection;
3638
import java.util.Collections;
@@ -349,7 +351,18 @@ public static GenericStatus computeChunkStatus2(
349351
if (DISABLE_WARNING_ACTION_LOOKUP) {
350352
return null;
351353
}
352-
// TODO: Cache the result?
354+
// Only memoise closed blocks: once a BlockEndNode exists, the set of inner nodes is
355+
// fixed and WarningActions on them don't change. For open-ended chunks (start == end,
356+
// or end isn't a block end) the scan is either trivial or the result may still change.
357+
boolean cacheable = start != end && end instanceof BlockEndNode<?>;
358+
WarningActionCache cache = cacheable ? LiveGraphRegistry.get().warningActionCache(start.getExecution()) : null;
359+
if (cache != null) {
360+
return cache.getOrCompute(start.getId(), end.getId(), () -> scanForWarning(start, end));
361+
}
362+
return scanForWarning(start, end);
363+
}
364+
365+
private static @CheckForNull WarningAction scanForWarning(@NonNull FlowNode start, @NonNull FlowNode end) {
353366
DepthFirstScanner scanner = new DepthFirstScanner();
354367
if (!scanner.setup(end, Collections.singletonList(start))) {
355368
return null;

src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.github.benmanes.caffeine.cache.Cache;
44
import com.github.benmanes.caffeine.cache.Caffeine;
5+
import edu.umd.cs.findbugs.annotations.CheckForNull;
56
import io.jenkins.plugins.pipelinegraphview.utils.PipelineGraph;
67
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList;
78
import java.time.Duration;
@@ -140,7 +141,7 @@ public void cacheAllSteps(WorkflowRun run, long version, PipelineStepList steps)
140141
* Returns a per-run monitor that callers can synchronise on to dedup concurrent graph
141142
* rebuilds. Null when the live state isn't present (caller just computes directly).
142143
*/
143-
@edu.umd.cs.findbugs.annotations.CheckForNull
144+
@CheckForNull
144145
public Object graphComputeLock(WorkflowRun run) {
145146
if (disabled()) {
146147
return null;
@@ -150,7 +151,7 @@ public Object graphComputeLock(WorkflowRun run) {
150151
}
151152

152153
/** See {@link #graphComputeLock(WorkflowRun)} — the matching lock for the steps path. */
153-
@edu.umd.cs.findbugs.annotations.CheckForNull
154+
@CheckForNull
154155
public Object allStepsComputeLock(WorkflowRun run) {
155156
if (disabled()) {
156157
return null;
@@ -159,6 +160,23 @@ public Object allStepsComputeLock(WorkflowRun run) {
159160
return state == null ? null : state.allStepsComputeLock();
160161
}
161162

163+
/**
164+
* Returns the {@link WarningActionCache} for this execution, or {@code null} when the
165+
* live state isn't present. Callers fall back to uncached scans on null.
166+
*/
167+
@CheckForNull
168+
public WarningActionCache warningActionCache(FlowExecution execution) {
169+
if (disabled()) {
170+
return null;
171+
}
172+
String key = keyFor(execution);
173+
if (key == null) {
174+
return null;
175+
}
176+
LiveGraphState state = states.getIfPresent(key);
177+
return state == null ? null : state.warningActionCache();
178+
}
179+
162180
private static String keyFor(FlowExecution execution) {
163181
try {
164182
Object exec = execution.getOwner().getExecutable();

src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ final class LiveGraphState {
4646
private VersionedCache<PipelineGraph> cachedGraph;
4747
private VersionedCache<PipelineStepList> cachedAllSteps;
4848

49+
private final WarningActionCache warningActionCache = new WarningActionCache();
50+
4951
// Serialise concurrent rebuilds so N HTTP readers don't each do the same O(nodes) work.
5052
// Separate locks for tree vs steps — a slow tree rebuild must not starve steps.
5153
private final Object graphComputeLock = new Object();
@@ -172,5 +174,9 @@ Object allStepsComputeLock() {
172174
return allStepsComputeLock;
173175
}
174176

177+
WarningActionCache warningActionCache() {
178+
return warningActionCache;
179+
}
180+
175181
private record VersionedCache<T>(long version, T value) {}
176182
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package io.jenkins.plugins.pipelinegraphview.livestate;
2+
3+
import edu.umd.cs.findbugs.annotations.CheckForNull;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import java.util.Map;
6+
import java.util.Optional;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.function.Supplier;
9+
import org.jenkinsci.plugins.workflow.actions.WarningAction;
10+
11+
/**
12+
* Per-run cache of {@link WarningAction} lookups between two flow nodes. The underlying scan
13+
* in {@code StatusAndTiming.findWorstWarningBetween} walks every node between a stage's start
14+
* and end, which dominates {@code /allSteps} on large completed runs. WarningActions don't
15+
* change after attach, so once both endpoints exist (and for completed blocks, the set of
16+
* inner nodes is fixed) the result is stable and can be memoised.
17+
*/
18+
public final class WarningActionCache {
19+
20+
// ConcurrentHashMap disallows null values, so we wrap each result in Optional — the
21+
// Optional is never itself null, it's either present (a WarningAction) or empty
22+
// (cached: no warning on this range). Map.get returning null means "not yet cached".
23+
private final Map<String, Optional<WarningAction>> byRange = new ConcurrentHashMap<>();
24+
25+
WarningActionCache() {}
26+
27+
/**
28+
* Returns the cached result for {@code (startId, endId)}, or invokes {@code computer} to
29+
* resolve and caches the result before returning.
30+
*/
31+
@CheckForNull
32+
public WarningAction getOrCompute(
33+
@NonNull String startId, @NonNull String endId, @NonNull Supplier<WarningAction> computer) {
34+
String k = key(startId, endId);
35+
Optional<WarningAction> cached = byRange.get(k);
36+
if (cached != null) {
37+
return cached.orElse(null);
38+
}
39+
WarningAction computed = computer.get();
40+
byRange.put(k, Optional.ofNullable(computed));
41+
return computed;
42+
}
43+
44+
private static String key(String startId, String endId) {
45+
return startId + ':' + endId;
46+
}
47+
}

0 commit comments

Comments
 (0)