[canvaskit] Add job to compile GMTests js/wasm

Next step is to add the following task:
Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2

Bug: skia:10812
Change-Id: Ibe45b7205cebd30f0e7904ea6d93a01ea3df87fe
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/324617
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Eric Boren <borenet@google.com>
diff --git a/infra/bots/gen_tasks_logic/gen_tasks_logic.go b/infra/bots/gen_tasks_logic/gen_tasks_logic.go
index 60c0dcf..82e07f4 100644
--- a/infra/bots/gen_tasks_logic/gen_tasks_logic.go
+++ b/infra/bots/gen_tasks_logic/gen_tasks_logic.go
@@ -442,7 +442,7 @@
 				"NoGPUThreads", "ProcDump", "DDL1", "DDL3", "OOPRDDL", "T8888",
 				"DDLTotal", "DDLRecord", "9x9", "BonusConfigs", "SkottieTracing", "SkottieWASM",
 				"GpuTess", "NonNVPR", "Mskp", "Docker", "PDF", "SkVM", "Puppeteer",
-				"SkottieFrames", "RenderSKP", "CanvasPerf", "AllPathsVolatile"}
+				"SkottieFrames", "RenderSKP", "CanvasPerf", "AllPathsVolatile", "WebGL2"}
 			keep := make([]string, 0, len(ec))
 			for _, part := range ec {
 				if !In(part, ignore) {
@@ -1015,84 +1015,88 @@
 // compile generates a compile task. Returns the name of the compile task.
 func (b *jobBuilder) compile() string {
 	name := b.deriveCompileTaskName()
-	b.addTask(name, func(b *taskBuilder) {
-		recipe := "compile"
-		isolate := "compile.isolate"
-		if b.extraConfig("NoDEPS", "CMake", "CommandBuffer", "Flutter", "SKQP") {
-			recipe = "sync_and_compile"
-			isolate = "swarm_recipe.isolate"
-			b.recipeProps(EXTRA_PROPS)
-			b.usesGit()
-			if !b.extraConfig("NoDEPS") {
-				b.cache(CACHES_WORKDIR...)
+	if b.extraConfig("WasmGMTests") {
+		b.compileWasmGMTests(name)
+	} else {
+		b.addTask(name, func(b *taskBuilder) {
+			recipe := "compile"
+			isolate := "compile.isolate"
+			if b.extraConfig("NoDEPS", "CMake", "CommandBuffer", "Flutter", "SKQP") {
+				recipe = "sync_and_compile"
+				isolate = "swarm_recipe.isolate"
+				b.recipeProps(EXTRA_PROPS)
+				b.usesGit()
+				if !b.extraConfig("NoDEPS") {
+					b.cache(CACHES_WORKDIR...)
+				}
+			} else {
+				b.idempotent()
 			}
-		} else {
-			b.idempotent()
-		}
-		b.kitchenTask(recipe, OUTPUT_BUILD)
-		b.isolate(isolate)
-		b.serviceAccount(b.cfg.ServiceAccountCompile)
-		b.swarmDimensions()
-		if b.extraConfig("Docker", "LottieWeb", "SKQP", "CMake") || b.compiler("EMCC") {
-			b.usesDocker()
-			b.cache(CACHES_DOCKER...)
-		}
+			b.kitchenTask(recipe, OUTPUT_BUILD)
+			b.isolate(isolate)
+			b.serviceAccount(b.cfg.ServiceAccountCompile)
+			b.swarmDimensions()
+			if b.extraConfig("Docker", "LottieWeb", "SKQP", "CMake") || b.compiler("EMCC") {
+				b.usesDocker()
+				b.cache(CACHES_DOCKER...)
+			}
 
-		// Android bots require a toolchain.
-		if b.extraConfig("Android") {
-			if b.matchOs("Mac") {
-				b.asset("android_ndk_darwin")
-			} else if b.matchOs("Win") {
-				pkg := b.MustGetCipdPackageFromAsset("android_ndk_windows")
-				pkg.Path = "n"
-				b.cipd(pkg)
-			} else if !b.extraConfig("SKQP") {
-				b.asset("android_ndk_linux")
-			}
-		} else if b.extraConfig("Chromebook") {
-			b.asset("clang_linux")
-			if b.arch("x86_64") {
-				b.asset("chromebook_x86_64_gles")
-			} else if b.arch("arm") {
-				b.asset("armhf_sysroot")
-				b.asset("chromebook_arm_gles")
-			}
-		} else if b.isLinux() {
-			if b.compiler("Clang") {
+			// Android bots require a toolchain.
+			if b.extraConfig("Android") {
+				if b.matchOs("Mac") {
+					b.asset("android_ndk_darwin")
+				} else if b.matchOs("Win") {
+					pkg := b.MustGetCipdPackageFromAsset("android_ndk_windows")
+					pkg.Path = "n"
+					b.cipd(pkg)
+				} else if !b.extraConfig("SKQP") {
+					b.asset("android_ndk_linux")
+				}
+			} else if b.extraConfig("Chromebook") {
 				b.asset("clang_linux")
+				if b.arch("x86_64") {
+					b.asset("chromebook_x86_64_gles")
+				} else if b.arch("arm") {
+					b.asset("armhf_sysroot")
+					b.asset("chromebook_arm_gles")
+				}
+			} else if b.isLinux() {
+				if b.compiler("Clang") {
+					b.asset("clang_linux")
+				}
+				if b.extraConfig("SwiftShader") {
+					b.asset("cmake_linux")
+				}
+				if b.extraConfig("OpenCL") {
+					b.asset("opencl_headers", "opencl_ocl_icd_linux")
+				}
+				b.asset("ccache_linux")
+				b.usesCCache()
+			} else if b.matchOs("Win") {
+				b.asset("win_toolchain")
+				if b.compiler("Clang") {
+					b.asset("clang_win")
+				}
+				if b.extraConfig("OpenCL") {
+					b.asset("opencl_headers")
+				}
+			} else if b.matchOs("Mac") {
+				b.cipd(CIPD_PKGS_XCODE...)
+				b.Spec.Caches = append(b.Spec.Caches, &specs.Cache{
+					Name: "xcode",
+					Path: "cache/Xcode.app",
+				})
+				b.asset("ccache_mac")
+				b.usesCCache()
+				if b.extraConfig("CommandBuffer") {
+					b.timeout(2 * time.Hour)
+				}
+				if b.extraConfig("iOS") {
+					b.asset("provisioning_profile_ios")
+				}
 			}
-			if b.extraConfig("SwiftShader") {
-				b.asset("cmake_linux")
-			}
-			if b.extraConfig("OpenCL") {
-				b.asset("opencl_headers", "opencl_ocl_icd_linux")
-			}
-			b.asset("ccache_linux")
-			b.usesCCache()
-		} else if b.matchOs("Win") {
-			b.asset("win_toolchain")
-			if b.compiler("Clang") {
-				b.asset("clang_win")
-			}
-			if b.extraConfig("OpenCL") {
-				b.asset("opencl_headers")
-			}
-		} else if b.matchOs("Mac") {
-			b.cipd(CIPD_PKGS_XCODE...)
-			b.Spec.Caches = append(b.Spec.Caches, &specs.Cache{
-				Name: "xcode",
-				Path: "cache/Xcode.app",
-			})
-			b.asset("ccache_mac")
-			b.usesCCache()
-			if b.extraConfig("CommandBuffer") {
-				b.timeout(2 * time.Hour)
-			}
-			if b.extraConfig("iOS") {
-				b.asset("provisioning_profile_ios")
-			}
-		}
-	})
+		})
+	}
 
 	// All compile tasks are runnable as their own Job. Assert that the Job
 	// is listed in jobs.
@@ -1714,3 +1718,34 @@
 		})
 	})
 }
