Skip to content

Commit 31e6b93

Browse files
authored
Add support for timing steps and pipelines (#613)
* Add support for timing steps and pipelines * Suppress PMD warnings * Add documentation to StepStarted and StepFinishedEvents * Add analysis of step timings * Updated UI test * Add benchmarking to analysis window * Undo accidental formatting * Fix and suppress issues from Codacy * Add tests for Timer * Improve UI and increase tolerance for timer tests * Increase timer test tolerance to 5ms (5%) * Add tests for BenchmarkRunner * Change analysis averager to a moving average, improve naming and documentation Should fix the StackOverflowErrors @AustinShalit is running into on OS X * Fix flickering, make Analysis immutable * Reset timers after running a benchmark. Fix an occasional NPE Increase default number of runs to 100 (was 5) * Add tests for MovingAverage * Update TimerTest for reset() * Fix tests (oops) * Freeze sources while benchmarking Remove useless suppressions * Improve Timer API, documentation, and tests * Fix accidental formatting (again) * Remove redundant (ms) * Fix PMD issues * Fix PMD in UI * Appease Codacy * Don't make analysis window block main window. Disable inputs and outputs while benchmarking. * Remove hacky data aggregation from Analysis class. * Replace custom FixedSizeQueue with guava's EvictingQueue * Add option to export results to CSV. Fixed an issue with steps not being in their proper order in the analysis * Fix tests * Add tests for metrics * Move CSV to it's own class * Fix some checkstyle stuff I missed * Rename Timer.stopped() to stop() * Delete redudant Analysis class and improve variable names in Statistics * Improve Javadoc for statistics hotness function * Fix import order * Move analysis window to fx:include
1 parent 765dd48 commit 31e6b93

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1772
-117
lines changed

core/src/main/java/edu/wpi/grip/core/GripCoreModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package edu.wpi.grip.core;
22

33
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
4+
import edu.wpi.grip.core.metrics.BenchmarkRunner;
5+
import edu.wpi.grip.core.metrics.Timer;
46
import edu.wpi.grip.core.serialization.Project;
57
import edu.wpi.grip.core.settings.SettingsProvider;
68
import edu.wpi.grip.core.sockets.InputSocket;
@@ -128,6 +130,7 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
128130
install(new FactoryModuleBuilder().build(new TypeLiteral<Connection.Factory<Object>>() {
129131
}));
130132

133+
bind(StepIndexer.class).to(Pipeline.class);
131134
bind(ConnectionValidator.class).to(Pipeline.class);
132135
bind(Source.SourceFactory.class).to(Source.SourceFactoryImpl.class);
133136

@@ -150,6 +153,9 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
150153
.build(NetworkTableEntrySource.Factory.class));
151154

152155
install(new FactoryModuleBuilder().build(ExceptionWitness.Factory.class));
156+
install(new FactoryModuleBuilder().build(Timer.Factory.class));
157+
158+
bind(BenchmarkRunner.class).asEagerSingleton();
153159
}
154160

