experimental route perf pane

add 1.14 to travis test list
more qutils tests

add panel_menu_stats_routes_perf phrase
This commit is contained in:
Azareal 2020-02-28 14:52:45 +10:00
parent 671134b1f3
commit ca8411a519
10 changed files with 565 additions and 360 deletions

View File

@ -4,6 +4,7 @@ env:
language: go language: go
go: go:
- "1.13" - "1.13"
- "1.14"
- master - master
before_install: before_install:
- cd $HOME - cd $HOME

View File

@ -472,6 +472,19 @@ type PanelAnalyticsRoutesPage struct {
TimeRange string TimeRange string
} }
type PanelAnalyticsRoutesPerfItem struct {
Route string
Count int
Unit string
}
type PanelAnalyticsRoutesPerfPage struct {
*BasePanelPage
ItemList []PanelAnalyticsRoutesPerfItem
Graph PanelTimeGraph
TimeRange string
}
// TODO: Rename the fields as this structure is being used in a generic way now // TODO: Rename the fields as this structure is being used in a generic way now
type PanelAnalyticsAgentsItem struct { type PanelAnalyticsAgentsItem struct {
Agent string Agent string

View File

@ -98,9 +98,9 @@ func cascadeForumPerms(fp *ForumPerms, u *User) {
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with // Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
// TODO: Do a panel specific theme? // TODO: Do a panel specific theme?
func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, stats PanelStats, rerr RouteError) { func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (h *Header, stats PanelStats, rerr RouteError) {
theme := GetThemeByReq(r) theme := GetThemeByReq(r)
header = &Header{ h = &Header{
Site: Site, Site: Site,
Settings: SettingBox.Load().(SettingMap), Settings: SettingBox.Load().(SettingMap),
Themes: Themes, Themes: Themes,
@ -110,15 +110,16 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
Zone: "panel", Zone: "panel",
Writer: w, Writer: w,
IsoCode: phrases.GetLangPack().IsoCode, IsoCode: phrases.GetLangPack().IsoCode,
StartedAt: time.Now(),
} }
// TODO: We should probably initialise header.ExtData // TODO: We should probably initialise header.ExtData
// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well // ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
//if user.IsAdmin { //if user.IsAdmin {
header.StartedAt = time.Now() //h.StartedAt = time.Now()
//} //}
header.AddSheet(theme.Name + "/main.css") h.AddSheet(theme.Name + "/main.css")
header.AddSheet(theme.Name + "/panel.css") h.AddSheet(theme.Name + "/panel.css")
if len(theme.Resources) > 0 { if len(theme.Resources) > 0 {
rlist := theme.Resources rlist := theme.Resources
for _, res := range rlist { for _, res := range rlist {
@ -126,12 +127,12 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
extarr := strings.Split(res.Name, ".") extarr := strings.Split(res.Name, ".")
ext := extarr[len(extarr)-1] ext := extarr[len(extarr)-1]
if ext == "css" { if ext == "css" {
header.AddSheet(res.Name) h.AddSheet(res.Name)
} else if ext == "js" { } else if ext == "js" {
if res.Async { if res.Async {
header.AddScriptAsync(res.Name) h.AddScriptAsync(res.Name)
} else { } else {
header.AddScript(res.Name) h.AddScript(res.Name)
} }
} }
} }
@ -146,7 +147,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
stats.Groups = Groups.Count() stats.Groups = Groups.Count()
stats.Forums = Forums.Count() stats.Forums = Forums.Count()
stats.Pages = Pages.Count() stats.Pages = Pages.Count()
stats.Settings = len(header.Settings) stats.Settings = len(h.Settings)
stats.WordFilters = WordFilters.EstCount() stats.WordFilters = WordFilters.EstCount()
stats.Themes = len(Themes) stats.Themes = len(Themes)
stats.Reports = 0 // TODO: Do the report count. Only show open threads? stats.Reports = 0 // TODO: Do the report count. Only show open threads?
@ -160,12 +161,12 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
tname = "_" + theme.Name tname = "_" + theme.Name
} }
} }
header.AddPreScriptAsync("template_" + name + tname + ".js") h.AddPreScriptAsync("template_" + name + tname + ".js")
} }
addPreScript("alert") addPreScript("alert")
addPreScript("notice") addPreScript("notice")
return header, stats, nil return h, stats, nil
} }
func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) { func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {

File diff suppressed because it is too large Load Diff

View File

@ -800,6 +800,7 @@
"panel_menu_stats_topics":"Topics", "panel_menu_stats_topics":"Topics",
"panel_menu_stats_forums":"Forums", "panel_menu_stats_forums":"Forums",
"panel_menu_stats_routes":"Routes", "panel_menu_stats_routes":"Routes",
"panel_menu_stats_routes_perf":"Routes Perf",
"panel_menu_stats_agents":"Agents", "panel_menu_stats_agents":"Agents",
"panel_menu_stats_systems":"Systems", "panel_menu_stats_systems":"Systems",
"panel_menu_stats_languages":"Languages", "panel_menu_stats_languages":"Languages",

View File

@ -46,6 +46,8 @@ func TestProcessWhere(t *testing.T) {
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"})
whs = processWhere("uid=20") whs = processWhere("uid=20")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "20"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "20"})
whs = processWhere("uid=uid+1")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenColumn, "uid"}, MT{TokenOp, "+"}, MT{TokenNumber, "1"})
whs = processWhere("uid='1'") whs = processWhere("uid='1'")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"})
whs = processWhere("uid='t'") whs = processWhere("uid='t'")

