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