loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[REFACTOR] Simplify converting struct to map in admin stats

- Instead of relying on JSON to convert the struct to map, use
`reflect` to do this conversion. Also simplify it a bit by only passing
one variable to the template.
- This avoids issues where the conversion to JSON causes changes in
the value, for example huge numbers are converted to its scientific
notation but are consequently not converted back when being displayed.
- Adds unit tests.
- Resolves an issue where the amount of comments is being displayed in
scientific notation on Codeberg.

Gusted f68bc0ec ec1b6463

+65 -22
+15 -20
routers/web/admin/admin.go
··· 7 7 import ( 8 8 "fmt" 9 9 "net/http" 10 + "reflect" 10 11 "runtime" 11 - "sort" 12 12 "time" 13 13 14 14 activities_model "code.gitea.io/gitea/models/activities" ··· 16 16 "code.gitea.io/gitea/modules/base" 17 17 "code.gitea.io/gitea/modules/context" 18 18 "code.gitea.io/gitea/modules/graceful" 19 - "code.gitea.io/gitea/modules/json" 20 19 "code.gitea.io/gitea/modules/log" 21 20 "code.gitea.io/gitea/modules/setting" 22 21 "code.gitea.io/gitea/modules/updatechecker" ··· 225 224 func MonitorStats(ctx *context.Context) { 226 225 ctx.Data["Title"] = ctx.Tr("admin.monitor.stats") 227 226 ctx.Data["PageIsAdminMonitorStats"] = true 228 - bs, err := json.Marshal(activities_model.GetStatistic(ctx).Counter) 229 - if err != nil { 230 - ctx.ServerError("MonitorStats", err) 231 - return 232 - } 233 - statsCounter := map[string]any{} 234 - err = json.Unmarshal(bs, &statsCounter) 235 - if err != nil { 236 - ctx.ServerError("MonitorStats", err) 237 - return 238 - } 239 - statsKeys := make([]string, 0, len(statsCounter)) 240 - for k := range statsCounter { 241 - if statsCounter[k] == nil { 227 + modelStats := activities_model.GetStatistic(ctx).Counter 228 + stats := map[string]any{} 229 + 230 + // To avoid manually converting the values of the stats struct to an map, 231 + // and to avoid using JSON to do this for us (JSON encoder converts numbers to 232 + // scientific notation). Use reflect to convert the struct to an map. 233 + rv := reflect.ValueOf(modelStats) 234 + for i := 0; i < rv.NumField(); i++ { 235 + field := rv.Field(i) 236 + // Preserve old behavior, do not show arrays that are empty. 237 + if field.Kind() == reflect.Slice && field.Len() == 0 { 242 238 continue 243 239 } 244 - statsKeys = append(statsKeys, k) 240 + stats[rv.Type().Field(i).Name] = field.Interface() 245 241 } 246 - sort.Strings(statsKeys) 247 - ctx.Data["StatsKeys"] = statsKeys 248 - ctx.Data["StatsCounter"] = statsCounter 242 + 243 + ctx.Data["Stats"] = stats 249 244 ctx.HTML(http.StatusOK, tplStats) 250 245 }
+48
routers/web/admin/admin_test.go
··· 6 6 import ( 7 7 "testing" 8 8 9 + activities_model "code.gitea.io/gitea/models/activities" 10 + "code.gitea.io/gitea/models/unittest" 11 + "code.gitea.io/gitea/modules/contexttest" 12 + "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/modules/test" 9 14 "github.com/stretchr/testify/assert" 10 15 ) 11 16 ··· 66 71 assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) 67 72 } 68 73 } 74 + 75 + func TestMonitorStats(t *testing.T) { 76 + unittest.PrepareTestEnv(t) 77 + 78 + t.Run("Normal", func(t *testing.T) { 79 + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByLabel, false)() 80 + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByRepository, false)() 81 + 82 + ctx, _ := contexttest.MockContext(t, "admin/stats") 83 + MonitorStats(ctx) 84 + 85 + // Test some of the stats manually. 86 + mappedStats := ctx.Data["Stats"].(map[string]any) 87 + stats := activities_model.GetStatistic(ctx).Counter 88 + 89 + assert.EqualValues(t, stats.Comment, mappedStats["Comment"]) 90 + assert.EqualValues(t, stats.Issue, mappedStats["Issue"]) 91 + assert.EqualValues(t, stats.User, mappedStats["User"]) 92 + assert.EqualValues(t, stats.Milestone, mappedStats["Milestone"]) 93 + 94 + // Ensure that these aren't set. 95 + assert.Empty(t, stats.IssueByLabel) 96 + assert.Empty(t, stats.IssueByRepository) 97 + assert.Nil(t, mappedStats["IssueByLabel"]) 98 + assert.Nil(t, mappedStats["IssueByRepository"]) 99 + }) 100 + 101 + t.Run("IssueByX", func(t *testing.T) { 102 + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByLabel, true)() 103 + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByRepository, true)() 104 + 105 + ctx, _ := contexttest.MockContext(t, "admin/stats") 106 + MonitorStats(ctx) 107 + 108 + mappedStats := ctx.Data["Stats"].(map[string]any) 109 + stats := activities_model.GetStatistic(ctx).Counter 110 + 111 + assert.NotEmpty(t, stats.IssueByLabel) 112 + assert.NotEmpty(t, stats.IssueByRepository) 113 + assert.EqualValues(t, stats.IssueByLabel, mappedStats["IssueByLabel"]) 114 + assert.EqualValues(t, stats.IssueByRepository, mappedStats["IssueByRepository"]) 115 + }) 116 + }
+2 -2
templates/admin/stats.tmpl
··· 5 5 </h4> 6 6 <div class="ui attached table segment"> 7 7 <table class="ui very basic striped table unstackable"> 8 - {{range $statsKey := .StatsKeys}} 8 + {{range $statsKey, $statsValue := .Stats}} 9 9 <tr> 10 10 <td width="200">{{$statsKey}}</td> 11 - <td>{{index $.StatsCounter $statsKey}}</td> 11 + <td>{{$statsValue}}</td> 12 12 </tr> 13 13 {{end}} 14 14 </table>