diff options
author | Luke Shumaker <lukeshu@lukeshu.com> | 2022-12-30 22:17:06 -0700 |
---|---|---|
committer | Luke Shumaker <lukeshu@lukeshu.com> | 2022-12-30 22:17:06 -0700 |
commit | 9971e38110d5f90d15c7b78f396f2638b3952a96 (patch) | |
tree | 28692225122d6d9c91d826801a4986d1c850744d /lib/textui/log_memstats.go | |
parent | bfe111c950da328b673ed4e3f8da0503bbd793d8 (diff) | |
parent | 3d0937e9ab148c074922b0d46ed33bdbcbef85b5 (diff) |
Merge branch 'lukeshu/log'
Diffstat (limited to 'lib/textui/log_memstats.go')
-rw-r--r-- | lib/textui/log_memstats.go | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/lib/textui/log_memstats.go b/lib/textui/log_memstats.go new file mode 100644 index 0000000..39733c6 --- /dev/null +++ b/lib/textui/log_memstats.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Luke Shumaker <lukeshu@lukeshu.com> +// +// SPDX-License-Identifier: GPL-2.0-or-later + +package textui + +import ( + "fmt" + "runtime" + "sync" + "time" +) + +type LiveMemUse struct { + mu sync.Mutex + stats runtime.MemStats + last time.Time +} + +var _ fmt.Stringer = (*LiveMemUse)(nil) + +const liveMemUseUpdateInterval = 1 * time.Second + +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")) +} |