Improved performance of screenshot comparison dialog.

Change-Id: I49bb2d40b5c03be69126f8548fc44eb93c420b26
diff --git a/src/com/google/gct/testing/ConfigurationResult.java b/src/com/google/gct/testing/ConfigurationResult.java
index 208719f..07511a4 100644
--- a/src/com/google/gct/testing/ConfigurationResult.java
+++ b/src/com/google/gct/testing/ConfigurationResult.java
@@ -161,7 +161,6 @@
   }
 
   public BufferedImage getScreenshotForTestAndStep(TestName testName, int step) {
-    System.out.println("About to load a screenshot");
     String fileName = screenshotKeyToFileName.get(new ScreenshotKey(testName, step));
 
     BufferedImage image = null;
@@ -180,7 +179,6 @@
       }
       image.flush();
     }
-    System.out.println("Finished loading the screenshot");
     return image;
   }
 
diff --git a/src/com/google/gct/testing/LoadingPortrait.png b/src/com/google/gct/testing/LoadingPortrait.png
new file mode 100644
index 0000000..c7233a6
--- /dev/null
+++ b/src/com/google/gct/testing/LoadingPortrait.png
Binary files differ
diff --git a/src/com/google/gct/testing/ScreenshotComparisonPanel.java b/src/com/google/gct/testing/ScreenshotComparisonPanel.java
index 66fe3ef..1906d3b 100644
--- a/src/com/google/gct/testing/ScreenshotComparisonPanel.java
+++ b/src/com/google/gct/testing/ScreenshotComparisonPanel.java
@@ -87,11 +87,17 @@
   //private static final int IMAGE_WIDTH = 300;
   //private static int imageHeight = 550;
   private static final BufferedImage NO_IMAGE;
