// Copyright (C) 2022 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package textui import ( "fmt" "runtime" "sync" "time" ) // LiveMemUse is an object that stringifies as the live memory use of // the program. // // It is intended to be used with dlog by attaching it as a field, so // that all log lines include the current memory use: // // ctx = dlog.WithField(ctx, "mem", new(textui.LiveMemUse)) type LiveMemUse struct { mu sync.Mutex stats runtime.MemStats last time.Time } var _ fmt.Stringer = (*LiveMemUse)(nil) // LiveMemUseUpdateInterval is the shortest interval on which // LiveMemUse is willing to update; we have this minimum interval // because it stops the world to collect memory statistics, so we // don't want to be updating the statistics too often. var LiveMemUseUpdateInterval = Tunable(1 * time.Second) // String implements fmt.Stringer. func (o *LiveMemUse) String() string { o.mu.Lock() // runtime.ReadMemStats() calls stopTheWorld(), so we want to // rate-limit how often we call it. if now := time.Now(); now.Sub(o.last) > LiveMemUseUpdateInterval { runtime.ReadMemStats(&o.stats) o.last = now } // runtime.MemStats only knows about memory managed by the Go runtime; // even for a pure Go program, there's also // // - memory mapped to the executable itself // - vDSO and friends // // But those are pretty small, just a few MiB. // // OK, so: memory managed by the Go runtime. runtime.MemStats is pretty // obtuse, I think it was designed more for "debugging the Go runtime" // than "monitoring the behavior of a Go program". From the Go // runtime's perspective, regions of the virtual address space are in // one of 4 states (see `runtime/mem.go`): // // - None : not mapped // // - Reserved : mapped, but without r/w permissions (PROT_NONE); so // this region isn't actually backed by anything // // - Prepared : mapped, but allowed to be collected by the OS // (MADV_FREE, or MADV_DONTNEED on systems without MADV_FREE); so // this region may or may not actually be backed by anything. // // - Ready : mapped, ready to be used // // Normal tools count Reserved+Prepared+Ready toward the VSS (which is a // little silly, when inspecting /proc/{pid}/maps to calculate the VSS, // IMO they should exclude maps without r/w permissions, which would // exclude Reserved), but we all know that VSS numbers are over // inflated. And RSS only useful if we fit in RAM and don't spill to // swap (this is being written for btrfs-rec, which is quite likely to // consume all RAM on a laptop). Useful numbers are Ready and Prepared; // as I said above, outside tools reporting Ready+Prepared would be easy // and useful, but none do; but I don't think outside tools have a way // to distinguish between Ready and Prepared (unless you can detect // MADV_FREE/MADV_DONTNEED in /proc/{pid}/smaps?). // // Of the 3 mapped states, here's how we get them from runtime: // // - Reserved : AFAICT, you can't :( // // - Prepared : `runtime.MemStats.HeapReleased` // // - Ready : `runtime.MemStats.Sys - runtime.MemStats.HeapReleased` // (that is, runtime.MemStats.Sys is Prepared+Ready) // // It's a bummer that we can't get Reserved from runtime, but as I've // said, it's not super useful; it's only use would really be // cross-referencing runtime's numbers against the VSS. // // The godocs for runtime.MemStats.Sys say "It's likely that not all of // the virtual address space is backed by physical memory at any given // moment, though in general it all was at some point." That's both // confusing and a lie. It's confusing because it doesn't give you // hooks to find out more; it could have said that this is // Ready+Prepared and that Prepared is the portion of the space that // might not be backed by physical memory, but instead it wants you to // throw your hands up and say "this is too weird for me to understand". // It's a lie because "in general it all was at some point" implies that // all Prepared memory was previously Ready, which is false; it can go // None->Reserved->Prepared (but it only does that Reserved->Prepared // transition if it thinks it will need to transition it to Ready very // soon, so maybe the doc author though that was negligible?). // // Now, those are still pretty opaque numbers; most of runtime.MemStats // goes toward accounting for what's going on inside of Ready space. // // For our purposes, we don't care too much about specifics of how Ready // space is being used; just how much is "actually storing data", vs // "overhead from heap-fragmentation", vs "idle". var ( // We're going to add up all of the `o.stats.{thing}Sys` // variables and check that against `o.stats.Sys`, in order to // make sure that we're not missing any {thing} when adding up // `inuse`. calcSys = o.stats.HeapSys + o.stats.StackSys + o.stats.MSpanSys + o.stats.MCacheSys + o.stats.BuckHashSys + o.stats.GCSys + o.stats.OtherSys inuse = o.stats.HeapInuse + o.stats.StackInuse + o.stats.MSpanInuse + o.stats.MCacheInuse + o.stats.BuckHashSys + o.stats.GCSys + o.stats.OtherSys ) if calcSys != o.stats.Sys { panic("should not happen") } prepared := o.stats.HeapReleased ready := o.stats.Sys - prepared readyFragOverhead := o.stats.HeapInuse - o.stats.HeapAlloc readyData := inuse - readyFragOverhead readyIdle := ready - inuse o.mu.Unlock() return Sprintf("Ready+Prepared=%.1f (Ready=%.1f (data:%.1f + fragOverhead:%.1f + idle:%.1f) ; Prepared=%.1f)", IEC(ready+prepared, "B"), IEC(ready, "B"), IEC(readyData, "B"), IEC(readyFragOverhead, "B"), IEC(readyIdle, "B"), IEC(prepared, "B")) }