+
+// compileWasmGMTests uses a task driver to compile the GMs and unit tests for Web Assembly (WASM)
+// using WebGL if necessary.
+func (b *jobBuilder) compileWasmGMTests(compileName string) {
+	b.addTask(compileName, func(b *taskBuilder) {
+		b.attempts(1)
+		b.usesDocker()
+		b.linuxGceDimensions(MACHINE_TYPE_MEDIUM)
+		b.cipd(CIPD_PKG_LUCI_AUTH)
+		b.dep(b.buildTaskDrivers())
+		b.output("wasm_out")
+		b.timeout(20 * time.Minute)
+		b.isolate("compile.isolate")
+		b.serviceAccount(b.cfg.ServiceAccountCompile)
+		b.cache(CACHES_DOCKER...)
+		// For now, we only have one compile mode - a GPU release mode. This should be sufficient to
+		// run CPU, WebGL1, and WebGL2 tests. Debug mode is not needed for the waterfall because
+		// when using puppeteer, stacktraces from exceptions are hard to get access to, so we do not
+		// even bother.
+		b.cmd(
+			"./compile_wasm_gm_tests",
+			"--project_id", "skia-swarming-bots",
+			"--task_id", specs.PLACEHOLDER_TASK_ID,
+			"--task_name", compileName,
+			"--out_path", "./wasm_out",
+			"--skia_path", "./skia",
+			"--work_path", "./cache/docker/wasm_gm",
+			"--alsologtostderr",
+		)
+	})
+}
diff --git a/infra/bots/jobs.json b/infra/bots/jobs.json
index 3d4e356..8d70099 100644
--- a/infra/bots/jobs.json
+++ b/infra/bots/jobs.json
@@ -75,6 +75,7 @@
   "Build-Debian10-EMCC-wasm-Release-CanvasKit",
   "Build-Debian10-EMCC-wasm-Release-CanvasKit_CPU",
   "Build-Debian10-EMCC-wasm-Release-PathKit",
