dashboard/app: cache per-namespace bug stats

We used to show number of fixed bugs at the top of the main page.
However, now with the button nagivation, "fixed" is shown on every page.
Fetching and processing all bugs on every page would be unwise.
Cache these stats in memcache. It will be useful to show more stats in future.
This commit is contained in:
Dmitry Vyukov 2020-06-30 20:01:01 +02:00
parent a1aebcca7f
commit fd3bba535d
5 changed files with 102 additions and 20 deletions

70
dashboard/app/cache.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2020 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package main
import (
"fmt"
"net/http"
"time"
"golang.org/x/net/context"
"google.golang.org/appengine/memcache"
)
type Cached struct {
Open int
Fixed int
Invalid int
}
func CacheGet(c context.Context, r *http.Request, ns string) (*Cached, error) {
accessLevel := accessLevel(c, r)
key := fmt.Sprintf("%v-%v", ns, accessLevel)
v := new(Cached)
_, err := memcache.Gob.Get(c, key, v)
if err != nil && err != memcache.ErrCacheMiss {
return nil, err
}
if err == nil {
return v, nil
}
if v, err = buildCached(c, ns, accessLevel); err != nil {
return nil, err
}
item := &memcache.Item{
Key: key,
Object: v,
Expiration: time.Hour,
}
if err := memcache.Gob.Set(c, item); err != nil {
return nil, err
}
return v, nil
}
func buildCached(c context.Context, ns string, accessLevel AccessLevel) (*Cached, error) {
v := &Cached{}
bugs, _, err := loadNamespaceBugs(c, ns)
if err != nil {
return nil, err
}
for _, bug := range bugs {
switch bug.Status {
case BugStatusOpen:
if accessLevel < bug.sanitizeAccess(accessLevel) {
continue
}
if len(bug.Commits) == 0 {
v.Open++
} else {
v.Fixed++
}
case BugStatusFixed:
v.Fixed++
case BugStatusInvalid:
v.Invalid++
}
}
return v, nil
}

View File

@ -97,6 +97,7 @@ type uiHeader struct {
AnalyticsTrackingID string
Subpage string
Namespace string
Cached *Cached
Namespaces []uiNamespace
}
@ -171,6 +172,11 @@ func commonHeader(c context.Context, r *http.Request, w http.ResponseWriter, ns
h.Namespace = ns
cookie.Namespace = ns
encodeCookie(w, cookie)
cached, err := CacheGet(c, r, ns)
if err != nil {
return nil, err
}
h.Cached = cached
}
return h, nil
}

View File

@ -240,7 +240,6 @@ func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) erro
return handleTerminalBugList(c, w, r, &TerminalBug{
Status: BugStatusFixed,
Subpage: "/fixed",
Caption: "fixed",
ShowPatch: true,
})
}
@ -249,7 +248,6 @@ func handleInvalid(c context.Context, w http.ResponseWriter, r *http.Request) er
return handleTerminalBugList(c, w, r, &TerminalBug{
Status: BugStatusInvalid,
Subpage: "/invalid",
Caption: "invalid",
ShowPatch: false,
})
}
@ -257,7 +255,6 @@ func handleInvalid(c context.Context, w http.ResponseWriter, r *http.Request) er
type TerminalBug struct {
Status int
Subpage string
Caption string
ShowPatch bool
}
@ -668,7 +665,6 @@ func fetchTerminalBugs(c context.Context, accessLevel AccessLevel,
}
res := &uiBugGroup{
Now: timeNow(c),
Caption: typ.Caption,
ShowPatch: typ.ShowPatch,
Namespace: ns,
}

View File

@ -44,9 +44,7 @@ func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport {
log.Errorf(c, "%v", err)
return nil
}
bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Status<", BugStatusFixed)
})
bugs, _, err := loadOpenBugs(c)
if err != nil {
log.Errorf(c, "%v", err)
return nil
@ -150,9 +148,7 @@ func needReport(c context.Context, typ string, state *ReportingState, bug *Bug)
}
func reportingPollNotifications(c context.Context, typ string) []*dashapi.BugNotification {
bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Status<", BugStatusFixed)
})
bugs, _, err := loadOpenBugs(c)
if err != nil {
log.Errorf(c, "%v", err)
return nil
@ -549,6 +545,18 @@ func loadAllBugs(c context.Context, filter func(*db.Query) *db.Query) ([]*Bug, [
return bugs, keys, nil
}
func loadNamespaceBugs(c context.Context, ns string) ([]*Bug, []*db.Key, error) {
return loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Namespace=", ns)
})
}
func loadOpenBugs(c context.Context) ([]*Bug, []*db.Key, error) {
return loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Status<", BugStatusFixed)
})
}
func foreachBug(c context.Context, filter func(*db.Query) *db.Query, fn func(bug *Bug, key *db.Key) error) error {
const batchSize = 1000
var cursor *db.Cursor

View File

@ -58,11 +58,11 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
<tr>
<td class="navigation">
<a class="navigation_tab{{if eq .URLPath (printf "/%v" $.Namespace)}}_selected{{end}}" href='/{{$.Namespace}}'>
<span style="color:DeepPink;">🐞</span> Open</a>
<span style="color:DeepPink;">🐞</span> Open [{{$.Cached.Open}}]</a>
<a class="navigation_tab{{if eq .URLPath (printf "/%v/fixed" $.Namespace)}}_selected{{end}}" href='/{{$.Namespace}}/fixed'>
<span style="color:ForestGreen;">🐞</span> Fixed</a>
<span style="color:ForestGreen;">🐞</span> Fixed [{{$.Cached.Fixed}}]</a>
<a class="navigation_tab{{if eq .URLPath (printf "/%v/invalid" $.Namespace)}}_selected{{end}}" href='/{{$.Namespace}}/invalid'>
<span style="color:RoyalBlue;">🐞</span> Invalid</a>
<span style="color:RoyalBlue;">🐞</span> Invalid [{{$.Cached.Invalid}}]</a>
</td>
</tr>
</table>
@ -76,14 +76,16 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
{{if .}}
{{if .Bugs}}
<table class="list_table">
{{if $.Fragment}}
<caption id="{{$.Fragment}}"><a class="plain" href="#{{$.Fragment}}">
{{else}}
<caption>
{{if $.Caption}}
{{if $.Fragment}}
<caption id="{{$.Fragment}}"><a class="plain" href="#{{$.Fragment}}">
{{else}}
<caption>
{{end}}
{{$.Caption}} ({{len $.Bugs}}):
{{if $.Fragment}}</a>{{end}}
</caption>
{{end}}
{{$.Caption}} ({{len $.Bugs}}):
{{if $.Fragment}}</a>{{end}}
</caption>
<thead>
<tr>
{{if $.ShowNamespace}}