[goldctl] Upload files in parallel

TBR=kjlubick
Change-Id: I34c2d6076db4aeddc16a6a23ebfbe588333b1439
Reviewed-on: https://skia-review.googlesource.com/c/183941
Reviewed-by: Stephan Altmueller <stephana@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Stephan Altmueller <stephana@google.com>
diff --git a/gold-client/go/goldclient/goldclient.go b/gold-client/go/goldclient/goldclient.go
index 0c3421f..c2d588a 100644
--- a/gold-client/go/goldclient/goldclient.go
+++ b/gold-client/go/goldclient/goldclient.go
@@ -29,6 +29,7 @@
 	"go.skia.org/infra/golden/go/shared"
 	"go.skia.org/infra/golden/go/types"
 	"golang.org/x/oauth2"
+	"golang.org/x/sync/errgroup"
 )
 
 const (
@@ -191,12 +192,16 @@
 		return false, err
 	}
 
+	var egroup errgroup.Group
 	// Check against known hashes and upload if needed.
 	if !c.resultState.KnownHashes[imgHash] {
-		gcsImagePath := c.resultState.getGCSImagePath(imgHash)
-		if err := uploader.uploadBytesOrFile(imgBytes, imgFileName, prefixGCS(gcsImagePath)); err != nil {
-			return false, skerr.Fmt("Error uploading image: %s", err)
-		}
+		egroup.Go(func() error {
+			gcsImagePath := c.resultState.getGCSImagePath(imgHash)
+			if err := uploader.uploadBytesOrFile(imgBytes, imgFileName, prefixGCS(gcsImagePath)); err != nil {
+				return skerr.Fmt("Error uploading image %s to %s. Got: %s", imgFileName, gcsImagePath, err)
+			}
+			return nil
+		})
 	}
 
 	// Add the result of this test.
@@ -208,16 +213,23 @@
 	}
 
 	// If we do per test pass/fail then upload the result and compare it to the baseline.
+	ret := true
 	if c.resultState.PerTestPassFail {
-		localFileName := filepath.Join(c.workDir, jsonTempFile)
-		if err := uploader.uploadJson(c.resultState.GoldResults, localFileName, c.resultState.getResultFilePath()); err != nil {
-			return false, err
-		}
-		return c.resultState.Expectations[name][imgHash] == types.POSITIVE, nil
+		egroup.Go(func() error {
+			localFileName := filepath.Join(c.workDir, jsonTempFile)
+			resultFilePath := c.resultState.getResultFilePath()
+			if err := uploader.uploadJson(c.resultState.GoldResults, localFileName, resultFilePath); err != nil {
+				return skerr.Fmt("Error uploading JSON file to GCS path %s: %s", resultFilePath, err)
+			}
+			return nil
+		})
+		ret = c.resultState.Expectations[name][imgHash] == types.POSITIVE
 	}
 
-	// If we don't do per-test pass/fail then return true.
-	return true, nil
+	if err := egroup.Wait(); err != nil {
+		return false, err
+	}
+	return ret, nil
 }
 
 // initResultState assembles the information that needs to be uploaded based on previous calls