+  "Build-Debian10-EMCC-wasm-Release-WasmGMTests",
   "Build-Mac10.15.5-Clang-arm64-Debug-iOS",
   "Build-Mac10.15.5-Clang-arm64-Debug-iOS_Metal",
   "Build-Mac10.15.5-Clang-arm64-Release-iOS",
diff --git a/infra/bots/task_drivers/cifuzz/cifuzz.go b/infra/bots/task_drivers/cifuzz/cifuzz.go
index fec7254..4f380bd 100644
--- a/infra/bots/task_drivers/cifuzz/cifuzz.go
+++ b/infra/bots/task_drivers/cifuzz/cifuzz.go
@@ -110,8 +110,6 @@
 	cifuzzDockerImage       = "gcr.io/oss-fuzz-base/cifuzz-base:latest"
 	buildFuzzersDockerImage = "local_build_fuzzers"
 	runFuzzersDockerImage   = "local_run_fuzzers"
-
-	pinnedSwiftshaderRevision = "45510ad8a77862c1ce2e33f0efed41544f5f048b"
 )
 
 func setupCIFuzzRepoAndDocker(ctx context.Context, workdir, gitAbsPath string) error {
diff --git a/infra/bots/task_drivers/compile_wasm_gm_tests/compile_wasm_gm_tests.go b/infra/bots/task_drivers/compile_wasm_gm_tests/compile_wasm_gm_tests.go
new file mode 100644
index 0000000..9b49264
--- /dev/null
+++ b/infra/bots/task_drivers/compile_wasm_gm_tests/compile_wasm_gm_tests.go
@@ -0,0 +1,117 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"context"
+	"flag"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"cloud.google.com/go/storage"
+	"go.skia.org/infra/go/auth"
+	"go.skia.org/infra/go/skerr"
+	"go.skia.org/infra/task_driver/go/lib/auth_steps"
+	"go.skia.org/infra/task_driver/go/lib/docker"
+	"go.skia.org/infra/task_driver/go/lib/os_steps"
+	"go.skia.org/infra/task_driver/go/td"
+)
+
+const dockerImage = "gcr.io/skia-public/canvaskit-emsdk:2.0.6_v1"
+const innerBuildScript = "/SRC/infra/canvaskit/build_gmtests.sh"
+
+func main() {
+	var (
+		// Required properties for this task.
+		outPath   = flag.String("out_path", "", "The directory to put the built wasm/js code.")
+		projectID = flag.String("project_id", "", "ID of the Google Cloud project.")
+		skiaPath  = flag.String("skia_path", "", "Path to skia repo root.")
+		taskID    = flag.String("task_id", "", "task id this data was generated on")
+		taskName  = flag.String("task_name", "", "Name of the task.")
+		workPath  = flag.String("work_path", "", "The directory to use to store temporary files (e.g. docker build)")
+
+		// Debugging flags.
+		local       = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
+		outputSteps = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
+	)
+
+	// Setup.
+	ctx := td.StartRun(projectID, taskID, taskName, outputSteps, local)
+	defer td.EndRun(ctx)
+
+	outAbsPath := getAbsoluteOfRequiredFlag(ctx, *outPath, "out_path")
+	skiaAbsPath := getAbsoluteOfRequiredFlag(ctx, *skiaPath, "skia_path")
+	workAbsPath := getAbsoluteOfRequiredFlag(ctx, *workPath, "work_path")
+
+	if err := os_steps.MkdirAll(ctx, workAbsPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+	if err := os_steps.MkdirAll(ctx, outAbsPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	doc, err := setupDocker(ctx, *local)
+	if err != nil {
+		td.Fatal(ctx, err)
+	}
+	defer doc.Cleanup(ctx)
+
+	command := []string{innerBuildScript}
+	volumes := []string{skiaAbsPath + ":/SRC", workAbsPath + ":/OUT"}
+
+	if err := doc.Run(ctx, dockerImage, command, volumes, nil); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	if err := extractOutput(ctx, workAbsPath, outAbsPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+}
+
+func setupDocker(ctx context.Context, isLocal bool) (*docker.Docker, error) {
+	ctx = td.StartStep(ctx, td.Props("setup docker").Infra())
+	defer td.EndStep(ctx)
+	// Create token source with scope for cloud registry (storage).
+	ts, err := auth_steps.Init(ctx, isLocal, auth.SCOPE_USERINFO_EMAIL, storage.ScopeReadOnly)
+	if err != nil {
+		return nil, td.FailStep(ctx, err)
+	}
+
+	return docker.New(ctx, ts)
+}
+
+func getAbsoluteOfRequiredFlag(ctx context.Context, nonEmptyPath, flag string) string {
+	if nonEmptyPath == "" {
+		td.Fatalf(ctx, "--%s must be specified", flag)
+	}
+	absPath, err := filepath.Abs(nonEmptyPath)
+	if err != nil {
+		td.Fatalf(ctx, "error with path %s - %s", nonEmptyPath, err)
+	}
+	return absPath
+}
+
+func extractOutput(ctx context.Context, workDir, outAbsPath string) error {
+	ctx = td.StartStep(ctx, td.Props("copy compiled JS and wasm into output"))
+	defer td.EndStep(ctx)
+
+	files, err := os_steps.ReadDir(ctx, workDir)
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrapf(err, "getting output from %s", workDir))
+	}
+
+	for _, f := range files {
+		name := f.Name()
+		if strings.Contains(name, "wasm_gm_tests") {
+			oldFile := filepath.Join(workDir, name)
+			newFile := filepath.Join(outAbsPath, name)
+			if err := os.Rename(oldFile, newFile); err != nil {
+				return td.FailStep(ctx, skerr.Wrapf(err, "copying %s to %s", oldFile, newFile))
+			}
+		}
+	}
+	return nil
+}
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index e33be2f..b868a14 100755
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -330,6 +330,11 @@
         "Build-Debian10-EMCC-wasm-Release-PathKit"
       ]
     },
