add diff recording and output code

R=bsalomon@google.com

Review URL: https://codereview.chromium.org/18348011

git-svn-id: http://skia.googlecode.com/svn/trunk@9875 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/experimental/skpdiff/SkDiffContext.cpp b/experimental/skpdiff/SkDiffContext.cpp
new file mode 100644
index 0000000..951bba0
--- /dev/null
+++ b/experimental/skpdiff/SkDiffContext.cpp
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkBitmap.h"
+#include "SkImageDecoder.h"
+#include "SkOSFile.h"
+#include "SkStream.h"
+
+#include "SkDiffContext.h"
+#include "SkImageDiffer.h"
+#include "skpdiff_util.h"
+
+SkDiffContext::SkDiffContext() {
+    fRecords = NULL;
+    fDiffers = NULL;
+    fDifferCount = 0;
+}
+
+SkDiffContext::~SkDiffContext() {
+    // Delete the record linked list
+    DiffRecord* currentRecord = fRecords;
+    while (NULL != currentRecord) {
+        DiffRecord* nextRecord = currentRecord->fNext;
+        SkDELETE(currentRecord);
+        currentRecord = nextRecord;
+    }
+
+    if (NULL != fDiffers) {
+        SkDELETE_ARRAY(fDiffers);
+    }
+}
+
+void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) {
+    // Delete whatever the last array of differs was
+    if (NULL != fDiffers) {
+        SkDELETE_ARRAY(fDiffers);
+        fDiffers = NULL;
+        fDifferCount = 0;
+    }
+
+    // Copy over the new differs
+    fDifferCount = differs.count();
+    fDiffers = SkNEW_ARRAY(SkImageDiffer*, fDifferCount);
+    differs.copy(fDiffers);
+}
+
+void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
+    // Load the images at the paths
+    SkBitmap baselineBitmap;
+    SkBitmap testBitmap;
+    if (!SkImageDecoder::DecodeFile(baselinePath, &baselineBitmap)) {
+        SkDebugf("Failed to load bitmap \"%s\"\n", baselinePath);
+        return;
+    }
+    if (!SkImageDecoder::DecodeFile(testPath, &testBitmap)) {
+        SkDebugf("Failed to load bitmap \"%s\"\n", testPath);
+        return;
+    }
+
+    // Setup a record for this diff
+    DiffRecord* newRecord = SkNEW(DiffRecord);
+    newRecord->fBaselinePath = baselinePath;
+    newRecord->fTestPath = testPath;
+    newRecord->fNext = fRecords;
+    fRecords = newRecord;
+
+    // Perform each diff
+    for (int differIndex = 0; differIndex < fDifferCount; differIndex++) {
+        SkImageDiffer* differ = fDiffers[differIndex];
+        int diffID = differ->queueDiff(&baselineBitmap, &testBitmap);
+        if (diffID >= 0) {
+
+            // Copy the results into data for this record
+            DiffData& diffData = newRecord->fDiffs.push_back();
+
+            diffData.fDiffName = differ->getName();
+            diffData.fResult = differ->getResult(diffID);
+
+            int poiCount = differ->getPointsOfInterestCount(diffID);
+            SkIPoint* poi = differ->getPointsOfInterest(diffID);
+            diffData.fPointsOfInterest.append(poiCount, poi);
+
+            // Because we are doing everything synchronously for now, we are done with the diff
+            // after reading it.
+            differ->deleteDiff(diffID);
+        }
+    }
+}
+
+
+void SkDiffContext::diffDirectories(const char baselinePath[], const char testPath[]) {
+    // Get the files in the baseline, we will then look for those inside the test path
+    SkTArray<SkString> baselineEntries;
+    if (!get_directory(baselinePath, &baselineEntries)) {
+        SkDebugf("Unable to open path \"%s\"\n", baselinePath);
+        return;
+    }
+
+    for (int baselineIndex = 0; baselineIndex < baselineEntries.count(); baselineIndex++) {
+        const char* baseFilename = baselineEntries[baselineIndex].c_str();
+
+        // Find the real location of each file to compare
+        SkString baselineFile = SkOSPath::SkPathJoin(baselinePath, baseFilename);
+        SkString testFile = SkOSPath::SkPathJoin(testPath, baseFilename);
+
+        // Check that the test file exists and is a file
+        if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) {
+            // Queue up the comparison with the differ
+            this->addDiff(baselineFile.c_str(), testFile.c_str());
+        } else {
+            SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str());
+        }
+    }
+}
+
+
+void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPattern[]) {
+    // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy
+    // to find corresponding images by matching entry indices.
+
+    SkTArray<SkString> baselineEntries;
+    if (!glob_files(baselinePattern, &baselineEntries)) {
+        SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern);
+        return;
+    }
+
+    SkTArray<SkString> testEntries;
+    if (!glob_files(testPattern, &testEntries)) {
+        SkDebugf("Unable to get pattern \"%s\"\n", testPattern);
+        return;
+    }
+
+    if (baselineEntries.count() != testEntries.count()) {
+        SkDebugf("Baseline and test patterns do not yield corresponding number of files\n");
+        return;
+    }
+
+    for (int entryIndex = 0; entryIndex < baselineEntries.count(); entryIndex++) {
+        const char* baselineFilename = baselineEntries[entryIndex].c_str();
+        const char* testFilename     = testEntries    [entryIndex].c_str();
+
+        this->addDiff(baselineFilename, testFilename);
+    }
+}
+
+void SkDiffContext::outputRecords(SkWStream& stream) {
+    DiffRecord* currentRecord = fRecords;
+    stream.writeText("{\n");
+    stream.writeText("    \"records\": [\n");
+    while (NULL != currentRecord) {
+        stream.writeText("        {\n");
+
+            stream.writeText("            \"baselinePath\": \"");
+            stream.writeText(currentRecord->fBaselinePath.c_str());
+            stream.writeText("\",\n");
+
+            stream.writeText("            \"testPath\": \"");
+            stream.writeText(currentRecord->fTestPath.c_str());
+            stream.writeText("\",\n");
+
+            stream.writeText("            \"diffs\": [\n");
+            for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
+                DiffData& data = currentRecord->fDiffs[diffIndex];
+                stream.writeText("                {\n");
+
+                    stream.writeText("                    \"differName\": \"");
+                    stream.writeText(data.fDiffName);
+                    stream.writeText("\",\n");
+
+                    stream.writeText("                    \"result\": ");
+                    stream.writeScalarAsText(data.fResult);
+                    stream.writeText(",\n");
+
+                    stream.writeText("                    \"pointsOfInterest\": [\n");
+                    for (int poiIndex = 0; poiIndex < data.fPointsOfInterest.count(); poiIndex++) {
+                        SkIPoint poi = data.fPointsOfInterest[poiIndex];
+                        stream.writeText("                        [");
+                        stream.writeDecAsText(poi.x());
+                        stream.writeText(",");
+                        stream.writeDecAsText(poi.y());
+                        stream.writeText("]");
+
+                        // JSON does not allow trailing commas
+                        if (poiIndex + 1 < data.fPointsOfInterest.count())
+                        {
+                            stream.writeText(",");
+                        }
+                        stream.writeText("\n");
+                    }
+                    stream.writeText("                    ]\n");
+                stream.writeText("                }");
+
+                // JSON does not allow trailing commas
+                if (diffIndex + 1 < currentRecord->fDiffs.count())
+                {
+                    stream.writeText(",");
+                }
+                stream.writeText("                \n");
+            }
+            stream.writeText("            ]\n");
+
+        stream.writeText("        }");
+
+        // JSON does not allow trailing commas
+        if (NULL != currentRecord->fNext)
+        {
+            stream.writeText(",");
+        }
+        stream.writeText("\n");
+        currentRecord = currentRecord->fNext;
+    }
+    stream.writeText("    ]\n");
+    stream.writeText("}\n");
+}
diff --git a/experimental/skpdiff/SkDiffContext.h b/experimental/skpdiff/SkDiffContext.h
new file mode 100644
index 0000000..545b515
--- /dev/null
+++ b/experimental/skpdiff/SkDiffContext.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkDiffContext_DEFINED
+#define SkDiffContext_DEFINED
+
+#include "SkString.h"
+#include "SkTArray.h"
+#include "SkTDArray.h"
+
+class SkWStream;
+class SkImageDiffer;
+
+/**
+ * Collects records of diffs and outputs them as JSON.
+ */
+class SkDiffContext {
+public:
+    SkDiffContext();
+    ~SkDiffContext();
+
+    /**
+     * Sets the differs to be used in each diff. Already started diffs will not retroactively use
+     * these.
+     * @param differs An array of differs to use. The array is copied, but not the differs
+     *                themselves.
+     */
+    void setDiffers(const SkTDArray<SkImageDiffer*>& differs);
+
+    /**
+     * Compares two directories of images with the given differ
+     * @param baselinePath The baseline directory's path
+     * @param testPath     The test directory's path
+     */
+    void diffDirectories(const char baselinePath[], const char testPath[]);
+
+    /**
+     * Compares two sets of images identified by glob style patterns with the given differ
+     * @param baselinePattern A pattern for baseline files
+     * @param testPattern     A pattern for test files that matches each file of the baseline file
+     */
+    void diffPatterns(const char baselinePattern[], const char testPattern[]);
+
+    /**
+     * Compares the images at the given paths
+     * @param baselinePath The baseline file path
+     * @param testPath     The matching test file path
+     */
+    void addDiff(const char* baselinePath, const char* testPath);
+
+    /**
+     * Output the records of each diff in JSON.
+     *
+     * The format of the JSON document is one top level array named "records".
+     * Each record in the array is an object with both a "baselinePath" and "testPath" string field.
+     * They also have an array named "diffs" with each element being one diff record for the two
+     * images indicated in the above field.
+     * A diff record includes:
+     *    "differName"       : string name of the diff metric used
+     *    "result"           : numerical result of the diff
+     *    "pointsOfInterest" : an array of coordinates (stored as a 2-array of ints) of interesting
+     *                         points
+     *
+     * Here is an example:
+     *
+     * {
+     *     "records": [
+     *         {
+     *             "baselinePath": "queue.png",
+     *             "testPath": "queue.png",
+     *             "diffs": [
+     *                 {
+     *                     "differName": "different_pixels",
+     *                     "result": 1,
+     *                     "pointsOfInterest": [
+     *                         [285,279],
+     *                     ]
+     *                 }
+     *             ]
+     *         }
+     *     ]
+     * }
+     *
+     * @param stream The stream to output the diff to
+     */
+    void outputRecords(SkWStream& stream);
+
+private:
+    struct DiffData {
+        const char* fDiffName;
+        double fResult;
+        SkTDArray<SkIPoint> fPointsOfInterest;
+    };
+
+    struct DiffRecord {
+        SkString           fBaselinePath;
+        SkString               fTestPath;
+        SkTArray<DiffData>        fDiffs;
+        DiffRecord*                fNext;
+    };
+
+    // We use linked list for the records so that their pointers remain stable. A resizable array
+    // might change its pointers, which would make it harder for async diffs to record their
+    // results.
+    DiffRecord * fRecords;
+
+    SkImageDiffer** fDiffers;
+    int fDifferCount;
+};
+
+#endif
diff --git a/experimental/skpdiff/main.cpp b/experimental/skpdiff/main.cpp
index b9d533d..de1171f 100644
--- a/experimental/skpdiff/main.cpp
+++ b/experimental/skpdiff/main.cpp
@@ -11,14 +11,12 @@
 
 #include "SkCommandLineFlags.h"
 #include "SkGraphics.h"