+  private static final BufferedImage LOADING_PORTRAIT;
+
+  private UpdateImageThread updateImageThread;
+  private final Object lock;
 
   static {
     try {
       NO_IMAGE = ImageIO.read(ScreenshotComparisonPanel.class.getResourceAsStream("NoImage.png"));
       NO_IMAGE.flush();
+      LOADING_PORTRAIT = ImageIO.read(ScreenshotComparisonPanel.class.getResourceAsStream("LoadingPortrait.png"));
+      LOADING_PORTRAIT.flush();
     }
     catch (IOException e) {
       throw new RuntimeException(e);
@@ -104,6 +110,7 @@
   public ScreenshotComparisonPanel(ScreenshotComparisonDialog parent, AbstractTestProxy testTreeRoot, CloudTestConfigurationImpl configuration,
                                    ConfigurationInstance configurationInstance, TestName currentTest, int currentStep,
                                    Map<String, ConfigurationResult> results) {
+    lock = this;
     this.parent = parent;
     this.testTreeRoot = testTreeRoot;
     this.configuration = configuration;
@@ -221,42 +228,93 @@
   }
 
   private void updateImage() {
-    ConfigurationResult selectedConfigurationResult = getSelectedConfigurationResult();
-    if (selectedConfigurationResult == null) {
-      myImageLabel.setIcon(new ImageIcon(NO_IMAGE.getScaledInstance(NO_IMAGE_WIDTH, MAX_IMAGE_HEIGHT, Image.SCALE_SMOOTH)));
-      return;
+    synchronized (lock) {
+      if (updateImageThread != null) {
+        updateImageThread.makeObsolete();
+      }
+      ConfigurationResult selectedConfigurationResult = getSelectedConfigurationResult();
+      if (selectedConfigurationResult == null) {
+        myImageLabel.setIcon(new ImageIcon(NO_IMAGE.getScaledInstance(NO_IMAGE_WIDTH, MAX_IMAGE_HEIGHT, Image.SCALE_SMOOTH)));
+        return;
+      }
+      myImageLabel.setIcon(new ImageIcon(LOADING_PORTRAIT.getScaledInstance(NO_IMAGE_WIDTH, MAX_IMAGE_HEIGHT, Image.SCALE_SMOOTH)));
+      updateImageThread = new UpdateImageThread(currentTest, currentStep, selectedConfigurationResult);
+      updateImageThread.start();
     }
-    currentImage = selectedConfigurationResult.getScreenshotForTestAndStep(currentTest, currentStep);
-    if (currentImage == null) {
-      myImageLabel.setIcon(new ImageIcon(NO_IMAGE.getScaledInstance(NO_IMAGE_WIDTH, MAX_IMAGE_HEIGHT, Image.SCALE_SMOOTH)));
-      return;
+  }
+
+  private class UpdateImageThread extends Thread {
+
+    private final TestName test;
+    private final int step;
+    private final ConfigurationResult configurationResult;
+
+    private BufferedImage loadedImage;
+
+    private volatile boolean isObsolete = false;
+
+    public UpdateImageThread(TestName test, int step, ConfigurationResult configurationResult) {
+      this.test = test;
+      this.step = step;
+      this.configurationResult = configurationResult;
     }
 
-    //TODO: This is a temporary rotation hack that should be removed after the backend produces correct landscape screenshots.
-    if (selectedConfigurationInstance.getEncodedString().endsWith("landscape")
-        && currentImage.getHeight() > currentImage.getWidth()) { // Rotate landscape screenshots that are indeed mis-rotated.
-
-      AffineTransform transform = new AffineTransform();
-      transform.translate(currentImage.getHeight() / 2, currentImage.getWidth() / 2);
-      transform.rotate(-Math.PI / 2);
-      //transform.scale(0.5, 0.5);
-      transform.translate(-currentImage.getWidth() / 2, -currentImage.getHeight() / 2);
-      //transform.rotate(-Math.PI / 2, currentImage.getWidth() / 2, currentImage.getHeight() / 2);
-      AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
-      currentImage = op.filter(currentImage, null);
+    public synchronized void makeObsolete() {
+      isObsolete = true;
     }
 
-    int imageWidth = currentImage.getWidth();
-    int imageHeight = currentImage.getHeight();
-    if (imageWidth > MAX_IMAGE_WIDTH) {
-      imageHeight = imageHeight * MAX_IMAGE_WIDTH / imageWidth;
-      imageWidth = MAX_IMAGE_WIDTH;
+    public synchronized boolean isObsolete() {
+      return isObsolete;
     }
-    if (imageHeight > MAX_IMAGE_HEIGHT) {
-      imageWidth = imageWidth * MAX_IMAGE_HEIGHT / imageHeight;
-      imageHeight = MAX_IMAGE_HEIGHT;
+
+    @Override
+    public void run() {
+      loadedImage = configurationResult.getScreenshotForTestAndStep(test, step); // A long-running operation.
+      if (isObsolete()) {
+        return;
+      }
+      SwingUtilities.invokeLater(new Runnable() {
+        @Override
+        public void run() {
+          synchronized (lock) {
+            if (isObsolete()) {
+              return;
+            }
+            currentImage = loadedImage;
+            if (currentImage == null) {
+              myImageLabel.setIcon(new ImageIcon(NO_IMAGE.getScaledInstance(NO_IMAGE_WIDTH, MAX_IMAGE_HEIGHT, Image.SCALE_SMOOTH)));
+              return;
+            }
+
+            //TODO: This is a temporary rotation hack that should be removed after the backend produces correct landscape screenshots.
+            // Rotate landscape screenshots that are indeed mis-rotated.
+            if (selectedConfigurationInstance.getEncodedString().endsWith("landscape") && currentImage.getHeight() > currentImage.getWidth()) {
+
+              AffineTransform transform = new AffineTransform();
+              transform.translate(currentImage.getHeight() / 2, currentImage.getWidth() / 2);
+              transform.rotate(-Math.PI / 2);
+              //transform.scale(0.5, 0.5);
+              transform.translate(-currentImage.getWidth() / 2, -currentImage.getHeight() / 2);
+              //transform.rotate(-Math.PI / 2, currentImage.getWidth() / 2, currentImage.getHeight() / 2);
+              AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
+              currentImage = op.filter(currentImage, null);
+            }
+
+            int imageWidth = currentImage.getWidth();
+            int imageHeight = currentImage.getHeight();
+            if (imageWidth > MAX_IMAGE_WIDTH) {
+              imageHeight = imageHeight * MAX_IMAGE_WIDTH / imageWidth;
+              imageWidth = MAX_IMAGE_WIDTH;
+            }
+            if (imageHeight > MAX_IMAGE_HEIGHT) {
+              imageWidth = imageWidth * MAX_IMAGE_HEIGHT / imageHeight;
+              imageHeight = MAX_IMAGE_HEIGHT;
+            }
+            myImageLabel.setIcon(new ImageIcon(currentImage.getScaledInstance(imageWidth, imageHeight, Image.SCALE_SMOOTH)));
+          }
+        }
+      });
     }
-    myImageLabel.setIcon(new ImageIcon(currentImage.getScaledInstance(imageWidth, imageHeight, Image.SCALE_SMOOTH)));
   }
 
   private void updateHeaderBar() {