Skip to content
quarry Artifact [8462d71bac]
Login

Artifact [8462d71bac]

Artifact 8462d71bac8536b3f43e6a794398d43018e10663c525ed717f99fda888efb061:


package api

import (
	"fmt"
	"time"

	"craftdesign.group/quarry/internal/obs"
	"craftdesign.group/quarry/internal/zitcli"
)

// opsMemory is obs.Memory with what the forge can account for: the
// parts of the heap it measures. The rest is "not accounted", and a
// large rest is what says leak rather than busy.
type opsMemory struct {
	obs.Memory
	Accounted []opsMemoryPart `json:"accounted"`
	// Unaccounted is the heap minus every accounted part, 0 when the
	// parts exceed it (they are measured at different moments).
	Unaccounted uint64 `json:"unaccounted"`
}

type opsMemoryPart struct {
	Name  string `json:"name"`
	Bytes uint64 `json:"bytes"`
	Limit uint64 `json:"limit,omitempty"`
}

func opsMemoryNow() opsMemory {
	m := opsMemory{Memory: obs.MemoryNow()}
	used, limit := zitcli.ForgeCacheUse()
	if used > 0 || limit > 0 {
		m.Accounted = append(m.Accounted, opsMemoryPart{Name: "store caches", Bytes: uint64(max(used, 0)), Limit: uint64(max(limit, 0))})
	}
	var sum uint64
	for _, p := range m.Accounted {
		sum += p.Bytes
	}
	if m.Heap > sum {
		m.Unaccounted = m.Heap - sum
	}
	return m
}

// swapWorry is the swap that is a finding on its own: past it, pages of
// the heap are being read back from disk on the request path.
const swapWorry = 1 << 30

// memoryFindings is the memory half of the verdict. A heap over the
// target is already a fault, before the cap: the collector then runs
// almost without pause and the heap still does not come down.
func memoryFindings(m opsMemory) []opsFinding {
	var out []opsFinding
	over := m.Limit > 0 && m.Heap > m.Limit
	nearCap := m.Cap > 0 && m.Heap > m.Cap/10*9
	if over || nearCap {
		text := "heap " + gb(m.Heap)
		if m.Limit > 0 {
			text += fmt.Sprintf(", %.1f× the %s target", float64(m.Heap)/float64(m.Limit), gb(m.Limit))
		}
		if m.Cap > 0 {
			text += ", cap " + gb(m.Cap)
		}
		if !m.OverSince.IsZero() {
			text += ", over the target for " + since(m.OverSince)
		}
		if m.Swap > 0 {
			text += "; " + gb(m.Swap) + " swapped"
		}
		text += fmt.Sprintf("; %s of it not accounted for", gb(m.Unaccounted))
		out = append(out, opsFinding{About: "memory", Severity: "bad", Text: text})
	} else if m.Swap > swapWorry {
		out = append(out, opsFinding{About: "memory", Severity: "warn",
			Text: gb(m.Swap) + " of the heap swapped out while it is under its target"})
	}
	return out
}

func gb(b uint64) string {
	switch {
	case b >= 1<<30:
		return fmt.Sprintf("%.1f GB", float64(b)/(1<<30))
	case b >= 1<<20:
		return fmt.Sprintf("%.0f MB", float64(b)/(1<<20))
	default:
		return fmt.Sprintf("%d KB", b>>10)
	}
}

func since(t time.Time) string {
	d := time.Since(t).Round(time.Minute)
	if d >= time.Hour {
		return fmt.Sprintf("%dh%02dm", int(d.Hours()), int(d.Minutes())%60)
	}
	return fmt.Sprintf("%dm", int(d.Minutes()))
}