Sketch DM refactor.

BUG=skia:3255

I think this supports everything DM used to, but has completely refactored how
it works to fit the design in the bug.

Configs like "tiles-gpu" are automatically wired up.

I wouldn't suggest looking at this as a diff.  There's just a bunch of deleted
files, a few new files, and one new file that shares a name with a deleted file
(DM.cpp).

NOTREECHECKS=true

Review URL: https://codereview.chromium.org/788243008
diff --git a/dm/DM.cpp b/dm/DM.cpp
index 7113ad8..d8efc58 100644
--- a/dm/DM.cpp
+++ b/dm/DM.cpp
@@ -1,293 +1,423 @@
-// Main binary for DM.
-// For a high-level overview, please see dm/README.
-
 #include "CrashHandler.h"
-#include "LazyDecodeBitmap.h"
+#include "DMJsonWriter.h"
+#include "DMSrcSink.h"
+#include "OverwriteLine.h"
+#include "ProcStats.h"
+#include "SkBBHFactory.h"
 #include "SkCommonFlags.h"
 #include "SkForceLinking.h"
 #include "SkGraphics.h"
+#include "SkMD5.h"
 #include "SkOSFile.h"
-#include "SkPicture.h"
-#include "SkString.h"
 #include "SkTaskGroup.h"
 #include "Test.h"
-#include "gm.h"
-#include "sk_tool_utils.h"
-#include "sk_tool_utils_flags.h"
+#include "Timer.h"
 
-#include "DMCpuGMTask.h"
-#include "DMGpuGMTask.h"
-#include "DMGpuSupport.h"
-#include "DMImageTask.h"
-#include "DMJsonWriter.h"
-#include "DMPDFTask.h"
-#include "DMPDFRasterizeTask.h"
-#include "DMReporter.h"
-#include "DMSKPTask.h"
-#include "DMTask.h"
-#include "DMTaskRunner.h"
-#include "DMTestTask.h"
-
-#ifdef SK_BUILD_POPPLER
-#  include "SkPDFRasterizer.h"
-#  define RASTERIZE_PDF_PROC SkPopplerRasterizePDF
-#elif defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS)
-#  include "SkCGUtils.h"
-#  define RASTERIZE_PDF_PROC SkPDFDocumentToBitmap
-#else
-#  define RASTERIZE_PDF_PROC NULL
-#endif
-
-#include <ctype.h>
-
-using skiagm::GM;
-using skiagm::GMRegistry;
-using skiatest::Test;
-using skiatest::TestRegistry;
-
-static const char kGpuAPINameGL[] = "gl";
-static const char kGpuAPINameGLES[] = "gles";
-
-DEFINE_bool(gms, true, "Run GMs?");
 DEFINE_bool(tests, true, "Run tests?");
-DEFINE_bool(reportUsedChars, false, "Output test font construction data to be pasted into"
-                                    " create_test_font.cpp.");
-DEFINE_string(images, "resources", "Path to directory containing images to decode.");
-DEFINE_bool(rasterPDF, true, "Rasterize PDFs?");
+DEFINE_string(images, "resources", "Images to decode.");
+DEFINE_string(src, "gm skp image subset", "Source types to test.");
+DEFINE_bool(nameByHash, false,
+            "If true, write to FLAGS_writePath[0]/<hash>.png instead of "
+            "to FLAGS_writePath[0]/<config>/<sourceType>/<name>.png");
+DEFINE_bool2(pathOpsExtended, x, false, "Run extended pathOps tests.");
+DEFINE_string(matrix, "1 0 0 0 1 0 0 0 1",
+              "Matrix to apply when using 'matrix' in config.");
 
 __SK_FORCE_IMAGE_DECODER_LINKING;
+using namespace DM;
 
