mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-12 16:05:42 +00:00
Rebase of #1881 by @SaarMesh-Bot onto current master. Their three commits are preserved, two of them cherry-picked with authorship intact; the sweep itself had to be regenerated. Opened as a new PR rather than force-pushing their branch. Closes #1881 once merged. Addresses parts 1 and 3 of #1859; part 2 landed as #1937. ## Why regenerated rather than merged The sweep in #1881 was cut on 2026-09-02 07:13 and roughly forty PRs landed after it, so it went `CONFLICTING/DIRTY`. Re-running `gofmt` on current master is cheaper and less error-prone than resolving 72 conflicts that are all whitespace. The drift it fixes also grew in the meantime: 66 files now, against 72 then, but spread differently. ## The three commits 1. **`style(#1859)`** — `gofmt -w` across the 14 modules. 66 files. 2. **`test(#1859)`** — @SaarMesh-Bot's fix for the one `go vet` copylocks finding, `cmd/ingestor/coverage_boost_test.go`: the range variable copied a `Config` embedding `sync.Once`. Cherry-picked unchanged. 3. **`ci(#1859)`** — @SaarMesh-Bot's CI step that fails on gofmt drift or vet findings, plus `.git-blame-ignore-revs`. Cherry-picked with one change, noted in the commit message: the ignore file pointed at `04bc80ee`, the sweep commit on their branch, which does not exist on this base and would make `git blame --ignore-revs-file` error. Repointed at `d3a02599`, the sweep here. ## Verification The claim "formatting only" is checked twice rather than asserted: - Every changed file is byte-identical to `gofmt(previous content)`. 0 of 66 deviate. - With line comments and all whitespace stripped, 0 of 66 files differ, so no code outside comments changed. 14 of the 66 also show doc-comment reflow. Since Go 1.19 `gofmt` re-indents indented comment blocks to tabs and inserts a blank comment line before them; the behavior matrix above `resolveHopWithContext` in `cmd/ingestor/path_resolver.go` is a clear example. That is gofmt's own output, not an edit, but it is worth naming because it makes the diff look larger than "whitespace" suggests. The gate was run locally exactly as the workflow runs it: `gofmt` clean, and `go vet` clean in all 14 modules, including `cmd/ingestor` which is what commit 2 fixes. Suites: `cmd/server` ok (80.7s), `internal/packetpath` ok (2.3s), `cmd/ingestor` passes except `TestWriteStatsAtomic_SymlinkAtDestIsReplaced`, which fails identically on bare master with "A required privilege is not held by the client" (Windows symlink privilege on my host, not code). ## Sequencing This should go last in the queue. The sweep touches 66 files, so merging it before the remaining open Go PRs gives each of them a conflict about nothing but formatting. After it lands the gate is active, and any PR with drift fails CI until it runs `gofmt -w`. Excluded from the sweep: the misnamed `Dockerfile.go`, which is a Dockerfile that gofmt cannot parse (the workflow excludes it too), and `docs/DEPLOYMENT.md`, which a case-insensitive filesystem surfaces as a spurious modification against `docs/deployment.md` and is unrelated. --------- Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
4.3 KiB
Go
133 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// MemorySnapshot is a point-in-time view of process memory across several
|
|
// vantage points. Values are in MB (1024*1024 bytes), rounded to one decimal.
|
|
//
|
|
// Field invariants (typical, not guaranteed under exotic conditions):
|
|
//
|
|
// processRSSMB >= goSysMB >= goHeapInuseMB >= storeDataMB
|
|
//
|
|
// - processRSSMB is what the kernel charges the process (resident set).
|
|
// Read from /proc/self/status `VmRSS:` on Linux; falls back to goSysMB
|
|
// on other platforms or when /proc is unavailable.
|
|
// - goSysMB is the total memory obtained from the OS by the Go runtime
|
|
// (heap, stacks, GC metadata, mspans, mcache, etc.). Includes
|
|
// fragmentation and unused-but-mapped span overhead.
|
|
// - goHeapInuseMB is the live, in-use Go heap (HeapInuse). Excludes
|
|
// idle spans and runtime overhead.
|
|
// - storeDataMB is the in-store packet byte estimate (transmissions +
|
|
// observations). Subset of HeapInuse. Does not include index maps,
|
|
// analytics caches, broadcast queues, or runtime overhead. Used as
|
|
// the input to the eviction watermark.
|
|
//
|
|
// processRSSMB and storeDataMB are monotonic only relative to ingest +
|
|
// eviction; both can shrink when packets age out. goHeapInuseMB and goSysMB
|
|
// fluctuate with GC.
|
|
//
|
|
// cgoBytesMB intentionally absent: this build uses the pure-Go
|
|
// modernc.org/sqlite driver, so there is no cgo allocator to measure.
|
|
// Reintroduce only if we ever switch back to mattn/go-sqlite3.
|
|
type MemorySnapshot struct {
|
|
ProcessRSSMB float64 `json:"processRSSMB"`
|
|
GoHeapInuseMB float64 `json:"goHeapInuseMB"`
|
|
GoSysMB float64 `json:"goSysMB"`
|
|
StoreDataMB float64 `json:"storeDataMB"`
|
|
}
|
|
|
|
// rssCache rate-limits the /proc/self/status read. Go memory stats are
|
|
// already cached by Server.getMemStats (5s TTL). We use a tighter 1s TTL
|
|
// here so processRSSMB stays reasonably fresh during ops debugging
|
|
// without paying the syscall cost on every /api/stats hit.
|
|
var (
|
|
rssCacheMu sync.Mutex
|
|
rssCacheValueMB float64
|
|
rssCacheCachedAt time.Time
|
|
)
|
|
|
|
const rssCacheTTL = 1 * time.Second
|
|
|
|
// getMemorySnapshot composes a MemorySnapshot using the Server's existing
|
|
// runtime.MemStats cache (5s TTL, used by /api/health and /api/perf too)
|
|
// plus a rate-limited /proc RSS read. storeDataMB is supplied by the
|
|
// caller because the packet store is the source of truth.
|
|
func (s *Server) getMemorySnapshot(storeDataMB float64) MemorySnapshot {
|
|
ms := s.getMemStats()
|
|
|
|
rssCacheMu.Lock()
|
|
if time.Since(rssCacheCachedAt) > rssCacheTTL {
|
|
rssCacheValueMB = readProcRSSMB()
|
|
rssCacheCachedAt = time.Now()
|
|
}
|
|
rssMB := rssCacheValueMB
|
|
rssCacheMu.Unlock()
|
|
|
|
if rssMB <= 0 {
|
|
// Fallback when /proc is unavailable (non-Linux, sandboxes, etc.).
|
|
// runtime.Sys is an upper bound on Go-attributable memory and a
|
|
// reasonable proxy for pure-Go builds.
|
|
rssMB = float64(ms.Sys) / 1048576.0
|
|
}
|
|
|
|
return MemorySnapshot{
|
|
ProcessRSSMB: roundMB(rssMB),
|
|
GoHeapInuseMB: roundMB(float64(ms.HeapInuse) / 1048576.0),
|
|
GoSysMB: roundMB(float64(ms.Sys) / 1048576.0),
|
|
StoreDataMB: roundMB(storeDataMB),
|
|
}
|
|
}
|
|
|
|
// readProcRSSMB parses /proc/self/status for the VmRSS line. Returns 0 on
|
|
// any failure (file missing, malformed line, parse error) — the caller
|
|
// then uses a runtime fallback. Linux only; macOS/Windows return 0.
|
|
//
|
|
// Safety notes (djb): the file path is hard-coded, no untrusted input is
|
|
// concatenated. We bound the read at 8 KiB (the whole status file is
|
|
// well under 4 KiB on modern kernels) so a corrupt /proc can't OOM us.
|
|
// We only parse digits with strconv; no shell, no exec, no format strings.
|
|
func readProcRSSMB() float64 {
|
|
const maxStatusBytes = 8 * 1024
|
|
f, err := os.Open("/proc/self/status")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := make([]byte, maxStatusBytes)
|
|
n, err := f.Read(buf)
|
|
if err != nil && n == 0 {
|
|
return 0
|
|
}
|
|
for _, line := range strings.Split(string(buf[:n]), "\n") {
|
|
if !strings.HasPrefix(line, "VmRSS:") {
|
|
continue
|
|
}
|
|
// Format: "VmRSS:\t 123456 kB"
|
|
fields := strings.Fields(line[len("VmRSS:"):])
|
|
if len(fields) < 2 {
|
|
return 0
|
|
}
|
|
kb, err := strconv.ParseFloat(fields[0], 64)
|
|
if err != nil || kb < 0 {
|
|
return 0
|
|
}
|
|
// Unit is kB per kernel convention; convert to MB.
|
|
return kb / 1024.0
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func roundMB(v float64) float64 {
|
|
if v < 0 {
|
|
return 0
|
|
}
|
|
return float64(int64(v*10+0.5)) / 10.0
|
|
}
|