-#include "SkPoint.h"
-#include "SkOSFile.h"
-#include "SkString.h"
-#include "SkTArray.h"
+#include "SkStream.h"
 #include "SkTDArray.h"
 
-#include "SkImageDiffer.h"
 #include "SkCLImageDiffer.h"
+#include "SkDiffContext.h"
+#include "SkImageDiffer.h"
 #include "SkPMetric.h"
 #include "skpdiff_util.h"
 
@@ -30,6 +28,7 @@
 DEFINE_string2(differs, d, "", "The names of the differs to use or all of them by default");
 DEFINE_string2(folders, f, "", "Compare two folders with identical subfile names: <baseline folder> <test folder>");
 DEFINE_string2(patterns, p, "", "Use two patterns to compare images: <baseline> <test>");
+DEFINE_string2(output, o, "skpdiff_output.json", "Writes the output of these diffs to output: <output>");
 
 /// A callback for any OpenCL errors
 CL_CALLBACK void error_notify(const char* errorInfo, const void* privateInfoSize, ::size_t cb, void* userData) {
@@ -72,78 +71,6 @@
     return true;
 }
 
-/// Compares two directories of images with the given differ
-static void diff_directories(const char baselinePath[], const char testPath[], SkImageDiffer* differ) {
-    // Get the files in the baseline, we will then look for those inside the test path
-    SkTArray<SkString> baselineEntries;
-    if (!get_directory(baselinePath, &baselineEntries)) {
-        SkDebugf("Unable to open path \"%s\"\n", baselinePath);
-        return;
-    }
-
-    SkTDArray<int> queuedDiffIDs;
-    for (int baselineIndex = 0; baselineIndex < baselineEntries.count(); baselineIndex++) {
-        const char* baseFilename = baselineEntries[baselineIndex].c_str();
-        SkDebugf("\n%s\n", baseFilename);
-
-        // Find the real location of each file to compare
-        SkString baselineFile = SkOSPath::SkPathJoin(baselinePath, baseFilename);
-        SkString testFile = SkOSPath::SkPathJoin(testPath, baseFilename);
-
-        // Check that the test file exists and is a file
-        if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) {
-            // Queue up the comparison with the differ
-            int diffID = differ->queueDiffOfFile(baselineFile.c_str(), testFile.c_str());
-            if (diffID >= 0) {
-                queuedDiffIDs.push(diffID);
-                SkDebugf("Result: %f\n", differ->getResult(diffID));
-                SkDebugf("POI Count: %i\n", differ->getPointsOfInterestCount(diffID));
-                differ->deleteDiff(diffID);
-            }
-        } else {
-            SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str());
-        }
-    }
-}
-
-
-/// Compares two sets of images identified by glob style patterns with the given differ
-static void diff_patterns(const char baselinePattern[], const char testPattern[], SkImageDiffer* differ) {
-    // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy
-    // to find corresponding images by matching entry indices.
-
-    SkTArray<SkString> baselineEntries;
-    if (!glob_files(baselinePattern, &baselineEntries)) {
-        SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern);
-        return;
-    }
-
-    SkTArray<SkString> testEntries;
-    if (!glob_files(testPattern, &testEntries)) {
-        SkDebugf("Unable to get pattern \"%s\"\n", testPattern);
-        return;
-    }
-
-    if (baselineEntries.count() != testEntries.count()) {
-        SkDebugf("Baseline and test patterns do not yield corresponding number of files\n");
-        return;
-    }
-
-    SkTDArray<int> queuedDiffIDs;
-    for (int entryIndex = 0; entryIndex < baselineEntries.count(); entryIndex++) {
-        const char* baselineFilename = baselineEntries[entryIndex].c_str();
-        const char* testFilename     = testEntries    [entryIndex].c_str();
-        SkDebugf("\n%s %s\n", baselineFilename, testFilename);
-
-        int diffID = differ->queueDiffOfFile(baselineFilename, testFilename);
-        if (diffID >= 0) {
-            queuedDiffIDs.push(diffID);
-            SkDebugf("Result: %f\n", differ->getResult(diffID));
-            SkDebugf("POI Count: %i\n", differ->getPointsOfInterestCount(diffID));
-            differ->deleteDiff(diffID);
-        }
-    }
-}
 
 
 static bool init_cl_diff(SkImageDiffer* differ) {
@@ -194,21 +121,24 @@
     }
 
     // Figure which differs the user chose, and optionally print them if the user requests it
