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 <gusted@noreply.codeberg.org>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
0ko 2025-12-09 14:38:40 +01:00 committed by Gusted
parent 7cfa6ef670
commit 2bde157a0d
3 changed files with 79 additions and 58 deletions

View file

@ -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)

View file

@ -5,11 +5,11 @@
<dd>{{.SysStatus.NumGoroutine}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
<dd>{{.SysStatus.MemAllocated}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MemAllocated}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
<dd>{{.SysStatus.MemTotal}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MemTotal}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
<dd>{{.SysStatus.MemSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MemSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
<dd>{{.SysStatus.Lookups}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
@ -18,39 +18,39 @@
<dd>{{.SysStatus.MemFrees}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
<dd>{{.SysStatus.HeapAlloc}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.HeapAlloc}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
<dd>{{.SysStatus.HeapSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.HeapSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
<dd>{{.SysStatus.HeapIdle}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.HeapIdle}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
<dd>{{.SysStatus.HeapInuse}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.HeapInuse}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
<dd>{{.SysStatus.HeapReleased}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.HeapReleased}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
<dd>{{.SysStatus.HeapObjects}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
<dd>{{.SysStatus.StackInuse}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.StackInuse}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
<dd>{{.SysStatus.StackSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.StackSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
<dd>{{.SysStatus.MSpanInuse}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MSpanInuse}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
<dd>{{.SysStatus.MSpanSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MSpanSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
<dd>{{.SysStatus.MCacheInuse}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MCacheInuse}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
<dd>{{.SysStatus.MCacheSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.MCacheSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
<dd>{{.SysStatus.BuckHashSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.BuckHashSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
<dd>{{.SysStatus.GCSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.GCSys}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
<dd>{{.SysStatus.OtherSys}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.OtherSys}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
<dd>{{.SysStatus.NextGC}}</dd>
<dd>{{ctx.Locale.TrSize .SysStatus.NextGC}}</dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
<dd><relative-time format="duration" datetime="{{.SysStatus.LastGCTime}}">{{.SysStatus.LastGCTime}}</relative-time></dd>
<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>

View file

@ -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(), "МиБ")
})
}