blob: dca66d4317435b62b7ff0c9d911738ba50e655de [file] [log] [blame]
// 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 (
"bufio"
"bytes"
"flag"
"fmt"
"math/rand"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/util"
"go.skia.org/infra/task_driver/go/td"
)
func main() {
var (
projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
taskId = flag.String("task_id", "", "ID of this task.")
bot = flag.String("bot", "", "Name of the task.")
output = flag.String("o", "", "Dump JSON step data to the given file, or stdout if -.")
local = flag.Bool("local", true, "Running locally (else on the bots)?")
resources = flag.String("resources", "resources", "Passed to fm -i.")
script = flag.String("script", "", "File (or - for stdin) with one job per line.")
)
ctx := td.StartRun(projectId, taskId, bot, output, local)
defer td.EndRun(ctx)
actualStdout := os.Stdout
actualStderr := os.Stderr
verbosity := exec.Info
if *local {
// Task Driver echoes every exec.Run() stdout and stderr to the console,
// which makes it hard to find failures (especially stdout). Send them to /dev/null.
devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
td.Fatal(ctx, err)
}
os.Stdout = devnull
os.Stderr = devnull
// Having stifled stderr/stdout, changing Command.Verbose won't have any visible effect,
// but setting it to Silent will bypass a fair chunk of wasted formatting work.
verbosity = exec.Silent
}
if flag.NArg() < 1 {
td.Fatalf(ctx, "Please pass an fm binary.")
}
fm := flag.Arg(0)
// Run `fm <flag>` to find the names of all linked GMs or tests.
query := func(flag string) []string {
stdout := &bytes.Buffer{}
cmd := &exec.Command{Name: fm, Stdout: stdout, Verbose: verbosity}
cmd.Args = append(cmd.Args, "-i", *resources)
cmd.Args = append(cmd.Args, flag)
if err := exec.Run(ctx, cmd); err != nil {
td.Fatal(ctx, err)
}
lines := []string{}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
td.Fatal(ctx, err)
}
return lines
}
gms := query("--listGMs")
tests := query("--listTests")
// Query Gold for all known hashes when running as a bot.
known := map[string]bool{
"0832f708a97acc6da385446384647a8f": true, // MD5 of passing unit test.
}
if *bot != "" {
func() {
url := "https://storage.googleapis.com/skia-infra-gm/hash_files/gold-prod-hashes.txt"
resp, err := http.Get(url)
if err != nil {
td.Fatal(ctx, err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
known[scanner.Text()] = true
}
if err := scanner.Err(); err != nil {
td.Fatal(ctx, err)
}
fmt.Fprintf(actualStdout, "Gold knew %v unique hashes.\n", len(known))
}()
}
type Work struct {
Sources []string // Passed to FM -s: names of gms/tests, paths to image files, .skps, etc.
Flags []string // Other flags to pass to FM: --ct 565, --msaa 16, etc.
}
queue := make(chan Work, 1<<20) // Arbitrarily huge buffer to avoid ever blocking.
wg := &sync.WaitGroup{}
var failures int32 = 0
var worker func([]string, []string)
worker = func(sources, flags []string) {
defer wg.Done()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := &exec.Command{Name: fm, Stdout: stdout, Stderr: stderr, Verbose: verbosity}
cmd.Args = append(cmd.Args, "-i", *resources)
cmd.Args = append(cmd.Args, flags...)
cmd.Args = append(cmd.Args, "-s")
cmd.Args = append(cmd.Args, sources...)
// Run our FM command.
err := exec.Run(ctx, cmd)
// On success, scan stdout for any unknown hashes.
unknownHash := func() string {
if err == nil && *bot != "" { // We only fetch known hashes when using -bot.
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
if parts := strings.Fields(scanner.Text()); len(parts) == 3 {
md5 := parts[1]
if !known[md5] {
return md5
}
}
}
if err := scanner.Err(); err != nil {
td.Fatal(ctx, err)
}
}
return ""
}()
// If a batch failed or produced an unknown hash, isolate with individual reruns.
if len(sources) > 1 && (err != nil || unknownHash != "") {
wg.Add(len(sources))
for i := range sources {
worker(sources[i:i+1], flags)
}
return
}
// If an individual run failed, nothing more to do but fail.
if err != nil {
atomic.AddInt32(&failures, 1)
td.FailStep(ctx, err)
if *local {
lines := []string{}
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
td.Fatal(ctx, err)
}
fmt.Fprintf(actualStderr, "%v %v #failed:\n\t%v\n",
cmd.Name,
strings.Join(cmd.Args, " "),
strings.Join(lines, "\n\t"))
}
return
}
// If an individual run succeeded but produced an unknown hash, TODO upload .png to Gold.
// For now just print out the command and the hash it produced.
if unknownHash != "" {
fmt.Fprintf(actualStdout, "%v %v #%v\n",
cmd.Name,
strings.Join(cmd.Args, " "),
unknownHash)
}
}
for i := 0; i < runtime.NumCPU(); i++ {
go func() {
for w := range queue {
worker(w.Sources, w.Flags)
}
}()
}
// Get some work going, first breaking it into batches to increase our parallelism.
kickoff := func(sources, flags []string) {
if len(sources) == 0 {
return // A blank or commented job line from -script or the command line.
}
// Shuffle the sources randomly as a cheap way to approximate evenly expensive batches.
// (Intentionally not rand.Seed()'d to stay deterministically reproducible.)
rand.Shuffle(len(sources), func(i, j int) {
sources[i], sources[j] = sources[j], sources[i]
})
nbatches := runtime.NumCPU() // Arbitrary, nice to scale ~= cores.
batch := (len(sources) + nbatches - 1) / nbatches // Round up to avoid empty batches.
util.ChunkIter(len(sources), batch, func(start, end int) error {
wg.Add(1)
queue <- Work{sources[start:end], flags}
return nil
})
}
// Parse a job like "gms b=cpu ct=8888" into sources and flags for kickoff().
parse := func(job []string) (sources, flags []string) {
for _, token := range job {
// Everything after # is a comment.
if strings.HasPrefix(token, "#") {
break
}
// Treat "gm" or "gms" as a shortcut for all known GMs.
if token == "gm" || token == "gms" {
sources = append(sources, gms...)
continue
}
// Same for tests.
if token == "test" || token == "tests" {
sources = append(sources, tests...)
continue
}
// Is this a flag to pass through to FM?
if parts := strings.Split(token, "="); len(parts) == 2 {
f := "-"
if len(parts[0]) > 1 {
f += "-"
}
f += parts[0]
flags = append(flags, f, parts[1])
continue
}
// Anything else must be the name of a source for FM to run.
sources = append(sources, token)
}
return
}
// Parse one job from the command line, handy for ad hoc local runs.
kickoff(parse(flag.Args()[1:]))
// Any number of jobs can come from -script.
if *script != "" {
file := os.Stdin
if *script != "-" {
file, err := os.Open(*script)
if err != nil {
td.Fatal(ctx, err)
}
defer file.Close()
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
kickoff(parse(strings.Fields(scanner.Text())))
}
if err := scanner.Err(); err != nil {
td.Fatal(ctx, err)
}
}
// If we're a bot (or acting as if we are one), kick off its work.
if *bot != "" {
parts := strings.Split(*bot, "-")
model, CPU_or_GPU := parts[3], parts[4]
commonFlags := []string{
"--nativeFonts",
strconv.FormatBool(strings.Contains(*bot, "NativeFonts")),
}
run := func(sources []string, extraFlags string) {
kickoff(sources, append(strings.Fields(extraFlags), commonFlags...))
}
if CPU_or_GPU == "CPU" {
commonFlags = append(commonFlags, "-b", "cpu")
run(tests, "")
run(gms, "--ct 8888 --legacy") // Equivalent to DM --config 8888.
if model == "GCE" {
run(gms, "--ct g8 --legacy") // --config g8
run(gms, "--ct 565 --legacy") // --config 565
run(gms, "--ct 8888") // --config srgb
run(gms, "--ct f16") // --config esrgb
run(gms, "--ct f16 --tf linear") // --config f16
run(gms, "--ct 8888 --gamut p3") // --config p3
run(gms, "--ct 8888 --gamut narrow --tf 2.2") // --config narrow
run(gms, "--ct f16 --gamut rec2020 --tf rec2020") // --config erec2020
run(gms, "--skvm")
run(gms, "--skvm --ct f16")
}
// TODO: image/colorImage/svg tests
// TODO: pic-8888 equivalent?
// TODO: serialize-8888 equivalent?
}
}
wg.Wait()
if failures > 0 {
if *local {
// td.Fatalf() would work fine, but barfs up a panic that we don't need to see.
fmt.Fprintf(actualStderr, "%v runs of %v failed after retries.\n", failures, fm)
os.Exit(1)
} else {
td.Fatalf(ctx, "%v runs of %v failed after retries.", failures, fm)
}
}
}