155161
protected void onSubscriberException(Throwable exception, @Nullable SubscriberExceptionContext

core/src/main/java/edu/wpi/grip/core/Pipeline.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Collections;
2727
import java.util.HashSet;
2828
import java.util.List;
29+
import java.util.NoSuchElementException;
2930
import java.util.Set;
3031
import java.util.concurrent.locks.Lock;
3132
import java.util.concurrent.locks.ReadWriteLock;
@@ -47,7 +48,7 @@
4748
*/
4849
@Singleton
4950
@XStreamAlias(value = "grip:Pipeline")
50-
public class Pipeline implements ConnectionValidator, SettingsProvider {
51+
public class Pipeline implements ConnectionValidator, SettingsProvider, StepIndexer {
5152

5253
private final transient ReadWriteLock sourceLock = new ReentrantReadWriteLock();
5354

@@ -359,7 +360,18 @@ public synchronized void moveStep(Step step, int delta) {
359360

360361
// Do not lock while posting the event
361362
eventBus.post(new StepMovedEvent(step, delta));
363+
}
362364

365+
@Override
366+
public int indexOf(Step step) {
367+
checkNotNull(step, "step");
368+
return readStepsSafely(steps -> {
369+
int index = steps.indexOf(step);
370+
if (index == -1) {
371+
throw new NoSuchElementException("Step " + step + " is not in the pipeline");
372+
}
373+
return index;
374+
});
363375
}
364376

365377
@Subscribe

core/src/main/java/edu/wpi/grip/core/PipelineRunner.java

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package edu.wpi.grip.core;
22

33

4+
import edu.wpi.grip.core.events.BenchmarkEvent;
45
import edu.wpi.grip.core.events.RenderEvent;
56
import edu.wpi.grip.core.events.RunPipelineEvent;
67
import edu.wpi.grip.core.events.RunStartedEvent;
78
import edu.wpi.grip.core.events.RunStoppedEvent;
89
import edu.wpi.grip.core.events.StopPipelineEvent;
10+
import edu.wpi.grip.core.metrics.Timer;
911
import edu.wpi.grip.core.util.SinglePermitSemaphore;
1012
import edu.wpi.grip.core.util.service.AutoRestartingService;
1113
import edu.wpi.grip.core.util.service.LoggingListener;
@@ -24,6 +26,7 @@
2426
import java.util.concurrent.Executor;
2527
import java.util.concurrent.TimeUnit;
2628
import java.util.concurrent.TimeoutException;
29+
import java.util.concurrent.atomic.AtomicBoolean;
2730
import java.util.function.Supplier;
2831
import java.util.logging.Logger;
2932

@@ -47,17 +50,25 @@ public class PipelineRunner implements RestartableService {
4750
private final Supplier<ImmutableList<Step>> stepSupplier;
4851
private final AutoRestartingService pipelineService;
4952

53+
private final AtomicBoolean benchmarking = new AtomicBoolean(false);
5054

5155
@Inject
52-
PipelineRunner(EventBus eventBus, Provider<Pipeline> pipelineProvider) {
53-
this(eventBus, () -> pipelineProvider.get().getSources(), () -> pipelineProvider.get()
54-
.getSteps());
55-
}
56-
57-
PipelineRunner(EventBus eventBus, Supplier<ImmutableList<Source>> sourceSupplier,
58-
Supplier<ImmutableList<Step>> stepSupplier) {
56+
PipelineRunner(EventBus eventBus,
57+
Provider<Pipeline> pipelineProvider,
58+
Timer.Factory timerFactory) {
59+
this(eventBus,
60+
() -> pipelineProvider.get().getSources(),
61+
() -> pipelineProvider.get().getSteps(),
62+
timerFactory);
63+
}
64+
65+
PipelineRunner(EventBus eventBus,
66+
Supplier<ImmutableList<Source>> sourceSupplier,
67+
Supplier<ImmutableList<Step>> stepSupplier,
68+
Timer.Factory timerFactory) {
5969
this.sourceSupplier = sourceSupplier;
6070
this.stepSupplier = stepSupplier;
71+
Timer timer = timerFactory.create(this);
6172
this.pipelineService = new AutoRestartingService<>(
6273
() -> new AbstractScheduledService() {
6374

@@ -77,12 +88,12 @@ protected void runOneIteration() throws InterruptedException {
7788
if (!super.isRunning()) {
7889
return;
7990
}
80-
runPipeline(super::isRunning);
91+
timer.time(() -> runPipeline(super::isRunning));
8192
// This should not block access to the steps array
93+
eventBus.post(new RunStoppedEvent());
8294
if (super.isRunning()) {
8395
eventBus.post(new RenderEvent());
8496
}
85-
eventBus.post(new RunStoppedEvent());
8697
}
8798

8899
@Override
@@ -182,20 +193,23 @@ private void runPipeline(Supplier<Boolean> isRunning) {
182193
final ImmutableList<Step> steps = stepSupplier.get();
183194
// Now that we have a snapshot we can run the pipeline with our copy.
184195

185-
for (Source source : sources) {
186-
// if we have been stopped then we need to exit as soon as possible.
187-
// then don't continue to run the pipeline.
188-
if (!isRunning.get()) {
189-
break;
196+
if (!benchmarking.get()) {
197+
// Don't update sources if this run is being benchmarked
198+
for (Source source : sources) {
199+
// if we have been stopped then we need to exit as soon as possible.
200+
// then don't continue to run the pipeline.
201+
if (!isRunning.get()) {
202+
break;
203+
}
204+
source.updateOutputSockets();
190205
}
191-
source.updateOutputSockets();
192206
}
193207

194208
for (Step step : steps) {
195209
if (!isRunning.get()) {
196210
break;
197211
}
198-
step.runPerformIfPossible();
212+
step.runPerform(benchmarking.get());
199213
}
200214
}
201215

@@ -213,4 +227,9 @@ public void onStopPipeline(@Nullable StopPipelineEvent event) {
213227
stopAsync();
214228
}
215229

230+
@Subscribe
231+
public void onBenchmarkEvent(BenchmarkEvent event) {
232+
benchmarking.set(event.isStart());
233+
}
234+
216235
}

core/src/main/java/edu/wpi/grip/core/Step.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package edu.wpi.grip.core;
22

3+
import edu.wpi.grip.core.metrics.Timer;
34
import edu.wpi.grip.core.sockets.InputSocket;
45
import edu.wpi.grip.core.sockets.OutputSocket;
56
import edu.wpi.grip.core.sockets.Socket;
@@ -27,6 +28,7 @@ public class Step {
2728
private static final String MISSING_SOCKET_MESSAGE_END = " must have a value to run this step.";
2829

2930
private final ExceptionWitness witness;
31+
private final Timer timer;
3032

3133
private final Operation operation;
3234
private final OperationDescription description;
@@ -41,17 +43,20 @@ public class Step {
4143
* @param inputSockets The input sockets from the operation.
4244
* @param outputSockets The output sockets provided by the operation.
4345
* @param exceptionWitnessFactory A factory used to create an {@link ExceptionWitness}.
46+
* @param timerFactory A factory used to create a {@link Timer}.
4447
*/
4548
Step(Operation operation,
4649
OperationDescription description,
4750
List<InputSocket> inputSockets,
4851
List<OutputSocket> outputSockets,
49-
ExceptionWitness.Factory exceptionWitnessFactory) {
52+
ExceptionWitness.Factory exceptionWitnessFactory,
53+
Timer.Factory timerFactory) {
5054
this.operation = operation;
5155
this.description = description;
5256
this.inputSockets = inputSockets;
5357
this.outputSockets = outputSockets;
5458
this.witness = exceptionWitnessFactory.create(this);
59+
this.timer = timerFactory.create(this);
5560
}
5661

5762
/**
@@ -88,9 +93,23 @@ private void resetOutputSockets() {
8893
/**
8994
* The {@link Operation#perform} method should only be called if all {@link
9095
* InputSocket#getValue()} are not empty. If one input is invalid then the perform method will not
91-
* run and all output sockets will be assigned to their default values.
96+
* run and all output sockets will be assigned to their default values. If no input sockets have
97+
* changed values, the perform method will not run.
9298
*/
9399
protected final void runPerformIfPossible() {
100+
runPerform(false);
101+
}
102+
103+
104+
/**
105+
* The {@link Operation#perform} method should only be called if all {@link
106+
* InputSocket#getValue()} are not empty. If one input is invalid then the perform method will not
107+
* run and all output sockets will be assigned to their default values.
108+
*
109+
* @param force if this step should be forced to run. If {@code true}, the operation's perform
110+
* method will be called if every input is valid regardless of 'dirtiness'.
111+
*/
112+
protected final void runPerform(boolean force) {
94113
boolean anyDirty = false; // Keeps track of if there are sockets that are dirty
95114

96115
for (InputSocket<?> inputSocket : inputSockets) {
@@ -104,26 +123,26 @@ protected final void runPerformIfPossible() {
104123
}
105124
// If one value is true then this will stay true
106125
anyDirty |= inputSocket.dirtied();
107-
108126
}
109-
if (!anyDirty) { // If there aren't any dirty inputs Don't clear the exceptions just return
127+
if (!force && !anyDirty) {
128+
// If there aren't any dirty inputs don't clear the exceptions, just return
110129
return;
111130
}
112131

113132
try {
114133
// We need to ensure that if perform disabled is switching states that we don't run the
115-
// perform method
116-
// while that is happening.
134+
// perform method while that is happening.
117135
synchronized (removedLock) {
118136
if (!removed) {
119-
this.operation.perform();
137+
timer.time(this.operation::perform);
120138
}
121139
}
122140
} catch (RuntimeException e) {
123141
// We do not want to catch all exceptions, only runtime exceptions.
124142
// This is especially important when it comes to InterruptedExceptions
125-
final String operationFailedMessage = String.format("The %s operation did not perform "
126-
+ "correctly.", getOperationDescription().name());
143+
final String operationFailedMessage =
144+
String.format("The %s operation did not perform correctly.",
145+
getOperationDescription().name());
127146
logger.log(Level.WARNING, operationFailedMessage, e);
128147
witness.flagException(e, operationFailedMessage);
129148
resetOutputSockets();
@@ -158,10 +177,13 @@ protected boolean removed() {
158177
@Singleton
159178
public static class Factory {
160179
private final ExceptionWitness.Factory exceptionWitnessFactory;
180+
private final Timer.Factory timerFactory;
161181

162182
@Inject
163-
public Factory(ExceptionWitness.Factory exceptionWitnessFactory) {
183+
public Factory(ExceptionWitness.Factory exceptionWitnessFactory,
184+
Timer.Factory timerFactory) {
164185
this.exceptionWitnessFactory = exceptionWitnessFactory;
186+
this.timerFactory = timerFactory;
165187
}
166188

167189
/**
@@ -180,7 +202,8 @@ public Step create(OperationMetaData operationData) {
180202
operationData.getDescription(),
181203
inputSockets,
182204
outputSockets,
183-
exceptionWitnessFactory
205+
exceptionWitnessFactory,
206+
timerFactory
184207
);
185208

186209
for (Socket<?> socket : inputSockets) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package edu.wpi.grip.core;
2+
3+
import java.util.Comparator;
4+
5+
/**
6+
* An interface for getting the indices of steps.
7+
*/
8+
public interface StepIndexer extends Comparator<Step> {
9+
10+
/**
11+
* Gets the index of the given step.
12+
*
13+
* @param step the step to get the index of
14+
* @return the index of the given step, or -1 if this object does not contain that step
15+
*/
16+
int indexOf(Step step);
17+
18+
/**
19+
* Compares two steps based on their indexes. <i>This is not consistent with {@code equals()}</i>.
20+
*
21+
* @param o1 the first step to compare
22+
* @param o2 the second step to compare
23+
*/
24+
@Override
25+
default int compare(Step o1, Step o2) {
26+
return indexOf(o1) - indexOf(o2);
27+
}
28+
29+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package edu.wpi.grip.core.events;
2+
3+
/**
4+
* An event posted before and after a pipeline is benchmarked.
5+
*/
6+
public final class BenchmarkEvent {
7+
8+
private final boolean isStart;
9+
10+
private BenchmarkEvent(boolean isStart) {
11+
this.isStart = isStart;
12+
}
13+
14+
public static BenchmarkEvent started() {
15+
return new BenchmarkEvent(true);
16+
}
17+
18+
public static BenchmarkEvent finished() {
19+
return new BenchmarkEvent(false);
20+
}
21+
22+
public boolean isStart() {
23+
return isStart;
24+
}
25+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package edu.wpi.grip.core.events;
2+
3+
/**
4+
* An event representing the start of a single benchmarked pipeline run.
5+
*/
6+
public class StartSingleBenchmarkRunEvent implements RunPipelineEvent {
7+
8+
@Override
9+
public boolean pipelineShouldRun() {
10+
return true;
11+
}
12+
}

0 commit comments

Comments
 (0)