diff --git a/runtime/_patch/runtime/metrics/memstats_llgo.go b/runtime/_patch/runtime/metrics/memstats_llgo.go new file mode 100644 index 0000000000..7700e3c014 --- /dev/null +++ b/runtime/_patch/runtime/metrics/memstats_llgo.go @@ -0,0 +1,14 @@ +//go:build !nogc + +package metrics + +import "runtime" + +func llgoReadMetricMemStats() runtime.MemStats { + var m runtime.MemStats + runtime.ReadMemStats(&m) + if m.HeapObjects == 0 { + m.HeapObjects = llgoSaturatingSub(m.Mallocs, m.Frees) + } + return m +} diff --git a/runtime/_patch/runtime/metrics/memstats_nogc_llgo.go b/runtime/_patch/runtime/metrics/memstats_nogc_llgo.go new file mode 100644 index 0000000000..a3ab61e981 --- /dev/null +++ b/runtime/_patch/runtime/metrics/memstats_nogc_llgo.go @@ -0,0 +1,9 @@ +//go:build nogc + +package metrics + +import "runtime" + +func llgoReadMetricMemStats() runtime.MemStats { + return runtime.MemStats{} +} diff --git a/runtime/_patch/runtime/metrics/read_llgo.go b/runtime/_patch/runtime/metrics/read_llgo.go new file mode 100644 index 0000000000..ca87943a36 --- /dev/null +++ b/runtime/_patch/runtime/metrics/read_llgo.go @@ -0,0 +1,141 @@ +package metrics + +import ( + "math" + "runtime" + "unsafe" +) + +var llgoMetricDefaultHistBuckets = []float64{0, math.Inf(1)} + +// runtime_readMetrics replaces the GOROOT declaration that is normally +// implemented by package runtime. Keep the catalog and value layouts in this +// package, so LLGo only supplies the small subset of values it can observe. +func runtime_readMetrics(samplesp unsafe.Pointer, n int, _ int) { + samples := unsafe.Slice((*Sample)(samplesp), n) + mem := llgoReadMetricMemStats() + + for i := range samples { + sample := &samples[i] + kind, ok := llgoMetricKind(sample.Name) + if !ok { + sample.Value = Value{} + continue + } + llgoSetMetricDefault(&sample.Value, kind) + llgoSetRuntimeMetric(sample.Name, &sample.Value, mem) + } +} + +func llgoMetricKind(name string) (ValueKind, bool) { + for _, desc := range allDesc { + if desc.Name == name { + return desc.Kind, true + } + } + return KindBad, false +} + +func llgoSetRuntimeMetric(name string, value *Value, mem runtime.MemStats) { + switch name { + case "/sched/gomaxprocs:threads", "/sched/threads/total:threads": + llgoSetUint64(value, uint64(runtime.GOMAXPROCS(0))) + case "/sched/goroutines-created:goroutines", + "/sched/goroutines/running:goroutines", + "/sched/goroutines:goroutines": + llgoSetUint64(value, 1) + case "/gc/cycles/automatic:gc-cycles": + llgoSetUint64(value, llgoSaturatingSub(uint64(mem.NumGC), uint64(mem.NumForcedGC))) + case "/gc/cycles/forced:gc-cycles": + llgoSetUint64(value, uint64(mem.NumForcedGC)) + case "/gc/cycles/total:gc-cycles": + llgoSetUint64(value, uint64(mem.NumGC)) + case "/gc/heap/allocs:bytes": + llgoSetUint64(value, mem.TotalAlloc) + case "/gc/heap/allocs:objects": + llgoSetUint64(value, mem.Mallocs) + case "/gc/heap/frees:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.TotalAlloc, mem.Alloc)) + case "/gc/heap/frees:objects": + llgoSetUint64(value, mem.Frees) + case "/gc/heap/goal:bytes": + llgoSetUint64(value, mem.NextGC) + case "/gc/heap/live:bytes", "/memory/classes/heap/objects:bytes": + llgoSetUint64(value, mem.HeapAlloc) + case "/gc/heap/objects:objects": + llgoSetUint64(value, mem.HeapObjects) + case "/memory/classes/heap/free:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.HeapIdle, mem.HeapReleased)) + case "/memory/classes/heap/released:bytes": + llgoSetUint64(value, mem.HeapReleased) + case "/memory/classes/heap/stacks:bytes": + llgoSetUint64(value, mem.StackInuse) + case "/memory/classes/heap/unused:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.HeapInuse, mem.HeapAlloc)) + case "/memory/classes/metadata/mcache/free:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.MCacheSys, mem.MCacheInuse)) + case "/memory/classes/metadata/mcache/inuse:bytes": + llgoSetUint64(value, mem.MCacheInuse) + case "/memory/classes/metadata/mspan/free:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.MSpanSys, mem.MSpanInuse)) + case "/memory/classes/metadata/mspan/inuse:bytes": + llgoSetUint64(value, mem.MSpanInuse) + case "/memory/classes/metadata/other:bytes": + llgoSetUint64(value, mem.GCSys) + case "/memory/classes/os-stacks:bytes": + llgoSetUint64(value, llgoSaturatingSub(mem.StackSys, mem.StackInuse)) + case "/memory/classes/other:bytes": + llgoSetUint64(value, mem.OtherSys) + case "/memory/classes/profiling/buckets:bytes": + llgoSetUint64(value, mem.BuckHashSys) + case "/memory/classes/total:bytes": + llgoSetUint64(value, mem.Sys) + } +} + +func llgoSetMetricDefault(value *Value, kind ValueKind) { + switch kind { + case KindUint64: + llgoSetUint64(value, 0) + case KindFloat64: + value.kind = KindFloat64 + value.scalar = math.Float64bits(0) + value.pointer = nil + case KindFloat64Histogram: + llgoFloat64HistOrInit(value, llgoMetricDefaultHistBuckets) + default: + *value = Value{} + } +} + +func llgoSetUint64(value *Value, n uint64) { + value.kind = KindUint64 + value.scalar = n + value.pointer = nil +} + +func llgoFloat64HistOrInit(value *Value, buckets []float64) *Float64Histogram { + var hist *Float64Histogram + if value.kind == KindFloat64Histogram && value.pointer != nil { + hist = (*Float64Histogram)(value.pointer) + } else { + hist = new(Float64Histogram) + value.pointer = unsafe.Pointer(hist) + } + value.kind = KindFloat64Histogram + value.scalar = 0 + hist.Buckets = buckets + if len(hist.Counts) != len(buckets)-1 { + hist.Counts = make([]uint64, len(buckets)-1) + } else { + clear(hist.Counts) + } + return hist +} + +func llgoSaturatingSub(a, b uint64) uint64 { + if a < b { + return 0 + } + return a - b +} diff --git a/runtime/build.go b/runtime/build.go index 1450e0ac59..f8df311598 100644 --- a/runtime/build.go +++ b/runtime/build.go @@ -56,4 +56,5 @@ var sourcePatchPkgs = map[string]struct{}{ "crypto/internal/constanttime": {}, "internal/sync": {}, "iter": {}, + "runtime/metrics": {}, } diff --git a/test/std/runtime/metrics/metrics_test.go b/test/std/runtime/metrics/metrics_test.go new file mode 100644 index 0000000000..71c40fe8c5 --- /dev/null +++ b/test/std/runtime/metrics/metrics_test.go @@ -0,0 +1,69 @@ +package metrics_test + +import ( + "runtime/metrics" + "testing" +) + +func TestReadAllMetricKinds(t *testing.T) { + descs := metrics.All() + if len(descs) == 0 { + t.Fatal("metrics.All returned no descriptions") + } + + samples := make([]metrics.Sample, len(descs)) + for i, desc := range descs { + samples[i].Name = desc.Name + } + metrics.Read(samples) + + seen := map[metrics.ValueKind]bool{} + for i, desc := range descs { + value := samples[i].Value + if got := value.Kind(); got != desc.Kind { + t.Fatalf("Read(%q) kind = %d, want %d", desc.Name, got, desc.Kind) + } + checkMetricValue(t, desc.Name, value, desc.Kind) + seen[desc.Kind] = true + } + + for _, kind := range []metrics.ValueKind{ + metrics.KindUint64, + metrics.KindFloat64, + metrics.KindFloat64Histogram, + } { + if !seen[kind] { + t.Fatalf("metrics.All did not include a metric of kind %d", kind) + } + } +} + +func TestReadUnknownMetric(t *testing.T) { + samples := []metrics.Sample{{Name: "/llgo/unknown:things"}} + metrics.Read(samples) + if got := samples[0].Value.Kind(); got != metrics.KindBad { + t.Fatalf("Read unknown metric kind = %d, want %d", got, metrics.KindBad) + } +} + +func checkMetricValue(t *testing.T, name string, value metrics.Value, kind metrics.ValueKind) { + t.Helper() + + switch kind { + case metrics.KindUint64: + _ = value.Uint64() + case metrics.KindFloat64: + _ = value.Float64() + case metrics.KindFloat64Histogram: + hist := value.Float64Histogram() + if hist == nil { + t.Fatalf("Read(%q) returned nil histogram", name) + } + if len(hist.Buckets) != len(hist.Counts)+1 { + t.Fatalf("Read(%q) histogram buckets/counts lengths = %d/%d, want buckets = counts+1", + name, len(hist.Buckets), len(hist.Counts)) + } + default: + t.Fatalf("Read(%q) returned unexpected kind %d", name, kind) + } +}