From 2bde157a0dc57c24891951cf2fac203f369689be Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Tue, 9 Dec 2025 14:38:40 +0100 Subject: [PATCH] feat(i18n): translate system status data units in runtime (#10358) Followup to https://codeberg.org/forgejo/forgejo/pulls/2528 Instead of storing translated strings in memory, store raw numbers and translate at template rendering time. Our implementation of `TrSize` is not very efficient and is more expensive than just the underlying `humanize.IBytes`, but for me on localhost both ways render response to HTMLX's request to `/admin/system_status` in 0-1 ms. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10358 Reviewed-by: Gusted Co-authored-by: 0ko <0ko@noreply.codeberg.org> Co-committed-by: 0ko <0ko@noreply.codeberg.org> --- routers/web/admin/admin.go | 72 +++++++++++------------ templates/admin/system_status.tmpl | 36 ++++++------ tests/integration/admin_dashboard_test.go | 29 +++++++-- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 4e913ef9a1..4ea62b0742 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -43,36 +43,36 @@ var sysStatus struct { NumGoroutine int // General statistics. - MemAllocated string // bytes allocated and still in use - MemTotal string // bytes allocated (even if freed) - MemSys string // bytes obtained from system (sum of XxxSys below) + MemAllocated int64 // bytes allocated and still in use + MemTotal int64 // bytes allocated (even if freed) + MemSys int64 // bytes obtained from system (sum of XxxSys below) Lookups uint64 // number of pointer lookups MemMallocs uint64 // number of mallocs MemFrees uint64 // number of frees // Main allocation heap statistics. - HeapAlloc string // bytes allocated and still in use - HeapSys string // bytes obtained from system - HeapIdle string // bytes in idle spans - HeapInuse string // bytes in non-idle span - HeapReleased string // bytes released to the OS + HeapAlloc int64 // bytes allocated and still in use + HeapSys int64 // bytes obtained from system + HeapIdle int64 // bytes in idle spans + HeapInuse int64 // bytes in non-idle span + HeapReleased int64 // bytes released to the OS HeapObjects uint64 // total number of allocated objects // Low-level fixed-size structure allocator statistics. // Inuse is bytes used now. // Sys is bytes obtained from system. - StackInuse string // bootstrap stacks - StackSys string - MSpanInuse string // mspan structures - MSpanSys string - MCacheInuse string // mcache structures - MCacheSys string - BuckHashSys string // profiling bucket hash table - GCSys string // GC metadata - OtherSys string // other system allocations + StackInuse int64 // bootstrap stacks + StackSys int64 + MSpanInuse int64 // mspan structures + MSpanSys int64 + MCacheInuse int64 // mcache structures + MCacheSys int64 + BuckHashSys int64 // profiling bucket hash table + GCSys int64 // GC metadata + OtherSys int64 // other system allocations // Garbage collector statistics. - NextGC string // next run in HeapAlloc time (bytes) + NextGC int64 // next run in HeapAlloc time (bytes) LastGCTime string // last run time PauseTotalNs string PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] @@ -86,31 +86,31 @@ func updateSystemStatus() { runtime.ReadMemStats(m) sysStatus.NumGoroutine = runtime.NumGoroutine() - sysStatus.MemAllocated = base.FileSize(int64(m.Alloc)) - sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc)) - sysStatus.MemSys = base.FileSize(int64(m.Sys)) + sysStatus.MemAllocated = int64(m.Alloc) + sysStatus.MemTotal = int64(m.TotalAlloc) + sysStatus.MemSys = int64(m.Sys) sysStatus.Lookups = m.Lookups sysStatus.MemMallocs = m.Mallocs sysStatus.MemFrees = m.Frees - sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc)) - sysStatus.HeapSys = base.FileSize(int64(m.HeapSys)) - sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle)) - sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse)) - sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased)) + sysStatus.HeapAlloc = int64(m.HeapAlloc) + sysStatus.HeapSys = int64(m.HeapSys) + sysStatus.HeapIdle = int64(m.HeapIdle) + sysStatus.HeapInuse = int64(m.HeapInuse) + sysStatus.HeapReleased = int64(m.HeapReleased) sysStatus.HeapObjects = m.HeapObjects - sysStatus.StackInuse = base.FileSize(int64(m.StackInuse)) - sysStatus.StackSys = base.FileSize(int64(m.StackSys)) - sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse)) - sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys)) - sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse)) - sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys)) - sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys)) - sysStatus.GCSys = base.FileSize(int64(m.GCSys)) - sysStatus.OtherSys = base.FileSize(int64(m.OtherSys)) + sysStatus.StackInuse = int64(m.StackInuse) + sysStatus.StackSys = int64(m.StackSys) + sysStatus.MSpanInuse = int64(m.MSpanInuse) + sysStatus.MSpanSys = int64(m.MSpanSys) + sysStatus.MCacheInuse = int64(m.MCacheInuse) + sysStatus.MCacheSys = int64(m.MCacheSys) + sysStatus.BuckHashSys = int64(m.BuckHashSys) + sysStatus.GCSys = int64(m.GCSys) + sysStatus.OtherSys = int64(m.OtherSys) - sysStatus.NextGC = base.FileSize(int64(m.NextGC)) + sysStatus.NextGC = int64(m.NextGC) sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339) sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl index 7b5c9be6cc..d9856ccd0b 100644 --- a/templates/admin/system_status.tmpl +++ b/templates/admin/system_status.tmpl @@ -5,11 +5,11 @@
{{.SysStatus.NumGoroutine}}
{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}
-
{{.SysStatus.MemAllocated}}
+
{{ctx.Locale.TrSize .SysStatus.MemAllocated}}
{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}
-
{{.SysStatus.MemTotal}}
+
{{ctx.Locale.TrSize .SysStatus.MemTotal}}
{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}
-
{{.SysStatus.MemSys}}
+
{{ctx.Locale.TrSize .SysStatus.MemSys}}
{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}
{{.SysStatus.Lookups}}
{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}
@@ -18,39 +18,39 @@
{{.SysStatus.MemFrees}}
{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}
-
{{.SysStatus.HeapAlloc}}
+
{{ctx.Locale.TrSize .SysStatus.HeapAlloc}}
{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}
-
{{.SysStatus.HeapSys}}
+
{{ctx.Locale.TrSize .SysStatus.HeapSys}}
{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}
-
{{.SysStatus.HeapIdle}}
+
{{ctx.Locale.TrSize .SysStatus.HeapIdle}}
{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}
-
{{.SysStatus.HeapInuse}}
+
{{ctx.Locale.TrSize .SysStatus.HeapInuse}}
{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}
-
{{.SysStatus.HeapReleased}}
+
{{ctx.Locale.TrSize .SysStatus.HeapReleased}}
{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}
{{.SysStatus.HeapObjects}}
{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}
-
{{.SysStatus.StackInuse}}
+
{{ctx.Locale.TrSize .SysStatus.StackInuse}}
{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}
-
{{.SysStatus.StackSys}}
+
{{ctx.Locale.TrSize .SysStatus.StackSys}}
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}
-
{{.SysStatus.MSpanInuse}}
+
{{ctx.Locale.TrSize .SysStatus.MSpanInuse}}
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}
-
{{.SysStatus.MSpanSys}}
+
{{ctx.Locale.TrSize .SysStatus.MSpanSys}}
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}
-
{{.SysStatus.MCacheInuse}}
+
{{ctx.Locale.TrSize .SysStatus.MCacheInuse}}
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}
-
{{.SysStatus.MCacheSys}}
+
{{ctx.Locale.TrSize .SysStatus.MCacheSys}}
{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}
-
{{.SysStatus.BuckHashSys}}
+
{{ctx.Locale.TrSize .SysStatus.BuckHashSys}}
{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}
-
{{.SysStatus.GCSys}}
+
{{ctx.Locale.TrSize .SysStatus.GCSys}}
{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}
-
{{.SysStatus.OtherSys}}
+
{{ctx.Locale.TrSize .SysStatus.OtherSys}}
{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}
-
{{.SysStatus.NextGC}}
+
{{ctx.Locale.TrSize .SysStatus.NextGC}}
{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}
{{.SysStatus.LastGCTime}}
{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}
diff --git a/tests/integration/admin_dashboard_test.go b/tests/integration/admin_dashboard_test.go index be4e97d56c..2aca7d9307 100644 --- a/tests/integration/admin_dashboard_test.go +++ b/tests/integration/admin_dashboard_test.go @@ -12,6 +12,8 @@ import ( "forgejo.org/modules/test" "forgejo.org/modules/translation" "forgejo.org/tests" + + "github.com/stretchr/testify/assert" ) var commonEntries = []string{ @@ -33,7 +35,8 @@ var sshEntries = []string{ "resync_all_sshprincipals", } -func testAssertAdminDashboardEntries(t *testing.T, page *HTMLDoc, locale translation.Locale, expectSSH bool) { +// Check cron options on /admin, including those that are available conditionally +func testAssertAdminDashboardOptions(t *testing.T, page *HTMLDoc, locale translation.Locale, expectSSH bool) { for _, entry := range commonEntries { page.AssertSelection(t, page.FindByText("table tr td", locale.TrString(fmt.Sprintf("admin.dashboard.%s", entry))), true) page.AssertSelection(t, page.Find(fmt.Sprintf("table tr td button[value='%s']", entry)), true) @@ -56,7 +59,7 @@ func TestAdminDashboard(t *testing.T) { defer test.MockVariableValue(&setting.SSH.Disabled, true)() page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK).Body) - testAssertAdminDashboardEntries(t, page, locale, false) + testAssertAdminDashboardOptions(t, page, locale, false) }) t.Run("SSH enabled, but built-in", func(t *testing.T) { @@ -65,7 +68,7 @@ func TestAdminDashboard(t *testing.T) { defer test.MockVariableValue(&setting.SSH.StartBuiltinServer, true)() page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK).Body) - testAssertAdminDashboardEntries(t, page, locale, false) + testAssertAdminDashboardOptions(t, page, locale, false) }) t.Run("SSH enabled and external", func(t *testing.T) { @@ -74,6 +77,24 @@ func TestAdminDashboard(t *testing.T) { defer test.MockVariableValue(&setting.SSH.StartBuiltinServer, false)() page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK).Body) - testAssertAdminDashboardEntries(t, page, locale, true) + testAssertAdminDashboardOptions(t, page, locale, true) + }) + + t.Run("System status", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Check data units translations in the System status table + selector := ".table[hx-get='/admin/system_status'] > dl > dd" + // ...in English + page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK).Body) + assert.Contains(t, page.Find(selector).Text(), "MiB") + + // ...in another language + lang := session.GetCookie("lang") + lang.Value = "ru-RU" + session.SetCookie(lang) + + page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK).Body) + assert.Contains(t, page.Find(selector).Text(), "МиБ") }) }