Skip to content

Instantly share code, notes, and snippets.

@Groostav
Last active September 27, 2016 23:01
Show Gist options
  • Save Groostav/638e31d4cd2842aa2f05bd9f50c43156 to your computer and use it in GitHub Desktop.
Save Groostav/638e31d4cd2842aa2f05bd9f50c43156 to your computer and use it in GitHub Desktop.
UI workload sample problem
class LoopWorker {
private val executor = SingleThreadedScheduledExecutorService()
fun doLoops() {
val stopwatch = Stopwatch.createUnstarted()
executor.scheduleWithFixedDelay(5000.ms) {
Platform.runLater {
println("starting work...")
stopwatch.start()
for (ignored in 0..60) politely { //note the "politely" between the 'for' and '{'.
//This is kotlin sugar, but effectively means 'politely' is an executor
doMysteryWorkload()
print("work ")
}
println()
println("finished work in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms, taking a break...")
stopwatch.reset()
}
}
}
private fun doMysteryWorkload() = try { Thread.sleep(20) } catch (e: InterruptedException) { }
fun politely(work: () -> Unit){
work.invoke()
yieldUIEventQueue()
}
fun yieldUIEventQueue() {
val key = UUID.randomUUID()
Platform.runLater {
Toolkit.getToolkit().exitNestedEventLoop(key, null)
}
Toolkit.getToolkit().enterNestedEventLoop(key)
}
}
/**
* Displays a "responsiveness spinner" over top of a UI component,
* indicating the liveliness of the UI thread.
*
* In the event that a big job is executed on the UI thread, the UI thread will be blocked, and this
* component will not be re-rendered, causing it to "freeze" in place for some period of time. Once
* the job is completed, all of the update jobs this component has enqueued since it was last executed
* will run in quick succession, causing this component to "warp" to its most recent frame.
*/
public class ResponsivenessSpinner extends Canvas {
public static final Logger log = Logger.getLogger(ResponsivenessSpinner.class.getCanonicalName());
private static boolean wasInstanced = false;
public static boolean wasInstanced() {
return wasInstanced;
}
public static final int SpinerRadius = 100;
public static final double StrokeFudgeFactor = 1.05;
public static final int SceneWidth = 300;
public static final int SceneHeight = 250;
public static final int SpeedDivisor = 800;
private final Instant start = Instant.now();
public ResponsivenessSpinner() {
super(SceneWidth, SceneHeight);
wasInstanced = true;
setMouseTransparent(true);
setDepthTest(DepthTest.DISABLE);
setFocusTraversable(false);
GraphicsContext gc = getGraphicsContext2D();
gc.translate(SceneWidth / 2, SceneHeight / 2);
gc.setStroke(DisplayValues.EmpowerOpsTeal);
gc.setLineWidth(3);
new Thread(() -> {
while (true) {
Platform.runLater(() -> drawShapes(gc));
try {
int sixtyFPS = (int) (1.0 / 60 * 1_000);
assert sixtyFPS > 1;
Thread.sleep(sixtyFPS);
}
catch (InterruptedException e) {
log.info("Interrupted on responsiveness thread?");
continue;
}
}
}, "Responsiveness test").start();
}
private void drawShapes(GraphicsContext gc) {
double offset = - SpinerRadius * StrokeFudgeFactor;
double across = 2 * SpinerRadius * StrokeFudgeFactor;
gc.clearRect(offset, offset, across, across);
Long secs = start.until(Instant.now(), ChronoUnit.MILLIS);
double x = SpinerRadius * Math.cos(secs.doubleValue() / SpeedDivisor);
double y = SpinerRadius * Math.sin(secs.doubleValue() / SpeedDivisor);
gc.strokeLine(x, y, - x, - y);
}
}
package com.empowerops.front_end;
import com.empowerops.common.ui.ResponsivenessSpinner;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.stage.Stage;
public class SimpleSpinnerDriver extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Drawing Operations Test");
Group root = new Group();
Canvas canvas = new ResponsivenessSpinner();
root.getChildren().add(canvas);
primaryStage.setScene(new Scene(root));
primaryStage.show();
LoopWorker loopWorker = new LoopWorker();
loopWorker.doLoops();
}
}
@Groostav
Copy link
Author

Groostav commented Sep 27, 2016

Note the difference in UI responsiveness between when politely exists and when it doesn't is dramatic:

  • with politely the UI thread is perfectly responsive, with a max frame-to-frame delay of 20ms + graphics overhead (16ms is 60fps)
  • without it, there is a noticable "hitch", as the UI becomes unresponsive for a full 1.2 seconds (20ms * 60), every 5 seconds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment