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 5 package 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 15 import ( 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.) 35 var JSON_FILENAME = fmt.Sprintf("%s_browser_bench.json", uuid.New().String()) 36 37 var ( 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. 57 type 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. 65 var defaultKeys map[string]string 66 67 // contains all the results reported in through report_perf_data 68 var results map[string][]reportBody 69 70 type 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 83 func 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. 113 func 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. 142 func 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. 156 func 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