| OLD | NEW |
| 1 package main | 1 package main |
| 2 | 2 |
| 3 import ( | 3 import ( |
| 4 "encoding/json" | 4 "encoding/json" |
| 5 "fmt" | 5 "fmt" |
| 6 "html/template" |
| 6 "net/http" | 7 "net/http" |
| 7 "net/url" | 8 "net/url" |
| 8 "path/filepath" | 9 "path/filepath" |
| 9 "regexp" | 10 "regexp" |
| 10 "runtime" | 11 "runtime" |
| 11 "sort" | 12 "sort" |
| 12 "strconv" | 13 "strconv" |
| 13 » "text/template" | 14 » "strings" |
| 14 "time" | 15 "time" |
| 15 | 16 |
| 16 "github.com/gorilla/mux" | 17 "github.com/gorilla/mux" |
| 17 "github.com/skia-dev/glog" | 18 "github.com/skia-dev/glog" |
| 18 "go.skia.org/infra/go/human" | 19 "go.skia.org/infra/go/human" |
| 19 "go.skia.org/infra/go/login" | 20 "go.skia.org/infra/go/login" |
| 20 "go.skia.org/infra/go/timer" | 21 "go.skia.org/infra/go/timer" |
| 21 "go.skia.org/infra/go/util" | 22 "go.skia.org/infra/go/util" |
| 22 "go.skia.org/infra/golden/go/blame" | 23 "go.skia.org/infra/golden/go/blame" |
| 23 "go.skia.org/infra/golden/go/diff" | 24 "go.skia.org/infra/golden/go/diff" |
| (...skipping 13 matching lines...) Expand all Loading... |
| 37 // Maximum page size used for pagination. | 38 // Maximum page size used for pagination. |
| 38 MAX_PAGE_SIZE = 100 | 39 MAX_PAGE_SIZE = 100 |
| 39 ) | 40 ) |
| 40 | 41 |
| 41 var ( | 42 var ( |
| 42 templates *template.Template | 43 templates *template.Template |
| 43 ) | 44 ) |
| 44 | 45 |
| 45 func loadTemplates() { | 46 func loadTemplates() { |
| 46 templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles
( | 47 templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles
( |
| 48 filepath.Join(*resourcesDir, "templates/blamelist.html"), |
| 49 filepath.Join(*resourcesDir, "templates/byblame.html"), |
| 47 filepath.Join(*resourcesDir, "templates/index.html"), | 50 filepath.Join(*resourcesDir, "templates/index.html"), |
| 48 filepath.Join(*resourcesDir, "templates/ignores.html"), | 51 filepath.Join(*resourcesDir, "templates/ignores.html"), |
| 49 filepath.Join(*resourcesDir, "templates/compare.html"), | 52 filepath.Join(*resourcesDir, "templates/compare.html"), |
| 50 filepath.Join(*resourcesDir, "templates/single.html"), | 53 filepath.Join(*resourcesDir, "templates/single.html"), |
| 51 filepath.Join(*resourcesDir, "templates/diff.html"), | 54 filepath.Join(*resourcesDir, "templates/diff.html"), |
| 52 filepath.Join(*resourcesDir, "templates/help.html"), | 55 filepath.Join(*resourcesDir, "templates/help.html"), |
| 53 filepath.Join(*resourcesDir, "templates/triagelog.html"), | 56 filepath.Join(*resourcesDir, "templates/triagelog.html"), |
| 54 filepath.Join(*resourcesDir, "templates/search.html"), | 57 filepath.Join(*resourcesDir, "templates/search.html"), |
| 55 // Sub templates used by other templates. | 58 // Sub templates used by other templates. |
| 56 filepath.Join(*resourcesDir, "templates/titlebar.html"), | 59 filepath.Join(*resourcesDir, "templates/titlebar.html"), |
| (...skipping 791 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 848 left := r.Form.Get("left") | 851 left := r.Form.Get("left") |
| 849 if top == "" || left == "" { | 852 if top == "" || left == "" { |
| 850 util.ReportError(w, r, fmt.Errorf("Missing the top or left query
parameter: %s %s", top, left), "No digests specified.") | 853 util.ReportError(w, r, fmt.Errorf("Missing the top or left query
parameter: %s %s", top, left), "No digests specified.") |
| 851 return | 854 return |
| 852 } | 855 } |
| 853 test := r.Form.Get("test") | 856 test := r.Form.Get("test") |
| 854 if test == "" { | 857 if test == "" { |
| 855 util.ReportError(w, r, fmt.Errorf("Missing the test query parame
ter."), "No test name specified.") | 858 util.ReportError(w, r, fmt.Errorf("Missing the test query parame
ter."), "No test name specified.") |
| 856 return | 859 return |
| 857 } | 860 } |
| 861 |
| 858 exp, err := storages.ExpectationsStore.Get() | 862 exp, err := storages.ExpectationsStore.Get() |
| 859 if err != nil { | 863 if err != nil { |
| 860 util.ReportError(w, r, err, "Failed to load expectations.") | 864 util.ReportError(w, r, err, "Failed to load expectations.") |
| 861 return | 865 return |
| 862 } | 866 } |
| 863 | 867 |
| 864 » ret := PolyDetailsGUI{ | 868 » ret := buildDetailsGUI(tile, exp, test, top, left, r.Form.Get("graphs")
== "true", r.Form.Get("closest") == "true") |
| 869 |
| 870 » w.Header().Set("Content-Type", "application/json") |
| 871 » enc := json.NewEncoder(w) |
| 872 » if err := enc.Encode(ret); err != nil { |
| 873 » » glog.Errorf("Failed to write or encode result: %s", err) |
| 874 » } |
| 875 } |
| 876 |
| 877 func buildDetailsGUI(tile *ptypes.Tile, exp *expstorage.Expectations, test strin
g, top string, left string, graphs bool, closest bool) *PolyDetailsGUI { |
| 878 » ret := &PolyDetailsGUI{ |
| 865 TopStatus: exp.Classification(test, top).String(), | 879 TopStatus: exp.Classification(test, top).String(), |
| 866 LeftStatus: exp.Classification(test, left).String(), | 880 LeftStatus: exp.Classification(test, left).String(), |
| 867 Params: []*PerParamCompare{}, | 881 Params: []*PerParamCompare{}, |
| 868 Traces: []*Trace{}, | 882 Traces: []*Trace{}, |
| 869 TileSize: len(tile.Commits), | 883 TileSize: len(tile.Commits), |
| 870 } | 884 } |
| 871 | 885 |
| 872 topParamSet := map[string][]string{} | 886 topParamSet := map[string][]string{} |
| 873 leftParamSet := map[string][]string{} | 887 leftParamSet := map[string][]string{} |
| 874 | 888 |
| (...skipping 20 matching lines...) Expand all Loading... |
| 895 sort.Strings(keys) | 909 sort.Strings(keys) |
| 896 for _, k := range keys { | 910 for _, k := range keys { |
| 897 ret.Params = append(ret.Params, &PerParamCompare{ | 911 ret.Params = append(ret.Params, &PerParamCompare{ |
| 898 Name: k, | 912 Name: k, |
| 899 Top: safeGet(topParamSet, k), | 913 Top: safeGet(topParamSet, k), |
| 900 Left: safeGet(leftParamSet, k), | 914 Left: safeGet(leftParamSet, k), |
| 901 }) | 915 }) |
| 902 } | 916 } |
| 903 | 917 |
| 904 // Now build the trace data. | 918 // Now build the trace data. |
| 905 » if r.Form.Get("graphs") == "true" { | 919 » if graphs { |
| 906 ret.Traces, ret.OtherDigests = buildTraceData(top, traceNames, t
ile, tally, exp) | 920 ret.Traces, ret.OtherDigests = buildTraceData(top, traceNames, t
ile, tally, exp) |
| 907 ret.Commits = tile.Commits | 921 ret.Commits = tile.Commits |
| 908 ret.Blame = blamer.GetBlame(test, top, ret.Commits) | 922 ret.Blame = blamer.GetBlame(test, top, ret.Commits) |
| 909 } | 923 } |
| 910 | 924 |
| 911 // Now find the closest positive and negative digests. | 925 // Now find the closest positive and negative digests. |
| 912 » if r.Form.Get("closest") == "true" { | 926 » if closest { |
| 913 ret.PosClosest = digesttools.ClosestDigest(test, top, exp, stora
ges.DiffStore, types.POSITIVE) | 927 ret.PosClosest = digesttools.ClosestDigest(test, top, exp, stora
ges.DiffStore, types.POSITIVE) |
| 914 ret.NegClosest = digesttools.ClosestDigest(test, top, exp, stora
ges.DiffStore, types.NEGATIVE) | 928 ret.NegClosest = digesttools.ClosestDigest(test, top, exp, stora
ges.DiffStore, types.NEGATIVE) |
| 915 } | 929 } |
| 916 | 930 |
| 917 » w.Header().Set("Content-Type", "application/json") | 931 » return ret |
| 918 » enc := json.NewEncoder(w) | |
| 919 » if err := enc.Encode(ret); err != nil { | |
| 920 » » glog.Errorf("Failed to write or encode result: %s", err) | |
| 921 » } | |
| 922 } | 932 } |
| 923 | 933 |
| 924 // digestIndex returns the index of the digest d in digestInfo, or -1 if not fou
nd. | 934 // digestIndex returns the index of the digest d in digestInfo, or -1 if not fou
nd. |
| 925 func digestIndex(d string, digestInfo []*DigestStatus) int { | 935 func digestIndex(d string, digestInfo []*DigestStatus) int { |
| 926 for i, di := range digestInfo { | 936 for i, di := range digestInfo { |
| 927 if di.Digest == d { | 937 if di.Digest == d { |
| 928 return i | 938 return i |
| 929 } | 939 } |
| 930 } | 940 } |
| 931 return -1 | 941 return -1 |
| (...skipping 106 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1038 } | 1048 } |
| 1039 } | 1049 } |
| 1040 } | 1050 } |
| 1041 | 1051 |
| 1042 // polyStatusHandler returns the current status of with respect to | 1052 // polyStatusHandler returns the current status of with respect to |
| 1043 // HEAD. | 1053 // HEAD. |
| 1044 func polyStatusHandler(w http.ResponseWriter, r *http.Request) { | 1054 func polyStatusHandler(w http.ResponseWriter, r *http.Request) { |
| 1045 sendJsonResponse(w, statusWatcher.GetStatus()) | 1055 sendJsonResponse(w, statusWatcher.GetStatus()) |
| 1046 } | 1056 } |
| 1047 | 1057 |
| 1058 // allUntriagedSummaries returns a tile and summaries for all untriaged GMs. |
| 1059 // |
| 1060 // TODO(jcgregorio) Make source_type selectable. |
| 1061 func allUntriagedSummaries() (*ptypes.Tile, map[string]*summary.Summary, error)
{ |
| 1062 tile, err := storages.GetLastTileTrimmed(true) |
| 1063 if err != nil { |
| 1064 return nil, nil, fmt.Errorf("Couldn't load tile: %s", err) |
| 1065 } |
| 1066 // Get a list of all untriaged images by test. |
| 1067 sum, err := summaries.CalcSummaries([]string{}, "source_type=gm", false,
true) |
| 1068 if err != nil { |
| 1069 return nil, nil, fmt.Errorf("Couldn't load summaries: %s", err) |
| 1070 } |
| 1071 return tile, sum, nil |
| 1072 } |
| 1073 |
| 1074 // ByBlame describes a single digest and it's blames. |
| 1075 type ByBlame struct { |
| 1076 Test string `json:"test"` |
| 1077 Digest string `json:"digest"` |
| 1078 Blame *blame.BlameDistribution `json:"blame"` |
| 1079 CommitIndices []int `json:"commit_indices"` |
| 1080 Key string |
| 1081 } |
| 1082 |
| 1083 // lookUpCommits returns the commit hashes for the commit indices in 'freq'. |
| 1084 func lookUpCommits(freq []int, commits []*ptypes.Commit) []string { |
| 1085 ret := []string{} |
| 1086 for _, index := range freq { |
| 1087 ret = append(ret, commits[index].Hash) |
| 1088 } |
| 1089 return ret |
| 1090 } |
| 1091 |
| 1092 // byBlameHandler returns a page with the digests to be triaged grouped by blame
list. |
| 1093 func byBlameHandler(w http.ResponseWriter, r *http.Request) { |
| 1094 w.Header().Set("Content-Type", "text/html") |
| 1095 if *local { |
| 1096 loadTemplates() |
| 1097 } |
| 1098 tile, sum, err := allUntriagedSummaries() |
| 1099 commits := tile.Commits |
| 1100 if err != nil { |
| 1101 util.ReportError(w, r, err, "Failed to load summaries.") |
| 1102 return |
| 1103 } |
| 1104 |
| 1105 // This is a very simple grouping of digests, for every digest we look u
p the |
| 1106 // blame list for that digest and then use the concatenated git hashes a
s a |
| 1107 // group id. All of the digests are then grouped by their group id. |
| 1108 |
| 1109 // Collects a ByBlame for each untriaged digest, keyed by group id. |
| 1110 grouped := map[string][]*ByBlame{} |
| 1111 |
| 1112 // The Commit info for each group id. |
| 1113 commitinfo := map[string][]*ptypes.Commit{} |
| 1114 for test, s := range sum { |
| 1115 for _, d := range s.UntHashes { |
| 1116 dist := blamer.GetBlame(test, d, commits) |
| 1117 groupid := strings.Join(lookUpCommits(dist.Freq, commits
), ":") |
| 1118 // Only fill in commitinfo for each groupid only once. |
| 1119 if _, ok := commitinfo[groupid]; !ok { |
| 1120 ci := []*ptypes.Commit{} |
| 1121 for _, index := range dist.Freq { |
| 1122 ci = append(ci, commits[index]) |
| 1123 } |
| 1124 commitinfo[groupid] = ci |
| 1125 } |
| 1126 // Construct a ByBlame and add it to grouped. |
| 1127 value := &ByBlame{ |
| 1128 Test: test, |
| 1129 Digest: d, |
| 1130 Blame: dist, |
| 1131 CommitIndices: dist.Freq, |
| 1132 } |
| 1133 if _, ok := grouped[groupid]; !ok { |
| 1134 grouped[groupid] = []*ByBlame{value} |
| 1135 } else { |
| 1136 grouped[groupid] = append(grouped[groupid], valu
e) |
| 1137 } |
| 1138 } |
| 1139 } |
| 1140 |
| 1141 // The Commit info needs to be accessed via Javascript, so serialize it
into |
| 1142 // JSON here. |
| 1143 commitinfojs, err := json.MarshalIndent(commitinfo, "", " ") |
| 1144 if err != nil { |
| 1145 util.ReportError(w, r, err, "Failed to encode response data.") |
| 1146 return |
| 1147 } |
| 1148 |
| 1149 keys := []string{} |
| 1150 for groupid, _ := range grouped { |
| 1151 keys = append(keys, groupid) |
| 1152 } |
| 1153 sort.Strings(keys) |
| 1154 |
| 1155 if err := templates.ExecuteTemplate(w, "byblame.html", |
| 1156 struct { |
| 1157 Keys []string |
| 1158 ByBlame map[string][]*ByBlame |
| 1159 CommitsJS template.JS |
| 1160 }{ |
| 1161 Keys: keys, |
| 1162 ByBlame: grouped, |
| 1163 CommitsJS: template.JS(string(commitinfojs)), |
| 1164 }); err != nil { |
| 1165 glog.Errorln("Failed to expand template:", err) |
| 1166 } |
| 1167 } |
| 1168 |
| 1169 func minFloat32(a, b float32) float32 { |
| 1170 if a < b { |
| 1171 return a |
| 1172 } |
| 1173 return b |
| 1174 } |
| 1175 |
| 1176 // BlameListDigest holds a PolyDetailsGUI and some other info needed to render |
| 1177 // a test-summary-details-sk. |
| 1178 type BlameListDigest struct { |
| 1179 Test string `json:"test"` |
| 1180 Digest string `json:"digest"` |
| 1181 Diff float32 `json:"diff"` // The smaller of the Pos and Neg d
iff. |
| 1182 Detail *PolyDetailsGUI `json:"detail"` |
| 1183 } |
| 1184 |
| 1185 // BlameListDigestSlice enables sorting BlameListDigest by the Diff score, with |
| 1186 // largest Diff's first. |
| 1187 type BlameListDigestSlice []*BlameListDigest |
| 1188 |
| 1189 func (p BlameListDigestSlice) Len() int { return len(p) } |
| 1190 func (p BlameListDigestSlice) Less(i, j int) bool { return p[i].Diff > p[j].Diff
} |
| 1191 func (p BlameListDigestSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| 1192 |
| 1193 // blameListHandler returns a page for triaging all digests that match the give
blame group id. |
| 1194 // |
| 1195 // Links to this page come from the byBlameHandler page. |
| 1196 // |
| 1197 // Request URLs query parameters: |
| 1198 // groupid - The blame list group id, which is just the list of git hashes |
| 1199 // that are in the blame concatenated with ':'s. |
| 1200 func blameListHandler(w http.ResponseWriter, r *http.Request) { |
| 1201 w.Header().Set("Content-Type", "text/html") |
| 1202 if *local { |
| 1203 loadTemplates() |
| 1204 } |
| 1205 tile, sum, err := allUntriagedSummaries() |
| 1206 if err != nil { |
| 1207 util.ReportError(w, r, err, "Failed to load summaries.") |
| 1208 return |
| 1209 } |
| 1210 commits := tile.Commits |
| 1211 exp, err := storages.ExpectationsStore.Get() |
| 1212 if err != nil { |
| 1213 util.ReportError(w, r, err, "Failed to load expectations.") |
| 1214 return |
| 1215 } |
| 1216 if err := r.ParseForm(); err != nil { |
| 1217 util.ReportError(w, r, err, "Failed to parse form data.") |
| 1218 return |
| 1219 } |
| 1220 groupid := r.Form.Get("groupid") |
| 1221 |
| 1222 // Populate a slice of BlameListDigest's that match the given blame list
. |
| 1223 list := []*BlameListDigest{} |
| 1224 for test, s := range sum { |
| 1225 for _, d := range s.UntHashes { |
| 1226 dist := blamer.GetBlame(test, d, commits) |
| 1227 key := strings.Join(lookUpCommits(dist.Freq, commits), "
:") |
| 1228 if key == groupid { |
| 1229 detail := buildDetailsGUI(tile, exp, test, d, d,
true, true) |
| 1230 |
| 1231 list = append(list, &BlameListDigest{ |
| 1232 Test: test, |
| 1233 Digest: d, |
| 1234 Diff: minFloat32(detail.NegClosest.Dif
f, detail.PosClosest.Diff), |
| 1235 Detail: detail, |
| 1236 }) |
| 1237 } |
| 1238 } |
| 1239 } |
| 1240 |
| 1241 sort.Sort(BlameListDigestSlice(list)) |
| 1242 |
| 1243 // The list needs to be available both via golang templates and also |
| 1244 // in Javascript, so we need to encode the list to JSON so it can |
| 1245 // appear in JS. |
| 1246 js, err := json.MarshalIndent(list, "", " ") |
| 1247 if err != nil { |
| 1248 util.ReportError(w, r, err, "Failed to encode response data.") |
| 1249 return |
| 1250 } |
| 1251 |
| 1252 context := struct { |
| 1253 Summaries []*BlameListDigest |
| 1254 JS template.JS |
| 1255 }{ |
| 1256 Summaries: list, |
| 1257 JS: template.JS(string(js)), |
| 1258 } |
| 1259 |
| 1260 if err := templates.ExecuteTemplate(w, "blamelist.html", context); err !
= nil { |
| 1261 glog.Errorln("Failed to expand template:", err) |
| 1262 } |
| 1263 } |
| 1264 |
| 1048 // sendJsonResponse serializes resp to JSON. If an error occurs | 1265 // sendJsonResponse serializes resp to JSON. If an error occurs |
| 1049 // a text based error code is send to the client. | 1266 // a text based error code is send to the client. |
| 1050 func sendJsonResponse(w http.ResponseWriter, resp interface{}) { | 1267 func sendJsonResponse(w http.ResponseWriter, resp interface{}) { |
| 1051 w.Header().Set("Content-Type", "application/json") | 1268 w.Header().Set("Content-Type", "application/json") |
| 1052 if err := json.NewEncoder(w).Encode(resp); err != nil { | 1269 if err := json.NewEncoder(w).Encode(resp); err != nil { |
| 1053 glog.Errorf("Failed to write or encode result: %s", err) | 1270 glog.Errorf("Failed to write or encode result: %s", err) |
| 1054 } | 1271 } |
| 1055 } | 1272 } |
| 1056 | 1273 |
| 1057 // makeResourceHandler creates a static file handler that sets a caching policy. | 1274 // makeResourceHandler creates a static file handler that sets a caching policy. |
| 1058 func makeResourceHandler() func(http.ResponseWriter, *http.Request) { | 1275 func makeResourceHandler() func(http.ResponseWriter, *http.Request) { |
| 1059 fileServer := http.FileServer(http.Dir(*resourcesDir)) | 1276 fileServer := http.FileServer(http.Dir(*resourcesDir)) |
| 1060 return func(w http.ResponseWriter, r *http.Request) { | 1277 return func(w http.ResponseWriter, r *http.Request) { |
| 1061 w.Header().Add("Cache-Control", string(300)) | 1278 w.Header().Add("Cache-Control", string(300)) |
| 1062 fileServer.ServeHTTP(w, r) | 1279 fileServer.ServeHTTP(w, r) |
| 1063 } | 1280 } |
| 1064 } | 1281 } |
| 1065 | 1282 |
| 1066 // Init figures out where the resources are and then loads the templates. | 1283 // Init figures out where the resources are and then loads the templates. |
| 1067 func Init() { | 1284 func Init() { |
| 1068 if *resourcesDir == "" { | 1285 if *resourcesDir == "" { |
| 1069 _, filename, _, _ := runtime.Caller(0) | 1286 _, filename, _, _ := runtime.Caller(0) |
| 1070 *resourcesDir = filepath.Join(filepath.Dir(filename), "../..") | 1287 *resourcesDir = filepath.Join(filepath.Dir(filename), "../..") |
| 1071 } | 1288 } |
| 1072 loadTemplates() | 1289 loadTemplates() |
| 1073 } | 1290 } |
| OLD | NEW |