perf(go): add runtime metrics and benchmarks

This commit is contained in:
zarzet
2026-07-16 10:53:26 +07:00
parent b5d02da759
commit 648f36244b
7 changed files with 194 additions and 1 deletions
@@ -3153,6 +3153,12 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"getGoRuntimeMetrics" -> {
val response = withContext(Dispatchers.IO) {
Gobackend.getRuntimeMetricsJSON()
}
result.success(response)
}
"setMetadataLanguage" -> {
val tag = call.argument<String>("tag") ?: ""
withContext(Dispatchers.IO) {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
flac "github.com/go-flac/go-flac/v2"
)
func writeSinglePassTestFlac(t *testing.T, path string, cover []byte) {
func writeSinglePassTestFlac(t testing.TB, path string, cover []byte) {
t.Helper()
streamInfo := make([]byte, 34)
const sampleRate = 44100
+106
View File
@@ -0,0 +1,106 @@
package gobackend
import (
"encoding/json"
"fmt"
"path/filepath"
"testing"
"time"
"github.com/dop251/goja"
)
var (
benchmarkStringSink string
benchmarkIntSink int64
benchmarkScanSink *LibraryScanResult
)
func BenchmarkGojaProviderInvocation(b *testing.B) {
vm := goja.New()
if _, err := vm.RunString(`var extension = { searchTracks: function(query, limit) { return query.length + limit; } };`); err != nil {
b.Fatal(err)
}
b.Run("direct_callable", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
value, err := invokeExtensionMethod(vm, "searchTracks", "lossless", 25)
if err != nil {
b.Fatal(err)
}
benchmarkIntSink = value.ToInteger()
}
})
b.Run("compile_source", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
script := fmt.Sprintf(`extension.searchTracks(%q, %d)`, "lossless", 25)
value, err := vm.RunString(script)
if err != nil {
b.Fatal(err)
}
benchmarkIntSink = value.ToInteger()
}
})
}
func BenchmarkCheckFilesExistBatch(b *testing.B) {
const trackCount = 1000
dir := b.TempDir()
idx := &ISRCIndex{
index: make(map[string]string, trackCount),
files: make(map[string]isrcFileEntry),
outputDir: dir,
}
tracks := make([]map[string]string, trackCount)
for i := 0; i < trackCount; i++ {
isrc := fmt.Sprintf("USAA%08d", i)
idx.index[isrc] = filepath.Join(dir, fmt.Sprintf("%d.flac", i))
tracks[i] = map[string]string{
"isrc": isrc,
"track_name": fmt.Sprintf("Track %d", i),
"artist_name": "Artist",
}
}
idx.buildTime.Store(time.Now().UnixNano())
isrcIndexCacheMu.Lock()
isrcIndexCache[dir] = idx
isrcIndexCacheMu.Unlock()
b.Cleanup(func() { InvalidateISRCCache(dir) })
payload, err := json.Marshal(tracks)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.ResetTimer()
for b.Loop() {
result, err := CheckFilesExistParallel(dir, string(payload))
if err != nil {
b.Fatal(err)
}
benchmarkStringSink = result
}
}
func BenchmarkLibraryScanFLACSinglePass(b *testing.B) {
dir := b.TempDir()
path := filepath.Join(dir, "track.flac")
cover := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3}
writeSinglePassTestFlac(b, path, cover)
cacheDir := filepath.Join(dir, "covers")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
result := &LibraryScanResult{FilePath: path, Format: "flac"}
var err error
benchmarkScanSink, err = scanFLACFileWithCoverCache(path, result, "", cacheDir, "stable-key")
if err != nil {
b.Fatal(err)
}
}
}
+54
View File
@@ -0,0 +1,54 @@
package gobackend
import (
"encoding/json"
"runtime"
"time"
)
type goRuntimeMetrics struct {
CapturedAtUnixMS int64 `json:"captured_at_unix_ms"`
HeapAllocBytes uint64 `json:"heap_alloc_bytes"`
HeapInuseBytes uint64 `json:"heap_inuse_bytes"`
HeapIdleBytes uint64 `json:"heap_idle_bytes"`
HeapReleasedBytes uint64 `json:"heap_released_bytes"`
HeapObjects uint64 `json:"heap_objects"`
StackInuseBytes uint64 `json:"stack_inuse_bytes"`
SysBytes uint64 `json:"sys_bytes"`
NextGCBytes uint64 `json:"next_gc_bytes"`
LastGCUnixMS int64 `json:"last_gc_unix_ms"`
NumGC uint32 `json:"num_gc"`
PauseTotalNS uint64 `json:"pause_total_ns"`
Goroutines int `json:"goroutines"`
GOMAXPROCS int `json:"gomaxprocs"`
CgoCalls int64 `json:"cgo_calls"`
}
// GetRuntimeMetricsJSON reports Go-owned runtime memory. Native allocations
// from Flutter, FFmpeg, SQLite, and the OS are intentionally outside it.
func GetRuntimeMetricsJSON() string {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
metrics := goRuntimeMetrics{
CapturedAtUnixMS: time.Now().UnixMilli(),
HeapAllocBytes: stats.HeapAlloc,
HeapInuseBytes: stats.HeapInuse,
HeapIdleBytes: stats.HeapIdle,
HeapReleasedBytes: stats.HeapReleased,
HeapObjects: stats.HeapObjects,
StackInuseBytes: stats.StackInuse,
SysBytes: stats.Sys,
NextGCBytes: stats.NextGC,
LastGCUnixMS: int64(stats.LastGC / uint64(time.Millisecond)),
NumGC: stats.NumGC,
PauseTotalNS: stats.PauseTotalNs,
Goroutines: runtime.NumGoroutine(),
GOMAXPROCS: runtime.GOMAXPROCS(0),
CgoCalls: runtime.NumCgoCall(),
}
payload, err := json.Marshal(metrics)
if err != nil {
return "{}"
}
return string(payload)
}
+19
View File
@@ -0,0 +1,19 @@
package gobackend
import (
"encoding/json"
"testing"
)
func TestGetRuntimeMetricsJSON(t *testing.T) {
var metrics goRuntimeMetrics
if err := json.Unmarshal([]byte(GetRuntimeMetricsJSON()), &metrics); err != nil {
t.Fatalf("runtime metrics JSON: %v", err)
}
if metrics.CapturedAtUnixMS <= 0 || metrics.HeapInuseBytes == 0 || metrics.SysBytes == 0 || metrics.Goroutines <= 0 || metrics.GOMAXPROCS <= 0 {
t.Fatalf("runtime metrics = %#v", metrics)
}
if metrics.HeapAllocBytes > metrics.HeapInuseBytes {
t.Fatalf("heap alloc %d exceeds heap in-use %d", metrics.HeapAllocBytes, metrics.HeapInuseBytes)
}
}
+3
View File
@@ -741,6 +741,9 @@ import Gobackend
case "releaseMemoryUnderPressure":
GobackendReleaseMemoryUnderPressure()
return nil
case "getGoRuntimeMetrics":
return GobackendGetRuntimeMetricsJSON()
case "getLogCount":
let response = GobackendGetLogCount()
+5
View File
@@ -1176,6 +1176,11 @@ class PlatformBridge {
} catch (_) {}
}
static Future<Map<String, dynamic>> getGoRuntimeMetrics() async {
final result = await _channel.invokeMethod('getGoRuntimeMetrics');
return _decodeRequiredMapResult(result, 'getGoRuntimeMetrics');
}
/// Tells the backend the app's display language so metadata providers
/// localize by it instead of IP geolocation. Best-effort.
static Future<void> setMetadataLanguage(String tag) async {