View File

@ -243,6 +243,7 @@ func panelRoutes() *RouteGroup {
View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"), View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"), View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),
View("panel.AnalyticsRoutesPerf", "/panel/analytics/routes-perf/").Before("ParseForm"),
View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"), View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"),
View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"), View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"),
View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"), View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"),

View File

@ -81,6 +81,7 @@ func analyticsTimeRange(rawTimeRange string) (*AnalyticsTimeRange, error) {
return tRange, nil return tRange, nil
} }
// TODO: Clamp it rather than using an offset off the current time to avoid chaotic changes in stats as adjacent sets converge and diverge?
func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
viewMap = make(map[int64]int64) viewMap = make(map[int64]int64)
currentTime := time.Now().Unix() currentTime := time.Now().Unix()
@ -715,6 +716,171 @@ func AnalyticsPerf(w http.ResponseWriter, r *http.Request, user c.User) c.RouteE
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi}) return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi})
} }
func analyticsRowsToAvgDuoMap(rows *sql.Rows, labelList []int64, avgMap map[int64]int64) (map[string]map[int64]int64, map[string]int, error) {
aMap := make(map[string]map[int64]int64)
nameMap := make(map[string]int)
defer rows.Close()
for rows.Next() {
var count int64
var name string
var createdAt time.Time
err := rows.Scan(&count, &name, &createdAt)
if err != nil {
return aMap, nameMap, err
}
// TODO: Bulk log this
unixCreatedAt := createdAt.Unix()
if c.Dev.SuperDebug {
log.Print("count: ", count)
log.Print("name: ", name)
log.Print("createdAt: ", createdAt)
log.Print("unixCreatedAt: ", unixCreatedAt)
}
vvMap, ok := aMap[name]
if !ok {
vvMap = make(map[int64]int64)
for key, val := range avgMap {
vvMap[key] = val
}
aMap[name] = vvMap
}
for _, value := range labelList {
if unixCreatedAt > value {
vvMap[value] = (vvMap[value] + count) / 2
break
}
}
nameMap[name] = (nameMap[name] + int(count)) / 2
}
return aMap, nameMap, rows.Err()
}
func sortOVList(ovList []OVItem) (tOVList []OVItem) {
// Use bubble sort for now as there shouldn't be too many items
for i := 0; i < len(ovList)-1; i++ {
for j := 0; j < len(ovList)-1; j++ {
if ovList[j].count > ovList[j+1].count {
temp := ovList[j]
ovList[j] = ovList[j+1]
ovList[j+1] = temp
}
}
}
// Invert the direction
for i := len(ovList) - 1; i >= 0; i-- {
tOVList = append(tOVList, ovList[i])
}
return tOVList
}
func analyticsAMapToOVList(aMap map[string]map[int64]int64) (ovList []OVItem) {
// Order the map
for name, avgMap := range aMap {
var totcount int
for _, count := range avgMap {
totcount = (totcount + int(count)) / 2
}
ovList = append(ovList, OVItem{name, totcount, avgMap})
}
return sortOVList(ovList)
}
func AnalyticsRoutesPerf(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
basePage.AddSheet("chartist/chartist-plugin-legend.css")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return c.LocalError(err.Error(), w, r, user)
}
// avgMap contains timestamps but not the averages for those stamps
revLabelList, labelList, avgMap := analyticsTimeRangeToLabelList(timeRange)
rows, err := qgen.NewAcc().Select("viewchunks").Columns("avg,route,createdAt").Where("count!=0 AND route!=''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return c.InternalError(err, w, r)
}
aMap, routeMap, err := analyticsRowsToAvgDuoMap(rows, labelList, avgMap)
if err != nil {
return c.InternalError(err, w, r)
}
//c.DebugLogf("aMap: %+v\n", aMap)
//c.DebugLogf("routeMap: %+v\n", routeMap)
ovList := analyticsAMapToOVList(aMap)
//c.DebugLogf("ovList: %+v\n", ovList)
ex := strings.Split(r.FormValue("ex"), ",")
inEx := func(name string) bool {
for _, e := range ex {
if e == name {
return true
}
}
return false
}
/*
// TODO: Adjust for the missing chunks in week and month
var avgList []int64
var avgItems []c.PanelAnalyticsItemUnit
for _, value := range revLabelList {
avgList = append(avgList, avgMap[value])
cv, cu := c.ConvertPerfUnit(float64(avgMap[value]))
avgItems = append(avgItems, c.PanelAnalyticsItemUnit{Time: value, Unit: cu, Count: int64(cv)})
}
graph := c.PanelTimeGraph{Series: [][]int64{avgList}, Labels: labelList}
c.DebugLogf("graph: %+v\n", graph)
pi := c.PanelAnalyticsPerf{graph, avgItems, timeRange.Range, timeRange.Unit, "time", typ}
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi})
*/
var vList [][]int64
var legendList []string
var i int
for _, ovitem := range ovList {
if inEx(ovitem.name) {
continue
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, ovitem.viewMap[value])
}
vList = append(vList, viewList)
legendList = append(legendList, ovitem.name)
if i >= 6 {
break
}
i++
}
graph := c.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList}
c.DebugLogf("graph: %+v\n", graph)
// TODO: Sort this slice
var routeItems []c.PanelAnalyticsRoutesPerfItem
for route, count := range routeMap {
if inEx(route) {
continue
}
cv, cu := c.ConvertPerfUnit(float64(count))
routeItems = append(routeItems, c.PanelAnalyticsRoutesPerfItem{
Route: route,
Unit: cu,
Count: int(cv),
})
}
pi := c.PanelAnalyticsRoutesPerfPage{basePage, routeItems, graph, timeRange.Range}
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_routes_perf", pi})
}
func analyticsRowsToRefMap(rows *sql.Rows) (map[string]int, error) { func analyticsRowsToRefMap(rows *sql.Rows) (map[string]int, error) {
nameMap := make(map[string]int) nameMap := make(map[string]int)
defer rows.Close() defer rows.Close()
@ -792,23 +958,7 @@ func analyticsVMapToOVList(vMap map[string]map[int64]int64) (ovList []OVItem) {
ovList = append(ovList, OVItem{name, totcount, viewMap}) ovList = append(ovList, OVItem{name, totcount, viewMap})
} }
// Use bubble sort for now as there shouldn't be too many items return sortOVList(ovList)
for i := 0; i < len(ovList)-1; i++ {
for j := 0; j < len(ovList)-1; j++ {
if ovList[j].count > ovList[j+1].count {
temp := ovList[j]
ovList[j] = ovList[j+1]
ovList[j+1] = temp
}
}
}
// Invert the direction
var tOVList []OVItem
for i := len(ovList) - 1; i >= 0; i-- {
tOVList = append(tOVList, ovList[i])
}
return tOVList
} }
func AnalyticsForums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError { func AnalyticsForums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
@ -917,7 +1067,10 @@ func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
//c.DebugLogf("vMap: %+v\n", vMap)
//c.DebugLogf("routeMap: %+v\n", routeMap)
ovList := analyticsVMapToOVList(vMap) ovList := analyticsVMapToOVList(vMap)
//c.DebugLogf("ovList: %+v\n", ovList)
ex := strings.Split(r.FormValue("ex"), ",") ex := strings.Split(r.FormValue("ex"), ",")
inEx := func(name string) bool { inEx := func(name string) bool {

View File

@ -0,0 +1,19 @@
<div class="colstack_item colstack_head">
<div class="rowitem">
<h1>{{lang "panel_stats_routes_head"}}</h1>
{{template "panel_analytics_time_range.html" . }}
</div>
</div>
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/routes-perf/" method="get"></form>
<div id="panel_analytics_routes_chart" class="colstack_graph_holder">
<div class="ct_chart"></div>
</div>
<div id="panel_analytics_routes" class="colstack_item rowlist">
{{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent">
<a href="/panel/analytics/route/{{.Route}}" class="panel_upshift">{{.Route}}</a>
<span class="panel_compacttext to_right">{{.Count}}{{.Unit}}</span>
</div>
{{else}}<div class="rowitem passive rowmsg">{{lang "panel_stats_routes_no_routes"}}</div>{{end}}
</div>
{{template "panel_analytics_script_perf.html" . }}

View File

@ -50,6 +50,9 @@
<div class="rowitem passive submenu"> <div class="rowitem passive submenu">
<a href="/panel/analytics/routes/">{{lang "panel_menu_stats_routes"}}</a> <a href="/panel/analytics/routes/">{{lang "panel_menu_stats_routes"}}</a>
</div> </div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/routes-perf/">{{lang "panel_menu_stats_routes_perf"}}</a>
</div>
<div class="rowitem passive submenu"> <div class="rowitem passive submenu">
<a href="/panel/analytics/agents/">{{lang "panel_menu_stats_agents"}}</a> <a href="/panel/analytics/agents/">{{lang "panel_menu_stats_agents"}}</a>
</div> </div>