-    SkTDArray<int> chosenDiffers;
+    SkTDArray<SkImageDiffer*> chosenDiffers;
     for (int differIndex = 0; NULL != gDiffers[differIndex]; differIndex++) {
+        SkImageDiffer* differ = gDiffers[differIndex];
         if (FLAGS_list) {
-            SkDebugf("    %s", gDiffers[differIndex]->getName());
+            SkDebugf("    %s", differ->getName());
             SkDebugf("\n");
         }
 
-        // Check if this differ was chosen by any of the flags
+        // Check if this differ was chosen by any of the flags. Initialize them if they were chosen.
         if (FLAGS_differs.isEmpty()) {
             // If no differs were chosen, they all get added
-            chosenDiffers.push(differIndex);
+            chosenDiffers.push(differ);
+            gDiffInits[differIndex](differ);
         } else {
             for (int flagIndex = 0; flagIndex < FLAGS_differs.count(); flagIndex++) {
-                if (SkString(FLAGS_differs[flagIndex]).equals(gDiffers[differIndex]->getName())) {
-                    chosenDiffers.push(differIndex);
+                if (SkString(FLAGS_differs[flagIndex]).equals(differ->getName())) {
+                    chosenDiffers.push(differ);
+                    gDiffInits[differIndex](differ);
                     break;
                 }
             }
@@ -235,31 +165,23 @@
         }
     }
 
-    // TODO Move the differ loop to after the bitmaps are decoded and/or uploaded to the OpenCL
-    // device. Those are often the slowest processes and should not be done more than once if it can
-    // be helped.
+    SkDiffContext ctx;
+    ctx.setDiffers(chosenDiffers);
 
-    // Perform each requested diff
-    for (int chosenDifferIndex = 0; chosenDifferIndex < chosenDiffers.count(); chosenDifferIndex++) {
-        int differIndex = chosenDiffers[chosenDifferIndex];
+    // Perform a folder diff if one is requested
+    if (!FLAGS_folders.isEmpty()) {
+        ctx.diffDirectories(FLAGS_folders[0], FLAGS_folders[1]);
+    }
 
-        // Get the chosen differ and say which one they chose
-        SkImageDiffer * differ = gDiffers[differIndex];
-        SkDebugf("Using metric \"%s\"\n", differ->getName());
+    // Perform a pattern diff if one is requested
+    if (!FLAGS_patterns.isEmpty()) {
+        ctx.diffPatterns(FLAGS_patterns[0], FLAGS_patterns[1]);
+    }
 
-        // Initialize the differ using the global list of init functions that match the list of
-        // differs
-        gDiffInits[differIndex](differ);
-
-        // Perform a folder diff if one is requested
-        if (!FLAGS_folders.isEmpty()) {
-            diff_directories(FLAGS_folders[0], FLAGS_folders[1], differ);
-        }
-
-        // Perform a pattern diff if one is requested
-        if (!FLAGS_patterns.isEmpty()) {
-            diff_patterns(FLAGS_patterns[0], FLAGS_patterns[1], differ);
-        }
+    // Output to the file specified
+    if (!FLAGS_output.isEmpty()) {
+        SkFILEWStream outputStream(FLAGS_output[0]);
+        ctx.outputRecords(outputStream);
     }
 
     return 0;
diff --git a/experimental/skpdiff/skpdiff.gyp b/experimental/skpdiff/skpdiff.gyp
index 0595684..0cb0d66 100644
--- a/experimental/skpdiff/skpdiff.gyp
+++ b/experimental/skpdiff/skpdiff.gyp
@@ -10,6 +10,7 @@
       'type': 'executable',
       'sources': [
         'main.cpp',
+        'SkDiffContext.cpp',
         'SkImageDiffer.cpp',
         'SkCLImageDiffer.cpp',
         'SkPMetric.cpp',