xref: /aosp_15_r20/external/skia/infra/lottiecap/gold/lottie-web-aggregator.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1*c8dee2aaSAndroid Build Coastguard Worker// Copyright 2018 The Chromium Authors. All rights reserved.
2*c8dee2aaSAndroid Build Coastguard Worker// Use of this source code is governed by a BSD-style license that can be
3*c8dee2aaSAndroid Build Coastguard Worker// found in the LICENSE file.
4*c8dee2aaSAndroid Build Coastguard Worker
5*c8dee2aaSAndroid Build Coastguard Workerpackage main
6*c8dee2aaSAndroid Build Coastguard Worker
7*c8dee2aaSAndroid Build Coastguard Worker// This server runs alongside lottiecap.js and istens for POST requests
8*c8dee2aaSAndroid Build Coastguard Worker// when any test case reports it has output for Gold.
9*c8dee2aaSAndroid Build Coastguard Worker
10*c8dee2aaSAndroid Build Coastguard Worker// TODO(kjlubick): Deduplicate with pathkit-aggregator
11*c8dee2aaSAndroid Build Coastguard Worker// TODO(kjlubick): Handle uninteresting_hash.txt if needed.
12*c8dee2aaSAndroid Build Coastguard Worker
13*c8dee2aaSAndroid Build Coastguard Workerimport (
14*c8dee2aaSAndroid Build Coastguard Worker	"bytes"
15*c8dee2aaSAndroid Build Coastguard Worker	"crypto/md5"
16*c8dee2aaSAndroid Build Coastguard Worker	"encoding/base64"
17*c8dee2aaSAndroid Build Coastguard Worker	"encoding/json"
18*c8dee2aaSAndroid Build Coastguard Worker	"flag"
19*c8dee2aaSAndroid Build Coastguard Worker	"fmt"
20*c8dee2aaSAndroid Build Coastguard Worker	"image"
21*c8dee2aaSAndroid Build Coastguard Worker	"image/png"
22*c8dee2aaSAndroid Build Coastguard Worker	"io/ioutil"
23*c8dee2aaSAndroid Build Coastguard Worker	"log"
24*c8dee2aaSAndroid Build Coastguard Worker	"net/http"
25*c8dee2aaSAndroid Build Coastguard Worker	"os"
26*c8dee2aaSAndroid Build Coastguard Worker	"path"
27*c8dee2aaSAndroid Build Coastguard Worker	"strings"
28*c8dee2aaSAndroid Build Coastguard Worker
29*c8dee2aaSAndroid Build Coastguard Worker	"go.skia.org/infra/golden/go/jsonio"
30*c8dee2aaSAndroid Build Coastguard Worker	"go.skia.org/infra/golden/go/types"
31*c8dee2aaSAndroid Build Coastguard Worker)
32*c8dee2aaSAndroid Build Coastguard Worker
33*c8dee2aaSAndroid Build Coastguard Worker// This allows us to use upload_dm_results.py out of the box
34*c8dee2aaSAndroid Build Coastguard Workerconst JSON_FILENAME = "dm.json"
35*c8dee2aaSAndroid Build Coastguard Worker
36*c8dee2aaSAndroid Build Coastguard Workervar (
37*c8dee2aaSAndroid Build Coastguard Worker	outDir = flag.String("out_dir", "/OUT/", "location to dump the Gold JSON and pngs")
38*c8dee2aaSAndroid Build Coastguard Worker	port   = flag.String("port", "8081", "Port to listen on.")
39*c8dee2aaSAndroid Build Coastguard Worker
40*c8dee2aaSAndroid Build Coastguard Worker	browser       = flag.String("browser", "Chrome", "Browser Key")
41*c8dee2aaSAndroid Build Coastguard Worker	buildBucketID = flag.String("buildbucket_build_id", "", "Buildbucket build id key")
42*c8dee2aaSAndroid Build Coastguard Worker	builder       = flag.String("builder", "", "Builder, like 'Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit'")
43*c8dee2aaSAndroid Build Coastguard Worker	renderer      = flag.String("renderer", "lottie-web", "e.g. lottie-web or skottie")
44*c8dee2aaSAndroid Build Coastguard Worker	config        = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key")
45*c8dee2aaSAndroid Build Coastguard Worker	gitHash       = flag.String("git_hash", "-", "The git commit hash of the version being tested")
46*c8dee2aaSAndroid Build Coastguard Worker	hostOS        = flag.String("host_os", "Debian9", "OS Key")
47*c8dee2aaSAndroid Build Coastguard Worker	issue         = flag.String("issue", "", "ChangeListID (if tryjob)")
48*c8dee2aaSAndroid Build Coastguard Worker	patchset      = flag.Int("patchset", 0, "patchset (if tryjob)")
49*c8dee2aaSAndroid Build Coastguard Worker	taskId        = flag.String("task_id", "", "Skia task id")
50*c8dee2aaSAndroid Build Coastguard Worker)
51*c8dee2aaSAndroid Build Coastguard Worker
52*c8dee2aaSAndroid Build Coastguard Worker// reportBody is the JSON recieved from the JS side. It represents
53*c8dee2aaSAndroid Build Coastguard Worker// exactly one unique Gold image/test.
54*c8dee2aaSAndroid Build Coastguard Workertype reportBody struct {
55*c8dee2aaSAndroid Build Coastguard Worker	// a base64 encoded PNG image.
56*c8dee2aaSAndroid Build Coastguard Worker	Data string `json:"data"`
57*c8dee2aaSAndroid Build Coastguard Worker	// a name describing the test. Should be unique enough to allow use of grep.
58*c8dee2aaSAndroid Build Coastguard Worker	TestName string `json:"test_name"`
59*c8dee2aaSAndroid Build Coastguard Worker}
60*c8dee2aaSAndroid Build Coastguard Worker
61*c8dee2aaSAndroid Build Coastguard Worker// The keys to be used at the top level for all Results.
62*c8dee2aaSAndroid Build Coastguard Workervar defaultKeys map[string]string
63*c8dee2aaSAndroid Build Coastguard Worker
64*c8dee2aaSAndroid Build Coastguard Worker// contains all the results reported in through report_gold_data
65*c8dee2aaSAndroid Build Coastguard Workervar results []jsonio.Result
66*c8dee2aaSAndroid Build Coastguard Worker
67*c8dee2aaSAndroid Build Coastguard Workerfunc main() {
68*c8dee2aaSAndroid Build Coastguard Worker	flag.Parse()
69*c8dee2aaSAndroid Build Coastguard Worker
70*c8dee2aaSAndroid Build Coastguard Worker	defaultKeys = map[string]string{
71*c8dee2aaSAndroid Build Coastguard Worker		"browser":          *browser,
72*c8dee2aaSAndroid Build Coastguard Worker		"renderer":         *renderer,
73*c8dee2aaSAndroid Build Coastguard Worker		"configuration":    *config,
74*c8dee2aaSAndroid Build Coastguard Worker		"cpu_or_gpu":       "CPU",
75*c8dee2aaSAndroid Build Coastguard Worker		"cpu_or_gpu_value": "Browser",
76*c8dee2aaSAndroid Build Coastguard Worker		"os":               *hostOS,
77*c8dee2aaSAndroid Build Coastguard Worker		"source_type":      "lottie",
78*c8dee2aaSAndroid Build Coastguard Worker	}
79*c8dee2aaSAndroid Build Coastguard Worker
80*c8dee2aaSAndroid Build Coastguard Worker	results = []jsonio.Result{}
81*c8dee2aaSAndroid Build Coastguard Worker
82*c8dee2aaSAndroid Build Coastguard Worker	http.HandleFunc("/report_gold_data", reporter)
83*c8dee2aaSAndroid Build Coastguard Worker	http.HandleFunc("/dump_json", dumpJSON)
84*c8dee2aaSAndroid Build Coastguard Worker
85*c8dee2aaSAndroid Build Coastguard Worker	fmt.Printf("Waiting for gold ingestion on port %s\n", *port)
86*c8dee2aaSAndroid Build Coastguard Worker
87*c8dee2aaSAndroid Build Coastguard Worker	log.Fatal(http.ListenAndServe(":"+*port, nil))
88*c8dee2aaSAndroid Build Coastguard Worker}
89*c8dee2aaSAndroid Build Coastguard Worker
90*c8dee2aaSAndroid Build Coastguard Worker// reporter handles when the client reports a test has Gold output.
91*c8dee2aaSAndroid Build Coastguard Worker// It writes the corresponding PNG to disk and appends a Result, assuming
92*c8dee2aaSAndroid Build Coastguard Worker// no errors.
93*c8dee2aaSAndroid Build Coastguard Workerfunc reporter(w http.ResponseWriter, r *http.Request) {
94*c8dee2aaSAndroid Build Coastguard Worker	if r.Method != "POST" {
95*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Only POST accepted", 400)
96*c8dee2aaSAndroid Build Coastguard Worker		return
97*c8dee2aaSAndroid Build Coastguard Worker	}
98*c8dee2aaSAndroid Build Coastguard Worker	defer r.Body.Close()
99*c8dee2aaSAndroid Build Coastguard Worker
100*c8dee2aaSAndroid Build Coastguard Worker	body, err := ioutil.ReadAll(r.Body)
101*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
102*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Malformed body", 400)
103*c8dee2aaSAndroid Build Coastguard Worker		return
104*c8dee2aaSAndroid Build Coastguard Worker	}
105*c8dee2aaSAndroid Build Coastguard Worker
106*c8dee2aaSAndroid Build Coastguard Worker	testOutput := reportBody{}
107*c8dee2aaSAndroid Build Coastguard Worker	if err := json.Unmarshal(body, &testOutput); err != nil {
108*c8dee2aaSAndroid Build Coastguard Worker		fmt.Println(err)
109*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Could not unmarshal JSON", 400)
110*c8dee2aaSAndroid Build Coastguard Worker		return
111*c8dee2aaSAndroid Build Coastguard Worker	}
112*c8dee2aaSAndroid Build Coastguard Worker
113*c8dee2aaSAndroid Build Coastguard Worker	hash := ""
114*c8dee2aaSAndroid Build Coastguard Worker	if hash, err = writeBase64EncodedPNG(testOutput.Data); err != nil {
115*c8dee2aaSAndroid Build Coastguard Worker		fmt.Println(err)
116*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Could not write image to disk", 500)
117*c8dee2aaSAndroid Build Coastguard Worker		return
118*c8dee2aaSAndroid Build Coastguard Worker	}
119*c8dee2aaSAndroid Build Coastguard Worker
120*c8dee2aaSAndroid Build Coastguard Worker	if _, err := w.Write([]byte("Accepted")); err != nil {
121*c8dee2aaSAndroid Build Coastguard Worker		fmt.Printf("Could not write response: %s\n", err)
122*c8dee2aaSAndroid Build Coastguard Worker		return
123*c8dee2aaSAndroid Build Coastguard Worker	}
124*c8dee2aaSAndroid Build Coastguard Worker
125*c8dee2aaSAndroid Build Coastguard Worker	results = append(results, jsonio.Result{
126*c8dee2aaSAndroid Build Coastguard Worker		Digest: types.Digest(hash),
127*c8dee2aaSAndroid Build Coastguard Worker		Key: map[string]string{
128*c8dee2aaSAndroid Build Coastguard Worker			"name": testOutput.TestName,
129*c8dee2aaSAndroid Build Coastguard Worker		},
130*c8dee2aaSAndroid Build Coastguard Worker		Options: map[string]string{
131*c8dee2aaSAndroid Build Coastguard Worker			"ext": "png",
132*c8dee2aaSAndroid Build Coastguard Worker		},
133*c8dee2aaSAndroid Build Coastguard Worker	})
134*c8dee2aaSAndroid Build Coastguard Worker}
135*c8dee2aaSAndroid Build Coastguard Worker
136*c8dee2aaSAndroid Build Coastguard Worker// createOutputFile creates a file and set permissions correctly.
137*c8dee2aaSAndroid Build Coastguard Workerfunc createOutputFile(p string) (*os.File, error) {
138*c8dee2aaSAndroid Build Coastguard Worker	outputFile, err := os.Create(p)
139*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
140*c8dee2aaSAndroid Build Coastguard Worker		return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err)
141*c8dee2aaSAndroid Build Coastguard Worker	}
142*c8dee2aaSAndroid Build Coastguard Worker	// Make this accessible (and deletable) by all users
143*c8dee2aaSAndroid Build Coastguard Worker	if err = outputFile.Chmod(0666); err != nil {
144*c8dee2aaSAndroid Build Coastguard Worker		return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err)
145*c8dee2aaSAndroid Build Coastguard Worker	}
146*c8dee2aaSAndroid Build Coastguard Worker	return outputFile, nil
147*c8dee2aaSAndroid Build Coastguard Worker}
148*c8dee2aaSAndroid Build Coastguard Worker
149*c8dee2aaSAndroid Build Coastguard Worker// dumpJSON writes out a JSON file with all the results, typically at the end of
150*c8dee2aaSAndroid Build Coastguard Worker// all the tests.
151*c8dee2aaSAndroid Build Coastguard Workerfunc dumpJSON(w http.ResponseWriter, r *http.Request) {
152*c8dee2aaSAndroid Build Coastguard Worker	if r.Method != "POST" {
153*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Only POST accepted", 400)
154*c8dee2aaSAndroid Build Coastguard Worker		return
155*c8dee2aaSAndroid Build Coastguard Worker	}
156*c8dee2aaSAndroid Build Coastguard Worker
157*c8dee2aaSAndroid Build Coastguard Worker	p := path.Join(*outDir, JSON_FILENAME)
158*c8dee2aaSAndroid Build Coastguard Worker	outputFile, err := createOutputFile(p)
159*c8dee2aaSAndroid Build Coastguard Worker	defer outputFile.Close()
160*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
161*c8dee2aaSAndroid Build Coastguard Worker		fmt.Println(err)
162*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Could not open json file on disk", 500)
163*c8dee2aaSAndroid Build Coastguard Worker		return
164*c8dee2aaSAndroid Build Coastguard Worker	}
165*c8dee2aaSAndroid Build Coastguard Worker
166*c8dee2aaSAndroid Build Coastguard Worker	results := jsonio.GoldResults{
167*c8dee2aaSAndroid Build Coastguard Worker		ChangelistID:                *issue,
168*c8dee2aaSAndroid Build Coastguard Worker		GitHash:                     *gitHash,
169*c8dee2aaSAndroid Build Coastguard Worker		CodeReviewSystem:            "gerrit",
170*c8dee2aaSAndroid Build Coastguard Worker		Key:                         defaultKeys,
171*c8dee2aaSAndroid Build Coastguard Worker		PatchsetOrder:               *patchset,
172*c8dee2aaSAndroid Build Coastguard Worker		Results:                     results,
173*c8dee2aaSAndroid Build Coastguard Worker		TryJobID:                    *buildBucketID,
174*c8dee2aaSAndroid Build Coastguard Worker		ContinuousIntegrationSystem: "buildbucket",
175*c8dee2aaSAndroid Build Coastguard Worker	}
176*c8dee2aaSAndroid Build Coastguard Worker
177*c8dee2aaSAndroid Build Coastguard Worker	enc := json.NewEncoder(outputFile)
178*c8dee2aaSAndroid Build Coastguard Worker	enc.SetIndent("", "  ") // Make it human readable.
179*c8dee2aaSAndroid Build Coastguard Worker	if err := enc.Encode(&results); err != nil {
180*c8dee2aaSAndroid Build Coastguard Worker		fmt.Println(err)
181*c8dee2aaSAndroid Build Coastguard Worker		http.Error(w, "Could not write json to disk", 500)
182*c8dee2aaSAndroid Build Coastguard Worker		return
183*c8dee2aaSAndroid Build Coastguard Worker	}
184*c8dee2aaSAndroid Build Coastguard Worker	fmt.Println("JSON Written")
185*c8dee2aaSAndroid Build Coastguard Worker}
186*c8dee2aaSAndroid Build Coastguard Worker
187*c8dee2aaSAndroid Build Coastguard Worker// writeBase64EncodedPNG writes a PNG to disk and returns the md5 of the
188*c8dee2aaSAndroid Build Coastguard Worker// decoded PNG bytes and any error. This hash is what will be used as
189*c8dee2aaSAndroid Build Coastguard Worker// the gold digest and the file name.
190*c8dee2aaSAndroid Build Coastguard Workerfunc writeBase64EncodedPNG(data string) (string, error) {
191*c8dee2aaSAndroid Build Coastguard Worker	// data starts with something like data:image/png;base64,[data]
192*c8dee2aaSAndroid Build Coastguard Worker	// https://en.wikipedia.org/wiki/Data_URI_scheme
193*c8dee2aaSAndroid Build Coastguard Worker	start := strings.Index(data, ",")
194*c8dee2aaSAndroid Build Coastguard Worker	b := bytes.NewBufferString(data[start+1:])
195*c8dee2aaSAndroid Build Coastguard Worker	pngReader := base64.NewDecoder(base64.StdEncoding, b)
196*c8dee2aaSAndroid Build Coastguard Worker
197*c8dee2aaSAndroid Build Coastguard Worker	pngBytes, err := ioutil.ReadAll(pngReader)
198*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
199*c8dee2aaSAndroid Build Coastguard Worker		return "", fmt.Errorf("Could not decode base 64 encoding %s", err)
200*c8dee2aaSAndroid Build Coastguard Worker	}
201*c8dee2aaSAndroid Build Coastguard Worker
202*c8dee2aaSAndroid Build Coastguard Worker	// compute the hash of the pixel values, like DM does
203*c8dee2aaSAndroid Build Coastguard Worker	img, err := png.Decode(bytes.NewBuffer(pngBytes))
204*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
205*c8dee2aaSAndroid Build Coastguard Worker		return "", fmt.Errorf("Not a valid png: %s", err)
206*c8dee2aaSAndroid Build Coastguard Worker	}
207*c8dee2aaSAndroid Build Coastguard Worker	hash := ""
208*c8dee2aaSAndroid Build Coastguard Worker	switch img.(type) {
209*c8dee2aaSAndroid Build Coastguard Worker	case *image.NRGBA:
210*c8dee2aaSAndroid Build Coastguard Worker		i := img.(*image.NRGBA)
211*c8dee2aaSAndroid Build Coastguard Worker		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
212*c8dee2aaSAndroid Build Coastguard Worker	case *image.RGBA:
213*c8dee2aaSAndroid Build Coastguard Worker		i := img.(*image.RGBA)
214*c8dee2aaSAndroid Build Coastguard Worker		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
215*c8dee2aaSAndroid Build Coastguard Worker	case *image.RGBA64:
216*c8dee2aaSAndroid Build Coastguard Worker		i := img.(*image.RGBA64)
217*c8dee2aaSAndroid Build Coastguard Worker		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
218*c8dee2aaSAndroid Build Coastguard Worker	default:
219*c8dee2aaSAndroid Build Coastguard Worker		return "", fmt.Errorf("Unknown type of image")
220*c8dee2aaSAndroid Build Coastguard Worker	}
221*c8dee2aaSAndroid Build Coastguard Worker
222*c8dee2aaSAndroid Build Coastguard Worker	p := path.Join(*outDir, hash+".png")
223*c8dee2aaSAndroid Build Coastguard Worker	outputFile, err := createOutputFile(p)
224*c8dee2aaSAndroid Build Coastguard Worker	defer outputFile.Close()
225*c8dee2aaSAndroid Build Coastguard Worker	if err != nil {
226*c8dee2aaSAndroid Build Coastguard Worker		return "", fmt.Errorf("Could not create png file %s: %s", p, err)
227*c8dee2aaSAndroid Build Coastguard Worker	}
228*c8dee2aaSAndroid Build Coastguard Worker	if _, err = outputFile.Write(pngBytes); err != nil {
229*c8dee2aaSAndroid Build Coastguard Worker		return "", fmt.Errorf("Could not write to file %s: %s", p, err)
230*c8dee2aaSAndroid Build Coastguard Worker	}
231*c8dee2aaSAndroid Build Coastguard Worker	return hash, nil
232*c8dee2aaSAndroid Build Coastguard Worker}
233