siso: webui lazy load templates and share common functions
Bug: 349287453
Change-Id: Ie6bbc8a2f778341cb1213f439a984153982471c3
Reviewed-on: https://chromium-review.googlesource.com/c/infra/infra/+/5675393
Commit-Queue: Richard Wang <richardwa@google.com>
Reviewed-by: Fumitoshi Ukai <ukai@google.com>
Cr-Commit-Position: refs/heads/main@{#66626}
diff --git a/go/src/infra/build/siso/webui/_steps.html b/go/src/infra/build/siso/webui/_steps.html
index ad96ff5..592749e 100644
--- a/go/src/infra/build/siso/webui/_steps.html
+++ b/go/src/infra/build/siso/webui/_steps.html
@@ -3,6 +3,7 @@
found in the LICENSE file. */}}
{{define "sidebar"}}
+{{$currentURL:=.currentURL}}
<form class="data-table-filter"
action="/steps/" method="GET"
hx-select="#data-table-body" hx-target="#data-table-body" hx-swap="outerHTML show:window:top"
@@ -50,9 +51,9 @@
{{ range $key, $count := .action_counts }}
{{ if gt $count 20 }}
<input type="checkbox" id="action-{{$key}}"
- name="action" value="{{$key}}"{{if currentURLHasParam "action" $key}}checked{{end}}>
+ name="action" value="{{$key}}"{{if urlHasParam $currentURL "action" $key}}checked{{end}}>
<label for="action-{{$key}}">
- <md-filter-chip label="{{printf "%.25s" $key}} ({{$count}})"{{if currentURLHasParam "action" $key}}selected{{end}}></md-filter-chip>
+ <md-filter-chip label="{{printf "%.25s" $key}} ({{$count}})"{{if urlHasParam $currentURL "action" $key}}selected{{end}}></md-filter-chip>
</label>
{{ end }}
{{ end }}
@@ -61,9 +62,9 @@
<md-chip-set aria-labelledby="rule-label">
{{ range $key, $count := .rule_counts }}
<input type="checkbox" id="rule-{{$key}}"
- name="rule" value="{{$key}}"{{if currentURLHasParam "rule" $key}}checked{{end}}>
+ name="rule" value="{{$key}}"{{if urlHasParam $currentURL "rule" $key}}checked{{end}}>
<label for="rule-{{$key}}">
- <md-filter-chip label="{{$key}} ({{$count}})"{{if currentURLHasParam "rule" $key}}selected{{end}}></md-filter-chip></label>
+ <md-filter-chip label="{{$key}} ({{$count}})"{{if urlHasParam $currentURL "rule" $key}}selected{{end}}></md-filter-chip></label>
{{ end }}
</md-chip-set>
</div>
diff --git a/go/src/infra/build/siso/webui/serve.go b/go/src/infra/build/siso/webui/serve.go
index 5b0ad29..9d86746 100644
--- a/go/src/infra/build/siso/webui/serve.go
+++ b/go/src/infra/build/siso/webui/serve.go
@@ -14,6 +14,7 @@
"io"
"io/fs"
"net/http"
+ "net/url"
"os"
"path"
"path/filepath"
@@ -31,6 +32,29 @@
const DefaultItemsPerPage = 100
+var (
+ templates = make(map[string]*template.Template)
+ baseFunctions = template.FuncMap{
+ "urlHasParam": func(url *url.URL, key, value string) bool {
+ return slices.Contains(url.Query()[key], value)
+ },
+ "divIntervalsScaled": func(a, b build.IntervalMetric, scale float64) float64 {
+ return float64(a) / float64(b) * scale
+ },
+ "addIntervals": func(a, b build.IntervalMetric) build.IntervalMetric {
+ return a + b
+ },
+ "formatIntervalMetric": func(i build.IntervalMetric) string {
+ d := time.Duration(i)
+ hour := int(d.Hours())
+ minute := int(d.Minutes()) % 60
+ second := int(d.Seconds()) % 60
+ milli := d.Milliseconds() % 1000
+ return fmt.Sprintf("%02d:%02d:%02d.%03d", hour, minute, second, milli)
+ },
+ }
+)
+
type outdirInfo struct {
buildMetrics []build.StepMetric
buildDuration build.IntervalMetric
@@ -92,10 +116,27 @@
return outdirInfo, err
}
+// loadView lazy-parses a view once, or parses every time if in local development mode.
+func loadView(localDevelopment bool, fs fs.FS, view string) (*template.Template, error) {
+ if template, ok := templates[view]; ok {
+ return template, nil
+ }
+ template, err := template.New("").Funcs(baseFunctions).ParseFS(fs, "base.html", view)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse view: %w", err)
+ }
+ if !localDevelopment {
+ templates[view] = template
+ }
+ return template, nil
+}
+
+// Serve serves the webui.
func Serve(version string, localDevelopment bool, port int, outdir string) int {
- renderView := func(wr io.Writer, tmpl *template.Template, data map[string]any) error {
+ renderView := func(wr io.Writer, r *http.Request, tmpl *template.Template, data map[string]any) error {
data["outdir"] = outdir
data["versionID"] = version
+ data["currentURL"] = r.URL
err := tmpl.ExecuteTemplate(wr, "base", data)
if err != nil {
return fmt.Errorf("failed to execute template: %w", err)
@@ -103,8 +144,7 @@
return nil
}
- // Prepare templates.
- // TODO(b/349287453): don't recompile templates every time if using embedded fs.
+ // Use templates from embed or local.
fs := fs.FS(content)
if localDevelopment {
fs = os.DirFS("webui/")
@@ -115,6 +155,7 @@
metrics, err := loadOutdirInfo(outdir)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load outdir: %v", err)
+ return 1
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@@ -126,11 +167,10 @@
})
http.HandleFunc("/logs/{file}", func(w http.ResponseWriter, r *http.Request) {
- templates := []string{"base.html", "_logs.html"}
- tmpl, err := template.ParseFS(fs, templates...)
+ tmpl, err := loadView(localDevelopment, fs, "_logs.html")
if err != nil {
// TODO(b/349287453): proper error handling.
- fmt.Fprintf(w, "failed to parse templates: %s\n", err)
+ fmt.Fprintf(w, "failed to load view: %s\n", err)
return
}
@@ -154,7 +194,7 @@
return
}
- err = renderView(w, tmpl, map[string]any{
+ err = renderView(w, r, tmpl, map[string]any{
"allowed_files": allowedFiles,
"file": file,
"file_contents": string(fileContents),
@@ -166,12 +206,10 @@
})
http.HandleFunc("POST /steps/{id}/recall/", func(w http.ResponseWriter, r *http.Request) {
- tmpl, err := template.ParseFiles(
- "webui/base.html",
- "webui/_recall.html",
- )
+ tmpl, err := loadView(localDevelopment, fs, "_recall.html")
if err != nil {
- fmt.Fprintf(w, "failed to parse templates: %s\n", err)
+ // TODO(b/349287453): proper error handling.
+ fmt.Fprintf(w, "failed to load view: %s\n", err)
return
}
@@ -181,7 +219,7 @@
return
}
- err = renderView(w, tmpl, map[string]any{
+ err = renderView(w, r, tmpl, map[string]any{
"step_id": metric.StepID,
"digest": metric.Digest,
"project": r.FormValue("project"),
@@ -193,10 +231,10 @@
})
http.HandleFunc("/steps/{id}/", func(w http.ResponseWriter, r *http.Request) {
- templates := []string{"base.html", "_step.html"}
- tmpl, err := template.ParseFS(fs, templates...)
+ tmpl, err := loadView(localDevelopment, fs, "_step.html")
if err != nil {
- fmt.Fprintf(w, "failed to parse templates: %s\n", err)
+ // TODO(b/349287453): proper error handling.
+ fmt.Fprintf(w, "failed to load view: %s\n", err)
return
}
@@ -218,39 +256,17 @@
fmt.Fprintf(w, "failed to unmarshal metrics: %v\n", err)
}
- err = renderView(w, tmpl, asMap)
+ err = renderView(w, r, tmpl, asMap)
if err != nil {
fmt.Fprintf(w, "failed to render view: %v\n", err)
}
})
http.HandleFunc("/steps/", func(w http.ResponseWriter, r *http.Request) {
- templates := []string{"base.html", "_steps.html"}
- tmpl, err := template.New("steps").Funcs(template.FuncMap{
- "currentURLHasParam": func(key string, value string) bool {
- q := r.URL.Query()
- if values, ok := q[key]; ok {
- return slices.Contains(values, value)
- }
- return false
- },
- "divIntervalsScaled": func(a build.IntervalMetric, b build.IntervalMetric, scale int) float64 {
- return float64(a) / float64(b) * float64(scale)
- },
- "addIntervals": func(a build.IntervalMetric, b build.IntervalMetric) build.IntervalMetric {
- return a + b
- },
- "formatIntervalMetric": func(i build.IntervalMetric) string {
- d := time.Duration(i)
- hour := int(d.Hours())
- minute := int(d.Minutes()) % 60
- second := int(d.Seconds()) % 60
- milli := d.Milliseconds() % 1000
- return fmt.Sprintf("%02d:%02d:%02d.%03d", hour, minute, second, milli)
- },
- }).ParseFS(fs, templates...)
+ tmpl, err := loadView(localDevelopment, fs, "_steps.html")
if err != nil {
- fmt.Fprintf(w, "failed to parse templates: %s\n", err)
+ // TODO(b/349287453): proper error handling.
+ fmt.Fprintf(w, "failed to load view: %s\n", err)
return
}
@@ -332,7 +348,7 @@
"rule_counts": metrics.ruleCounts,
"build_duration": metrics.buildDuration,
}
- err = renderView(w, tmpl, data)
+ err = renderView(w, r, tmpl, data)
if err != nil {
fmt.Fprintf(w, "failed to render view: %s\n", err)
}