mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
perf(go): add runtime metrics and benchmarks
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -741,6 +741,9 @@ import Gobackend
|
||||
case "releaseMemoryUnderPressure":
|
||||
GobackendReleaseMemoryUnderPressure()
|
||||
return nil
|
||||
|
||||
case "getGoRuntimeMetrics":
|
||||
return GobackendGetRuntimeMetricsJSON()
|
||||
|
||||
case "getLogCount":
|
||||
let response = GobackendGetLogCount()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user