+    "Build-Debian10-EMCC-wasm-Release-WasmGMTests": {
+      "tasks": [
+        "Build-Debian10-EMCC-wasm-Release-WasmGMTests"
+      ]
+    },
     "Build-Debian10-GCC-x86-Debug-Docker": {
       "tasks": [
         "Build-Debian10-GCC-x86-Debug-Docker"
@@ -8477,6 +8482,56 @@
       ],
       "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "Build-Debian10-EMCC-wasm-Release-WasmGMTests": {
+      "caches": [
+        {
+          "name": "docker",
+          "path": "cache/docker"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:08768c6d238082f3c552dcabef6aaf4c6792d91a"
+        }
+      ],
+      "command": [
+        "./compile_wasm_gm_tests",
+        "--project_id",
+        "skia-swarming-bots",
+        "--task_id",
+        "<(TASK_ID)",
+        "--task_name",
+        "Build-Debian10-EMCC-wasm-Release-WasmGMTests",
+        "--out_path",
+        "./wasm_out",
+        "--skia_path",
+        "./skia",
+        "--work_path",
+        "./cache/docker/wasm_gm",
+        "--alsologtostderr"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BuildTaskDrivers"
+      ],
+      "dimensions": [
+        "docker_installed:true",
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-standard-16",
+        "os:Debian-10.3",
+        "pool:Skia"
+      ],
+      "execution_timeout_ns": 1200000000000,
+      "io_timeout_ns": 1200000000000,
+      "isolate": "compile.isolate",
+      "max_attempts": 1,
+      "outputs": [
+        "wasm_out"
+      ],
+      "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "Build-Debian10-GCC-x86-Debug-Docker": {
       "caches": [
         {
diff --git a/infra/canvaskit/build_gmtests.sh b/infra/canvaskit/build_gmtests.sh
new file mode 100755
index 0000000..ba7e8bd
--- /dev/null
+++ b/infra/canvaskit/build_gmtests.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This assumes it is being run inside a docker container of emsdk-base
+# and a Skia checkout has been mounted at /SRC and the output directory
+# is mounted at /OUT
+
+set +e
+set -x
+# Clean out previous builds (ignoring any errors for things like folders)
+# (e.g. we don't want to delete /OUT/depot_tools/)
+rm -f /OUT/*
+set -e
+
+# BASE_DIR is the dir this script is in ($SKIA_ROOT/infra/canvaskit)
+BASE_DIR=`cd $(dirname ${BASH_SOURCE[0]}) && pwd`
+CANVASKIT_DIR=$BASE_DIR/../../modules/canvaskit
+
+BUILD_DIR=/OUT $CANVASKIT_DIR/compile_gm.sh $@
+
+# Make sure everybody can read and write the contents of /OUT
+chmod -R a+rw /OUT/*
+
diff --git a/modules/canvaskit/README.md b/modules/canvaskit/README.md
index fed7bd5..8294c61 100644
--- a/modules/canvaskit/README.md
+++ b/modules/canvaskit/README.md
@@ -133,6 +133,8 @@
   7. In `$SKIA_ROOT/infra/bots/recipe_modules/build/`, update `canvaskit.py`
      and `pathkit.py` to have `DOCKER_IMAGE` point to the desired tagged Docker
      containers from steps 2 and 5 (which should be the same).
+  8. In `$SKIA_ROOT/infra/bots/task_drivers/compile_wasm_gm_tests.go`, update dockerImage
+     to refer to the desired Docker containers from steps 2 and 5.
   9. In `$SKIA_ROOT/infra/bots/`, run `make train` to re-train the recipes.
   10. Optional: Run something like `git grep 1\\.38\\.` in `$SKIA_ROOT` to see if
      there are any other references that need updating.