Files
2026-08-05 16:34:41 -04:00

85 lines
2.8 KiB
Go

package content
import (
"sync/atomic"
"time"
)
// IOStats accumulates disk-contention counters for a single OCI store, so
// one pasted log line explains a field performance report -- the previous
// parallel-blob work was abandoned partly because regression reports were
// unreproducible with the reporter's contention invisible. Counters live on
// OCI rather than being context-attached (the store.WithImageStats idiom)
// because they measure store-level semaphore/mutex contention, not
// per-image state like ImageStats. All fields are atomic since every
// increment runs concurrently by design.
type IOStats struct {
BlobsWritten atomic.Int64
BlobsCached atomic.Int64
BlobBytesWritten atomic.Int64
BlobSemWaitNanos atomic.Int64
BlobPeakInFlight atomic.Int64
IndexWrites atomic.Int64
IndexDurableWrites atomic.Int64
IndexBytesWritten atomic.Int64
IndexLockWaitNanos atomic.Int64
// blobInFlight backs BlobPeakInFlight. It is never reported: the
// instantaneous value at the end of a run is always 0 and says nothing.
blobInFlight atomic.Int64
}
// IOStatsSnapshot is a plain-value copy of IOStats, so formatting code never
// touches atomics and can be unit-tested with a literal.
type IOStatsSnapshot struct {
BlobsWritten int64
BlobsCached int64
BlobBytesWritten int64
BlobSemWait time.Duration
BlobPeakInFlight int64
IndexWrites int64
IndexDurableWrites int64
IndexBytesWritten int64
IndexLockWait time.Duration
}
// Snapshot reads every counter. It is not atomic as a whole -- counters are
// read one at a time and may skew relative to each other if a sync is still
// running. Callers report it after all work has finished, where that does
// not matter.
func (s *IOStats) Snapshot() IOStatsSnapshot {
return IOStatsSnapshot{
BlobsWritten: s.BlobsWritten.Load(),
BlobsCached: s.BlobsCached.Load(),
BlobBytesWritten: s.BlobBytesWritten.Load(),
BlobSemWait: time.Duration(s.BlobSemWaitNanos.Load()),
BlobPeakInFlight: s.BlobPeakInFlight.Load(),
IndexWrites: s.IndexWrites.Load(),
IndexDurableWrites: s.IndexDurableWrites.Load(),
IndexBytesWritten: s.IndexBytesWritten.Load(),
IndexLockWait: time.Duration(s.IndexLockWaitNanos.Load()),
}
}
// enterBlob records that a blob write has just acquired a permit, updating
// the high-water mark with a CAS loop. Must be paired with exitBlob.
func (s *IOStats) enterBlob() {
cur := s.blobInFlight.Add(1)
for {
peak := s.BlobPeakInFlight.Load()
if cur <= peak || s.BlobPeakInFlight.CompareAndSwap(peak, cur) {
return
}
}
}
// exitBlob records that a blob write has released its permit.
func (s *IOStats) exitBlob() {
s.blobInFlight.Add(-1)
}
// addSemWait records time spent blocked acquiring blobSem.
func (s *IOStats) addSemWait(d time.Duration) {
s.BlobSemWaitNanos.Add(int64(d))
}