1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package main
6
7// This server runs along side the karma tests and listens for POST requests
8// when any test case reports it has output for Perf. See perfReporter.js
9// for the browser side part.
10
11// Unlike the gold ingester, the perf ingester allows multiple reports
12// of the same benchmark and will output the average of these results
13// on a call to dump
14
15import (
16	"encoding/json"
17	"flag"
18	"fmt"
19	"io/ioutil"
20	"log"
21	"net/http"
22	"os"
23	"path"
24	"strconv"
25	"strings"
26
27	"github.com/google/uuid"
28	"go.skia.org/infra/perf/go/ingestcommon"
29)
30
31// upload_nano_results looks for anything*.json
32// We add the random UUID to avoid name clashes when uploading to
33// the perf bucket (which uploads to folders based on Month/Day/Hour, which can
34// easily have duplication if multiple perf tasks run in an hour.)
35var JSON_FILENAME = fmt.Sprintf("%s_browser_bench.json", uuid.New().String())
36
37var (
38	outDir = flag.String("out_dir", "/OUT/", "location to dump the Perf JSON")
39	port   = flag.String("port", "8081", "Port to listen on.")
40
41	botId            = flag.String("bot_id", "", "swarming bot id")
42	browser          = flag.String("browser", "Chrome", "Browser Key")
43	buildBucketID    = flag.Int64("buildbucket_build_id", 0, "Buildbucket build id key")
44	builder          = flag.String("builder", "", "Builder, like 'Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit'")
45	compiledLanguage = flag.String("compiled_language", "wasm", "wasm or asm.js")
46	config           = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key")
47	gitHash          = flag.String("git_hash", "-", "The git commit hash of the version being tested")
48	hostOS           = flag.String("host_os", "Debian9", "OS Key")
49	issue            = flag.Int64("issue", 0, "issue (if tryjob)")
50	patch_storage    = flag.String("patch_storage", "", "patch storage (if tryjob)")
51	patchset         = flag.Int64("patchset", 0, "patchset (if tryjob)")
52	taskId           = flag.String("task_id", "", "swarming task id")
53	sourceType       = flag.String("source_type", "pathkit", "Gold Source type, like pathkit,canvaskit")
54)
55
56// Received from the JS side.
57type reportBody struct {
58	// a name describing the benchmark. Should be unique enough to allow use of grep.
59	BenchName string `json:"bench_name"`
60	// The number of microseconds of the task.
61	TimeMicroSeconds float64 `json:"time_us"`
62}
63
64// The keys to be used at the top level for all Results.
65var defaultKeys map[string]string
66
67// contains all the results reported in through report_perf_data
68var results map[string][]reportBody
69
70type BenchData struct {
71	Hash         string                               `json:"gitHash"`
72	Issue        string                               `json:"issue"`
73	PatchSet     string                               `json:"patchset"`
74	Key          map[string]string                    `json:"key"`
75	Options      map[string]string                    `json:"options,omitempty"`
76	Results      map[string]ingestcommon.BenchResults `json:"results"`
77	PatchStorage string                               `json:"patch_storage,omitempty"`
78
79	SwarmingTaskID string `json:"swarming_task_id,omitempty"`
80	SwarmingBotID  string `json:"swarming_bot_id,omitempty"`
81}
82
83func main() {
84	flag.Parse()
85
86	cpuGPU := "CPU"
87	if strings.Index(*builder, "-GPU-") != -1 {
88		cpuGPU = "GPU"
89	}
90	defaultKeys = map[string]string{
91		"arch":              "WASM",
92		"browser":           *browser,
93		"compiled_language": *compiledLanguage,
94		"compiler":          "emsdk",
95		"configuration":     *config,
96		"cpu_or_gpu":        cpuGPU,
97		"cpu_or_gpu_value":  "Browser",
98		"os":                *hostOS,
99		"source_type":       *sourceType,
100	}
101
102	results = make(map[string][]reportBody)
103
104	http.HandleFunc("/report_perf_data", reporter)
105	http.HandleFunc("/dump_json", dumpJSON)
106
107	fmt.Printf("Waiting for perf ingestion on port %s\n", *port)
108
109	log.Fatal(http.ListenAndServe(":"+*port, nil))
110}
111
112// reporter handles when the client reports a test has a benchmark.
113func reporter(w http.ResponseWriter, r *http.Request) {
114	if r.Method != "POST" {
115		http.Error(w, "Only POST accepted", 400)
116		return
117	}
118	defer r.Body.Close()
119
120	body, err := ioutil.ReadAll(r.Body)
121	if err != nil {
122		http.Error(w, "Malformed body", 400)
123		return
124	}
125
126	benchOutput := reportBody{}
127	if err := json.Unmarshal(body, &benchOutput); err != nil {
128		fmt.Println(err)
129		http.Error(w, "Could not unmarshal JSON", 400)
130		return
131	}
132
133	if _, err := w.Write([]byte("Accepted")); err != nil {
134		fmt.Printf("Could not write response: %s\n", err)
135		return
136	}
137
138	results[benchOutput.BenchName] = append(results[benchOutput.BenchName], benchOutput)
139}
140
141// createOutputFile creates a file and set permissions correctly.
142func createOutputFile(p string) (*os.File, error) {
143	outputFile, err := os.Create(p)
144	if err != nil {
145		return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err)
146	}
147	// Make this accessible (and deletable) by all users
148	if err = outputFile.Chmod(0666); err != nil {
149		return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err)
150	}
151	return outputFile, nil
152}
153
154// dumpJSON writes out a JSON file with all the results, typically at the end of
155// all the tests. If there is more than one result per benchmark, we report the average.
156func dumpJSON(w http.ResponseWriter, r *http.Request) {
157	if r.Method != "POST" {
158		http.Error(w, "Only POST accepted", 400)
159		return
160	}
161
162	p := path.Join(*outDir, JSON_FILENAME)
163	outputFile, err := createOutputFile(p)
164	defer outputFile.Close()
165	if err != nil {
166		fmt.Println(err)
167		http.Error(w, "Could not open json file on disk", 500)
168		return
169	}
170
171	benchData := BenchData{
172		Hash:           *gitHash,
173		Issue:          strconv.FormatInt(*issue, 10),
174		PatchStorage:   *patch_storage,
175		PatchSet:       strconv.FormatInt(*patchset, 10),
176		Key:            defaultKeys,
177		SwarmingBotID:  *botId,
178		SwarmingTaskID: *taskId,
179	}
180
181	allResults := make(map[string]ingestcommon.BenchResults)
182	for name, benches := range results {
183		samples := []float64{}
184		total := float64(0)
185		for _, t := range benches {
186			samples = append(samples, t.TimeMicroSeconds)
187			total += t.TimeMicroSeconds
188		}
189		allResults[name] = map[string]ingestcommon.BenchResult{
190			"default": map[string]interface{}{
191				"average_us": total / float64(len(benches)),
192				"samples":    samples,
193			},
194		}
195	}
196	benchData.Results = allResults
197
198	enc := json.NewEncoder(outputFile)
199	enc.SetIndent("", "  ") // Make it human readable.
200	if err := enc.Encode(&benchData); err != nil {
201		fmt.Println(err)
202		http.Error(w, "Could not write json to disk", 500)
203		return
204	}
205	fmt.Println("JSON Written")
206}
207