-static DM::RasterizePdfProc get_pdf_rasterizer_proc() {
-    return reinterpret_cast<DM::RasterizePdfProc>(
-            FLAGS_rasterPDF ? RASTERIZE_PDF_PROC : NULL);
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+static int gPending = 0, gFailures = 0;
+
+static void fail(ImplicitString err) {
+    SkDebugf("\n\nERROR: %s\n\n", err.c_str());
+    sk_atomic_inc(&gFailures);
 }
 
-// "FooBar" -> "foobar".  Obviously, ASCII only.
-static SkString lowercase(SkString s) {
-    for (size_t i = 0; i < s.size(); i++) {
-        s[i] = tolower(s[i]);
-    }
-    return s;
+static void done(double ms, ImplicitString config, ImplicitString src, ImplicitString name) {
+    SkDebugf("%s(%4dMB %5d) %s\t%s %s %s  ", FLAGS_verbose ? "\n" : kSkOverwriteLine
+                                           , sk_tools::getMaxResidentSetSizeMB()
+                                           , sk_atomic_dec(&gPending)-1
+                                           , HumanizeMs(ms).c_str()
+                                           , config.c_str()
+                                           , src.c_str()
+                                           , name.c_str());
 }
 
-static const GrContextFactory::GLContextType native = GrContextFactory::kNative_GLContextType;
-static const GrContextFactory::GLContextType nvpr   = GrContextFactory::kNVPR_GLContextType;
-static const GrContextFactory::GLContextType null   = GrContextFactory::kNull_GLContextType;
-static const GrContextFactory::GLContextType debug  = GrContextFactory::kDebug_GLContextType;
-#if SK_ANGLE
-static const GrContextFactory::GLContextType angle  = GrContextFactory::kANGLE_GLContextType;
-#endif
-#if SK_MESA
-static const GrContextFactory::GLContextType mesa   = GrContextFactory::kMESA_GLContextType;
-#endif
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 
-static void kick_off_gms(const SkTDArray<GMRegistry::Factory>& gms,
-                         const SkTArray<SkString>& configs,
-                         GrGLStandard gpuAPI,
-                         DM::Reporter* reporter,
-                         DM::TaskRunner* tasks) {
-#define START(name, type, ...)                                                              \
-    if (lowercase(configs[j]).equals(name)) {                                               \
-        tasks->add(SkNEW_ARGS(DM::type, (name, reporter, tasks, gms[i], ## __VA_ARGS__)));  \
+template <typename T>
+struct Tagged : public SkAutoTDelete<T> { const char* tag; };
+
+static const bool kMemcpyOK = true;
+
+static SkTArray<Tagged<Src>,  kMemcpyOK>  gSrcs;
+static SkTArray<Tagged<Sink>, kMemcpyOK> gSinks;
+
+static void push_src(const char* tag, Src* s) {
+    SkAutoTDelete<Src> src(s);
+    if (FLAGS_src.contains(tag) &&
+        !SkCommandLineFlags::ShouldSkip(FLAGS_match, src->name().c_str())) {
+        Tagged<Src>& s = gSrcs.push_back();
+        s.reset(src.detach());
+        s.tag = tag;
     }
-    for (int i = 0; i < gms.count(); i++) {
-        for (int j = 0; j < configs.count(); j++) {
+}
 
-            START("565",        CpuGMTask, kRGB_565_SkColorType);
-            START("8888",       CpuGMTask, kN32_SkColorType);
-            START("gpu",        GpuGMTask, native, gpuAPI, 0,  false);
-            START("msaa4",      GpuGMTask, native, gpuAPI, 4,  false);
-            START("msaa16",     GpuGMTask, native, gpuAPI, 16, false);
-            START("nvprmsaa4",  GpuGMTask, nvpr,   gpuAPI, 4,  false);
-            START("nvprmsaa16", GpuGMTask, nvpr,   gpuAPI, 16, false);
-            START("gpudft",     GpuGMTask, native, gpuAPI, 0,  true);
-            START("gpunull",    GpuGMTask, null,   gpuAPI, 0,  false);
-            START("gpudebug",   GpuGMTask, debug,  gpuAPI, 0,  false);
-#if SK_ANGLE
-            START("angle",      GpuGMTask, angle,  gpuAPI, 0,  false);
-#endif
-#if SK_MESA
-            START("mesa",       GpuGMTask, mesa,   gpuAPI, 0,  false);
-#endif
-            START("pdf",        PDFTask,   get_pdf_rasterizer_proc());
+static void gather_srcs() {
+    for (const skiagm::GMRegistry* r = skiagm::GMRegistry::Head(); r; r = r->next()) {
+        push_src("gm", new GMSrc(r->factory()));
+    }
+    if (!FLAGS_skps.isEmpty()) {
+        SkOSFile::Iter it(FLAGS_skps[0], "skp");
+        for (SkString file; it.next(&file); ) {
+            push_src("skp", new SKPSrc(SkOSPath::Join(FLAGS_skps[0], file.c_str())));
         }
     }
-#undef START
-}
-
-static void kick_off_tests(const SkTDArray<TestRegistry::Factory>& tests,
-                           DM::Reporter* reporter,
-                           DM::TaskRunner* tasks) {
-    for (int i = 0; i < tests.count(); i++) {
-        SkAutoTDelete<Test> test(tests[i](NULL));
-        if (test->isGPUTest()) {
-            tasks->add(SkNEW_ARGS(DM::GpuTestTask, (reporter, tasks, tests[i])));
-        } else {
-            tasks->add(SkNEW_ARGS(DM::CpuTestTask, (reporter, tasks, tests[i])));
-        }
-    }
-}
-
-static void find_files(const char* dir,
-                       const char* suffixes[],
-                       size_t num_suffixes,
-                       SkTArray<SkString>* files) {
-    if (0 == strcmp(dir, "")) {
-        return;
-    }
-
-    SkString filename;
-    for (size_t i = 0; i < num_suffixes; i++) {
-        SkOSFile::Iter it(dir, suffixes[i]);
-        while (it.next(&filename)) {
-            if (!SkCommandLineFlags::ShouldSkip(FLAGS_match, filename.c_str())) {
-                files->push_back(SkOSPath::Join(dir, filename.c_str()));
+    if (!FLAGS_images.isEmpty()) {
+        const char* exts[] = {
+            "bmp", "gif", "jpg", "jpeg", "png", "webp", "ktx", "astc", "wbmp", "ico",
+            "BMP", "GIF", "JPG", "JPEG", "PNG", "WEBP", "KTX", "ASTC", "WBMP", "ICO",
+        };
+        for (size_t i = 0; i < SK_ARRAY_COUNT(exts); i++) {
+            SkOSFile::Iter it(FLAGS_images[0], exts[i]);
+            for (SkString file; it.next(&file); ) {
+                SkString path = SkOSPath::Join(FLAGS_images[0], file.c_str());
+                push_src("image",  new ImageSrc(path));     // Decode entire image.
+                push_src("subset", new ImageSrc(path, 5));  // Decode 5 random subsets.
             }
         }
     }
 }
 
-static void kick_off_skps(const SkTArray<SkString>& skps,
-                          DM::Reporter* reporter,
-                          DM::TaskRunner* tasks) {
-    for (int i = 0; i < skps.count(); ++i) {
-        SkAutoTUnref<SkStream> stream(SkStream::NewFromFile(skps[i].c_str()));
-        if (stream.get() == NULL) {
-            SkDebugf("Could not read %s.\n", skps[i].c_str());
-            exit(1);
-        }
-        SkAutoTUnref<SkPicture> pic(
-                SkPicture::CreateFromStream(stream.get(), &sk_tools::LazyDecodeBitmap));
-        if (pic.get() == NULL) {
-            SkDebugf("Could not read %s as an SkPicture.\n", skps[i].c_str());
-            exit(1);
-        }
-
-        SkString filename = SkOSPath::Basename(skps[i].c_str());
-        tasks->add(SkNEW_ARGS(DM::SKPTask, (reporter, tasks, pic, filename)));
-        tasks->add(SkNEW_ARGS(DM::PDFTask, (reporter, tasks, pic, filename,
-                                            get_pdf_rasterizer_proc())));
-    }
+static GrGLStandard get_gpu_api() {
+    if (FLAGS_gpuAPI.contains("gl"))   { return kGL_GrGLStandard; }
+    if (FLAGS_gpuAPI.contains("gles")) { return kGLES_GrGLStandard; }
+    return kNone_GrGLStandard;
 }
 
-static void kick_off_images(const SkTArray<SkString>& images,
-                            DM::Reporter* reporter,
-                            DM::TaskRunner* tasks) {
-    for (int i = 0; i < images.count(); i++) {
-        SkAutoTUnref<SkData> image(SkData::NewFromFileName(images[i].c_str()));
-        if (!image) {
-            SkDebugf("Could not read %s.\n", images[i].c_str());
-            exit(1);
-        }
-        SkString filename = SkOSPath::Basename(images[i].c_str());
-        tasks->add(SkNEW_ARGS(DM::ImageTask, (reporter, tasks, image, filename)));
-        tasks->add(SkNEW_ARGS(DM::ImageTask, (reporter, tasks, image, filename, 5/*subsets*/)));
+static void push_sink(const char* tag, Sink* s) {
+    SkAutoTDelete<Sink> sink(s);
+    if (!FLAGS_config.contains(tag)) {
+        return;
     }
-}
+    // Try a noop Src as a canary.  If it fails, skip this sink.
+    struct : public Src {
+        Error draw(SkCanvas*) const SK_OVERRIDE { return ""; }
+        SkISize size() const SK_OVERRIDE { return SkISize::Make(16, 16); }
+        Name name() const SK_OVERRIDE { return "noop"; }
+    } noop;
 
-
-static void report_failures(const SkTArray<SkString>& failures) {
-    if (failures.count() == 0) {
+    SkBitmap bitmap;
+    SkDynamicMemoryWStream stream;
+    Error err = sink->draw(noop, &bitmap, &stream);
+    if (!err.isEmpty()) {
+        SkDebugf("Skipping %s: %s\n", tag, err.c_str());
         return;
     }
 
-    SkDebugf("Failures:\n");
-    for (int i = 0; i < failures.count(); i++) {
-        SkDebugf("  %s\n", failures[i].c_str());
+    Tagged<Sink>& ts = gSinks.push_back();
+    ts.reset(sink.detach());
+    ts.tag = tag;
+}
+
+static bool gpu_supported() {
+#if SK_SUPPORT_GPU
+    return FLAGS_gpu;
+#else
+    return false;
+#endif
+}
+
+static Sink* create_sink(const char* tag) {
+#define SINK(t, sink, ...) if (0 == strcmp(t, tag)) { return new sink(__VA_ARGS__); }
+    if (gpu_supported()) {
+        const GrGLStandard api = get_gpu_api();
+        SINK("gpunull",    GPUSink, GrContextFactory::kNull_GLContextType,   api,  0, false);
+        SINK("gpudebug",   GPUSink, GrContextFactory::kDebug_GLContextType,  api,  0, false);
+        SINK("gpu",        GPUSink, GrContextFactory::kNative_GLContextType, api,  0, false);
+        SINK("gpudft",     GPUSink, GrContextFactory::kNative_GLContextType, api,  0,  true);
+        SINK("msaa4",      GPUSink, GrContextFactory::kNative_GLContextType, api,  4, false);
+        SINK("msaa16",     GPUSink, GrContextFactory::kNative_GLContextType, api, 16, false);
+        SINK("nvprmsaa4",  GPUSink, GrContextFactory::kNVPR_GLContextType,   api,  4, false);
+        SINK("nvprmsaa16", GPUSink, GrContextFactory::kNVPR_GLContextType,   api, 16, false);
+    #if SK_ANGLE
+        SINK("angle",      GPUSink, GrContextFactory::kANGLE_GLContextType,  api,  0, false);
+    #endif
+    #if SK_MESA
+        SINK("mesa",       GPUSink, GrContextFactory::kMESA_GLContextType,   api,  0, false);
+    #endif
     }
-    SkDebugf("%d failures.\n", failures.count());
+
+    if (FLAGS_cpu) {
+        SINK("565",  RasterSink, kRGB_565_SkColorType);
+        SINK("8888", RasterSink, kN32_SkColorType);
+        // TODO(mtklein): reenable once skiagold can handle .pdf uploads.
+        //SINK("pdf",  PDFSink);
+    }
+#undef SINK
+    return NULL;
 }
 
-static GrGLStandard get_gl_standard() {
-  if (FLAGS_gpuAPI.contains(kGpuAPINameGL)) {
-      return kGL_GrGLStandard;
-  }
-  if (FLAGS_gpuAPI.contains(kGpuAPINameGLES)) {
-      return kGLES_GrGLStandard;
-  }
-  return kNone_GrGLStandard;
+static Sink* create_via(const char* tag, Sink* wrapped) {
+#define VIA(t, via, ...) if (0 == strcmp(t, tag)) { return new via(__VA_ARGS__); }
+    VIA("serialize", ViaSerialization, wrapped);
+
+    VIA("tiles",    ViaTiles, 256, 256,               NULL, wrapped);
+    VIA("tiles_rt", ViaTiles, 256, 256, new SkRTreeFactory, wrapped);
+
+    const int xp = SkGPipeWriter::kCrossProcess_Flag,
+              sa = xp | SkGPipeWriter::kSharedAddressSpace_Flag;
+    VIA("pipe",    ViaPipe,  0, wrapped);
+    VIA("pipe_xp", ViaPipe, xp, wrapped);
+    VIA("pipe_sa", ViaPipe, sa, wrapped);
+
+    if (FLAGS_matrix.count() == 9) {
+        SkMatrix m;
+        for (int i = 0; i < 9; i++) {
+            m[i] = (SkScalar)atof(FLAGS_matrix[i]);
+        }
+        VIA("matrix", ViaMatrix, m, wrapped);
+    }
+#undef VIA
+    return NULL;
 }
 
-template <typename T, typename Registry>
-static void append_matching_factories(Registry* head, SkTDArray<typename Registry::Factory>* out) {
-    for (const Registry* reg = head; reg != NULL; reg = reg->next()) {
-        SkAutoTDelete<T> forName(reg->factory()(NULL));
-        if (!SkCommandLineFlags::ShouldSkip(FLAGS_match, forName->getName())) {
-            *out->append() = reg->factory();
+static void gather_sinks() {
+    for (int i = 0; i < FLAGS_config.count(); i++) {
+        const char* config = FLAGS_config[i];
+        SkTArray<SkString> parts;
+        SkStrSplit(config, "-", &parts);
+
+        Sink* sink = NULL;
+        for (int i = parts.count(); i-- > 0;) {
+            const char* part = parts[i].c_str();
+            Sink* next = (sink == NULL) ? create_sink(part) : create_via(part, sink);
+            if (next == NULL) {
+                SkDebugf("Skipping %s: Don't understand '%s'.\n", config, part);
+                delete sink;
+                sink = NULL;
+                break;
+            }
+            sink = next;
+        }
+        if (sink) {
+            push_sink(config, sink);
         }
     }
 }
 
+// The finest-grained unit of work we can run: draw a single Src into a single Sink,
+// report any errors, and perhaps write out the output: a .png of the bitmap, or a raw stream.
+struct Task {
+    Task(const Tagged<Src>& src, const Tagged<Sink>& sink) : src(src), sink(sink) {}
+    const Tagged<Src>&  src;
+    const Tagged<Sink>& sink;
+
+    static void Run(Task* task) {
+        WallTimer timer;
+        timer.start();
+        if (!FLAGS_dryRun) {
+            SkBitmap bitmap;
+            SkDynamicMemoryWStream stream;
+            Error err = task->sink->draw(*task->src, &bitmap, &stream);
+            if (!err.isEmpty()) {
+                fail(SkStringPrintf("%s %s %s: %s",
+                                    task->sink.tag,
+                                    task->src.tag,
+                                    task->src->name().c_str(),
+                                    err.c_str()));
+            }
+            if (!FLAGS_writePath.isEmpty()) {
+                const char* ext = task->sink->fileExtension();
+                if (stream.bytesWritten() == 0) {
+                    SkMemoryStream pixels(bitmap.getPixels(), bitmap.getSize());
+                    WriteToDisk(*task, &pixels, bitmap.getSize(), &bitmap, ext);
+                } else {
+                    SkAutoTDelete<SkStreamAsset> data(stream.detachAsStream());
+                    WriteToDisk(*task, data, data->getLength(), NULL, ext);
+                }
+            }
+        }
+        timer.end();
+        done(timer.fWall, task->sink.tag, task->src.tag, task->src->name());
+    }
+
+    static void WriteToDisk(const Task& task,
+                            SkStream* data, size_t len,
+                            const SkBitmap* bitmap,
+                            const char* ext) {
+        SkMD5 hash;
+        hash.writeStream(data, len);
+        SkMD5::Digest digest;
+        hash.finish(digest);
+
+        JsonWriter::BitmapResult result;
+        result.name       = task.src->name();
+        result.config     = task.sink.tag;
+        result.sourceType = task.src.tag;
+        result.ext        = ext;
+        for (int i = 0; i < 16; i++) {
+            result.md5.appendf("%02x", digest.data[i]);
+        }
+        JsonWriter::AddBitmapResult(result);
+
+        const char* dir = FLAGS_writePath[0];
+        if (0 == strcmp(dir, "@")) {  // Needed for iOS.
+            dir = FLAGS_resourcePath[0];
+        }
+        sk_mkdir(dir);
+
+        SkString path;
+        if (FLAGS_nameByHash) {
+            path = SkOSPath::Join(dir, result.md5.c_str());
+            path.append(".");
+            path.append(ext);
+            if (sk_exists(path.c_str())) {
+                return;  // Content-addressed.  If it exists already, we're done.
+            }
+        } else {
+            path = SkOSPath::Join(dir, task.sink.tag);
+            sk_mkdir(path.c_str());
+            path = SkOSPath::Join(path.c_str(), task.src.tag);
+            sk_mkdir(path.c_str());
+            path = SkOSPath::Join(path.c_str(), task.src->name().c_str());
+            path.append(".");
+            path.append(ext);
+        }
+
+        SkFILEWStream file(path.c_str());
+        if (!file.isValid()) {
+            fail(SkStringPrintf("Can't open %s for writing.\n", path.c_str()));
+            return;
+        }
+
+        data->rewind();
+        if (bitmap) {
+            // We can't encode A8 bitmaps as PNGs.  Convert them to 8888 first.
+            SkBitmap converted;
+            if (bitmap->info().colorType() == kAlpha_8_SkColorType) {
+                if (!bitmap->copyTo(&converted, kN32_SkColorType)) {
+                    fail("Can't convert A8 to 8888.\n");
+                    return;
+                }
+                bitmap = &converted;
+            }
+            if (!SkImageEncoder::EncodeStream(&file, *bitmap, SkImageEncoder::kPNG_Type, 100)) {
+                fail(SkStringPrintf("Can't encode PNG to %s.\n", path.c_str()));
+                return;
+            }
+        } else {
+            if (!file.writeStream(data, len)) {
+                fail(SkStringPrintf("Can't write to %s.\n", path.c_str()));
+                return;
+            }
+        }
+    }
+};
+
+// Run all tasks in the same enclave serially on the same thread.
+// They can't possibly run concurrently with each other.
+static void run_enclave(SkTArray<Task>* tasks) {
+    for (int i = 0; i < tasks->count(); i++) {
+        Task::Run(tasks->begin() + i);
+    }
+}
+
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+// Unit tests don't fit so well into the Src/Sink model, so we give them special treatment.
+
+static struct : public skiatest::Reporter {
+    void onReportFailed(const skiatest::Failure& failure) SK_OVERRIDE {
+        SkString s;
+        failure.getFailureString(&s);
+        fail(s);
+        JsonWriter::AddTestFailure(failure);
+    }
+    bool allowExtendedTest() const SK_OVERRIDE { return FLAGS_pathOpsExtended; }
+    bool verbose()           const SK_OVERRIDE { return FLAGS_veryVerbose; }
+} gTestReporter;
+
+static SkTArray<SkAutoTDelete<skiatest::Test>, kMemcpyOK> gTests;
+
+static void gather_tests() {
+    if (!FLAGS_tests) {
+        return;
+    }
+    for (const skiatest::TestRegistry* r = skiatest::TestRegistry::Head(); r; r = r->next()) {
+        SkAutoTDelete<skiatest::Test> test(r->factory()(NULL));
+        if (SkCommandLineFlags::ShouldSkip(FLAGS_match, test->getName())) {
+            continue;
+        }
+        if (test->isGPUTest() && !gpu_supported()) {
+            continue;
+        }
+        if (!test->isGPUTest() && !FLAGS_cpu) {
+            continue;
+        }
+        test->setReporter(&gTestReporter);
+        gTests.push_back().reset(test.detach());
+    }
+}
+
+static void run_test(SkAutoTDelete<skiatest::Test>* t) {
+    WallTimer timer;
+    timer.start();
+    skiatest::Test* test = t->get();
+    if (!FLAGS_dryRun) {
+        GrContextFactory grFactory;
+        test->setGrContextFactory(&grFactory);
+        test->run();
+        if (!test->passed()) {
+            fail(SkStringPrintf("test %s failed", test->getName()));
+        }
+    }
+    timer.end();
+    done(timer.fWall, "test", "", test->getName());
+}
+
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
 int dm_main();
 int dm_main() {
     SetupCrashHandler();
     SkAutoGraphics ag;
     SkTaskGroup::Enabler enabled(FLAGS_threads);
 
-    if (FLAGS_dryRun || FLAGS_veryVerbose) {
-        FLAGS_verbose = true;
-    }
-#if SK_ENABLE_INST_COUNT
-    gPrintInstCount = FLAGS_leaks;
-#endif
+    gather_srcs();
+    gather_sinks();
+    gather_tests();
 
-    SkTArray<SkString> configs;
-    for (int i = 0; i < FLAGS_config.count(); i++) {
-        SkStrSplit(FLAGS_config[i], ", ", &configs);
+    gPending = gSrcs.count() * gSinks.count() + gTests.count();
+    SkDebugf("%d srcs * %d sinks + %d tests == %d tasks\n",
+             gSrcs.count(), gSinks.count(), gTests.count(), gPending);
+
+    // We try to exploit as much parallelism as is safe.  Most Src/Sink pairs run on any thread,
+    // but Sinks that identify as part of a particular enclave run serially on a single thread.
+    // Tests run on any thread, with a separate GrContextFactory for each GPU test.
+    SkTArray<Task> enclaves[kNumEnclaves];
+    for (int j = 0; j < gSinks.count(); j++) {
+        SkTArray<Task>& tasks = enclaves[gSinks[j]->enclave()];
+        for (int i = 0; i < gSrcs.count(); i++) {
+            tasks.push_back(Task(gSrcs[i], gSinks[j]));
+        }
     }
 
-    GrGLStandard gpuAPI = get_gl_standard();
+    SK_COMPILE_ASSERT(kAnyThread_Enclave == 0, AnyThreadZero);
+    SkTaskGroup tg;
+        tg.batch(  Task::Run, enclaves[0].begin(), enclaves[0].count());
+        tg.batch(run_enclave,          enclaves+1,      kNumEnclaves-1);
+        tg.batch(   run_test,      gTests.begin(),      gTests.count());
+    tg.wait();
 
-    SkTDArray<GMRegistry::Factory> gms;
-    if (FLAGS_gms) {
-        append_matching_factories<GM>(GMRegistry::Head(), &gms);
+    if (!FLAGS_verbose) {
+        SkDebugf("\n");
     }
 
-    SkTDArray<TestRegistry::Factory> tests;
-    if (FLAGS_tests) {
-        append_matching_factories<Test>(TestRegistry::Head(), &tests);
-    }
-
-
-    SkTArray<SkString> skps;
-    if (!FLAGS_skps.isEmpty()) {
-        const char* suffixes[] = { "skp" };
-        find_files(FLAGS_skps[0], suffixes, SK_ARRAY_COUNT(suffixes), &skps);
-    }
-
-    SkTArray<SkString> images;
-    if (!FLAGS_images.isEmpty()) {
-        const char* suffixes[] = {
-            "bmp", "gif", "jpg", "jpeg", "png", "webp", "ktx", "astc", "wbmp", "ico",
-            "BMP", "GIF", "JPG", "JPEG", "PNG", "WEBP", "KTX", "ASTC", "WBMP", "ICO",
-        };
-        find_files(FLAGS_images[0], suffixes, SK_ARRAY_COUNT(suffixes), &images);
-    }
-
-    SkDebugf("%d GMs x %d configs, %d tests, %d pictures, %d images\n",
-             gms.count(), configs.count(), tests.count(), skps.count(), images.count());
-    DM::Reporter reporter;
-
-    DM::TaskRunner tasks;
-    kick_off_tests(tests, &reporter, &tasks);
-    kick_off_gms(gms, configs, gpuAPI, &reporter, &tasks);
-    kick_off_skps(skps, &reporter, &tasks);
-    kick_off_images(images, &reporter, &tasks);
-    tasks.wait();
-
-    DM::JsonWriter::DumpJson();
-
-    SkDebugf("\n");
-#ifdef SK_DEBUG
-    if (FLAGS_portableFonts && FLAGS_reportUsedChars) {
-        sk_tool_utils::report_used_chars();
-    }
-#endif
-
-    SkTArray<SkString> failures;
-    reporter.getFailures(&failures);
-    report_failures(failures);
-    return failures.count() > 0;
+    JsonWriter::DumpJson();
+    return gPending == 0 && gFailures == 0 ? 0 : 1;
 }
 
 #if !defined(SK_BUILD_FOR_IOS) && !defined(SK_BUILD_FOR_NACL)