Files
sing-box-extended-mirror/service/powerreport/recorder.go
T

863 lines
24 KiB
Go

package powerreport
import (
"cmp"
"encoding/json"
"os"
"path/filepath"
"runtime/metrics"
"runtime/pprof"
"slices"
"sync"
"sync/atomic"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/memory"
"github.com/sagernet/sing/service/pause"
)
const (
DraftDirectoryName = "power_draft"
ReportsDirectoryName = "power_reports"
timelineFileName = "timeline.jsonl"
eventsFileName = "events.jsonl"
metadataFileName = "metadata.json"
logFileName = "go.log"
goroutineProfileFileName = "goroutine.pb.gz"
defaultGateInterval = 5 * time.Second
defaultSampleInterval = time.Minute
defaultFlushInterval = 15 * time.Minute
defaultFallbackInterval = 10 * time.Minute
activityRefreshNano = int64(time.Second)
rowCapacity = 4096
eventCapacity = 8192
maxDNSDomains = 512
maxRowDNSDomains = 8
maxProfiles = 3
profileMinInterval = 2 * time.Minute
profileMaxDuration = 10 * time.Second
)
type Options struct {
BasePath string
Logger logger.Logger
Metadata any
OwnerCallback func(path string)
LogCallback func() []byte
ProfileCallback func(path string)
GateInterval time.Duration
SampleInterval time.Duration
FlushInterval time.Duration
FallbackInterval time.Duration
}
type Recorder struct {
draftPath string
logger logger.Logger
metadata any
ownerCallback func(path string)
logCallback func() []byte
profileCallback func(path string)
gateNano int64
sampleNano int64
flushInterval time.Duration
fallbackInterval time.Duration
baseTime time.Time
_ [64]byte
lastActivity atomic.Int64
lastActivityWall atomic.Int64
_ [64]byte
wakePending atomic.Bool
lastWakeNano atomic.Int64
lastSampleAt atomic.Int64
pendingBreak atomic.Pointer[breakRecord]
notify chan struct{}
dnsQueries atomic.Uint64
connectionsOpened atomic.Uint64
networkPathUpdates atomic.Uint64
trafficAccess sync.RWMutex
traffic map[trafficKey]*TrafficCounter
dnsAccess sync.Mutex
dnsDomains map[string]uint64
access sync.Mutex
networkType string
networkPath string
rows []timelineRow
events []eventRecord
previous previousSample
lastFlushAt time.Time
started bool
closed bool
metricsSamples []metrics.Sample
lastDeviceState deviceState
deviceStateValid bool
profiles []profileEntry
lastProfileAt time.Time
profileFile *os.File
profileTimer *time.Timer
awake bool
wakeAt time.Time
wakeUsage systemUsage
wakeDNSQueries uint64
wakeConnections uint64
windowRate float64
done chan struct{}
workerDone chan struct{}
}
type breakRecord struct {
at time.Time
idleMS int64
wallIdleMS int64
afterWake bool
sinceWakeMS int64
direction Direction
size int
by *Attribution
}
type previousSample struct {
at time.Time
usage systemUsage
gcSeconds float64
gcCycles uint64
absoluteTime int64
continuousTime int64
interfaces map[string]interfaceCounters
dnsQueries uint64
connectionsOpened uint64
networkPathUpdates uint64
traffic map[trafficKey]trafficBytes
}
type profileEntry struct {
rate float64
path string
}
type profileRequest struct {
writePath string
removePath string
}
func NewRecorder(options Options) *Recorder {
recorderLogger := options.Logger
if recorderLogger == nil {
recorderLogger = logger.NOP()
}
gateInterval := options.GateInterval
if gateInterval == 0 {
gateInterval = defaultGateInterval
}
sampleInterval := options.SampleInterval
if sampleInterval == 0 {
sampleInterval = defaultSampleInterval
}
flushInterval := options.FlushInterval
if flushInterval == 0 {
flushInterval = defaultFlushInterval
}
fallbackInterval := options.FallbackInterval
if fallbackInterval == 0 {
fallbackInterval = defaultFallbackInterval
}
return &Recorder{
draftPath: filepath.Join(options.BasePath, DraftDirectoryName),
logger: recorderLogger,
metadata: options.Metadata,
ownerCallback: options.OwnerCallback,
logCallback: options.LogCallback,
profileCallback: options.ProfileCallback,
gateNano: int64(gateInterval),
sampleNano: int64(sampleInterval),
flushInterval: flushInterval,
fallbackInterval: fallbackInterval,
baseTime: time.Now(),
notify: make(chan struct{}, 1),
traffic: make(map[trafficKey]*TrafficCounter),
dnsDomains: make(map[string]uint64),
metricsSamples: []metrics.Sample{
{Name: "/cpu/classes/gc/total:cpu-seconds"},
{Name: "/sched/goroutines:goroutines"},
{Name: "/gc/cycles/total:gc-cycles"},
{Name: "/memory/classes/total:bytes"},
{Name: "/gc/heap/live:bytes"},
},
done: make(chan struct{}),
workerDone: make(chan struct{}),
}
}
func (r *Recorder) Start() error {
r.access.Lock()
defer r.access.Unlock()
if r.started {
return nil
}
DiscardDraft(filepath.Dir(r.draftPath))
err := os.MkdirAll(r.draftPath, 0o777)
if err != nil {
return E.Cause(err, "create power report draft directory")
}
r.chown(r.draftPath)
if r.metadata != nil {
metadataContent, marshalErr := json.Marshal(r.metadata)
if marshalErr == nil {
metadataPath := filepath.Join(r.draftPath, metadataFileName)
os.WriteFile(metadataPath, metadataContent, 0o666)
r.chown(metadataPath)
}
}
now := time.Now()
r.resetPreviousLocked(now)
r.lastSampleAt.Store(int64(now.Sub(r.baseTime)))
r.lastFlushAt = now
r.started = true
r.beginWakeWindowLocked(now)
r.updateDeviceStateLocked(now)
go r.worker()
return nil
}
func (r *Recorder) Close() error {
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return nil
}
r.closed = true
r.access.Unlock()
close(r.done)
<-r.workerDone
now := time.Now()
r.access.Lock()
r.stopProfileLocked()
r.consumeBreakLocked()
r.sampleLocked(now)
r.flushLocked(now)
r.access.Unlock()
r.writeProfiles()
r.writeLog()
finalizeDraft(r.draftPath)
return nil
}
func (r *Recorder) writeLog() {
if r.logCallback == nil {
return
}
content := r.logCallback()
if len(content) == 0 {
return
}
logPath := filepath.Join(r.draftPath, logFileName)
err := os.WriteFile(logPath, content, 0o666)
if err != nil {
return
}
r.chown(logPath)
}
// Touch reports one I/O activity: one read or write call, or one batched receive or send
// syscall on paths that batch packets. size is the size of the first packet of the activity
// and characterizes what ended an idle period; it is not accumulated. Volume totals come from
// the sampled interface counters instead.
func (r *Recorder) Touch(direction Direction, size int, by *Attribution) {
now := time.Now()
nowNano := int64(now.Sub(r.baseTime))
if r.wakePending.Load() && r.wakePending.CompareAndSwap(true, false) {
record := r.newBreakRecord(now, nowNano, direction, size, by)
record.afterWake = true
record.sinceWakeMS = (nowNano - r.lastWakeNano.Load()) / int64(time.Millisecond)
r.pendingBreak.Store(record)
r.notifyWorker()
return
}
lastNano := r.lastActivity.Load()
if nowNano-lastNano < activityRefreshNano {
return
}
record := r.newBreakRecord(now, nowNano, direction, size, by)
if record.idleMS >= r.gateNano/int64(time.Millisecond) {
r.pendingBreak.Store(record)
r.notifyWorker()
} else if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
r.notifyWorker()
}
}
func (r *Recorder) newBreakRecord(now time.Time, nowNano int64, direction Direction, size int, by *Attribution) *breakRecord {
previousNano := r.lastActivity.Swap(nowNano)
previousWall := r.lastActivityWall.Swap(now.UnixNano())
record := &breakRecord{
at: now,
direction: direction,
size: size,
by: by,
}
if previousNano != 0 {
record.idleMS = (nowNano - previousNano) / int64(time.Millisecond)
record.wallIdleMS = (now.UnixNano() - previousWall) / int64(time.Millisecond)
}
return record
}
func (r *Recorder) CountDNSQuery(domain string) {
r.dnsQueries.Add(1)
if domain == "" {
return
}
r.dnsAccess.Lock()
count, found := r.dnsDomains[domain]
if found || len(r.dnsDomains) < maxDNSDomains {
r.dnsDomains[domain] = count + 1
}
r.dnsAccess.Unlock()
}
func (r *Recorder) RecordPauseEvent(event int) {
switch event {
case pause.EventDevicePaused:
r.recordPlatformEvent(eventTypeDevicePause)
case pause.EventDeviceWake:
r.recordPlatformEvent(eventTypeDeviceWake)
case pause.EventNetworkPause:
r.recordPlatformEvent(eventTypeNetworkPause)
case pause.EventNetworkWake:
r.recordPlatformEvent(eventTypeNetworkWake)
}
}
func (r *Recorder) CountConnectionOpened() {
r.connectionsOpened.Add(1)
}
// The service log prefixes each line with whole monotonic seconds since createdAt, so a
// line at [N] sits at logBaseMonoMS + N*1000 on the monoMS axis of events and timeline rows.
func (r *Recorder) RecordServiceStart(createdAt time.Time) {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
event := r.newEvent(eventTypeService, now)
event.LogBaseMonoMS = int64(createdAt.Sub(r.baseTime) / time.Millisecond)
r.events = append(r.events, event)
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) RecordDeviceSleep() {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
r.wakePending.Store(false)
r.stopProfileLocked()
event := r.newEvent(eventTypeSleep, now)
if r.awake {
event.Window = r.endWakeWindowLocked(now)
}
r.events = append(r.events, event)
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) RecordDeviceWake() {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
r.events = append(r.events, r.newEvent(eventTypeWake, now))
var request *profileRequest
if !r.awake {
r.beginWakeWindowLocked(now)
r.lastWakeNano.Store(int64(now.Sub(r.baseTime)))
r.wakePending.Store(true)
request = r.requestProfileLocked(now, r.windowRate)
r.windowRate = 0
}
r.access.Unlock()
if request != nil {
r.startProfile(request)
}
r.notifyWorker()
}
func (r *Recorder) beginWakeWindowLocked(now time.Time) {
r.awake = true
r.wakeAt = now
r.wakeUsage = readSystemUsage()
r.wakeDNSQueries = r.dnsQueries.Load()
r.wakeConnections = r.connectionsOpened.Load()
}
func (r *Recorder) endWakeWindowLocked(now time.Time) *wakeWindow {
r.awake = false
usage := readSystemUsage()
window := &wakeWindow{
AwakeMS: int64(now.Sub(r.wakeAt) / time.Millisecond),
DNSQueries: r.dnsQueries.Load() - r.wakeDNSQueries,
ConnectionsOpened: r.connectionsOpened.Load() - r.wakeConnections,
}
if usage.valid && r.wakeUsage.valid {
window.CPUUserMS = (usage.userTime - r.wakeUsage.userTime) / int64(time.Millisecond)
window.CPUSystemMS = (usage.systemTime - r.wakeUsage.systemTime) / int64(time.Millisecond)
window.EnergyNanojoules = usage.energyNanojoules - r.wakeUsage.energyNanojoules
window.PackageIdleWakeups = usage.packageIdleWakeups - r.wakeUsage.packageIdleWakeups
window.InterruptWakeups = usage.interruptWakeups - r.wakeUsage.interruptWakeups
}
r.windowRate = profileRate(window.EnergyNanojoules, window.CPUUserMS+window.CPUSystemMS, now.Sub(r.wakeAt))
return window
}
func (r *Recorder) recordPlatformEvent(eventType string) {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
r.events = append(r.events, r.newEvent(eventType, now))
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) UpdateNetworkType(networkType string) {
now := time.Now()
r.access.Lock()
if r.closed || r.networkType == networkType {
r.access.Unlock()
return
}
r.networkType = networkType
event := r.newEvent(eventTypeNetwork, now)
event.NetworkType = networkType
r.events = append(r.events, event)
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) UpdateNetworkPath(description string) {
r.networkPathUpdates.Add(1)
now := time.Now()
r.access.Lock()
if r.closed || r.networkPath == description {
r.access.Unlock()
return
}
r.networkPath = description
event := r.newEvent(eventTypePath, now)
event.NetworkPath = description
r.events = append(r.events, event)
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) newEvent(eventType string, now time.Time) eventRecord {
return eventRecord{
Type: eventType,
At: now.UTC().Format(time.RFC3339),
MonoMS: int64(now.Sub(r.baseTime) / time.Millisecond),
}
}
func (r *Recorder) notifyWorker() {
select {
case r.notify <- struct{}{}:
default:
}
}
func (r *Recorder) worker() {
defer close(r.workerDone)
timer := time.NewTimer(r.fallbackInterval)
defer timer.Stop()
for {
select {
case <-r.done:
return
case <-r.notify:
case <-timer.C:
timer.Reset(r.fallbackInterval)
}
r.process()
}
}
func (r *Recorder) process() {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
r.consumeBreakLocked()
var request *profileRequest
nowNano := int64(now.Sub(r.baseTime))
if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
r.lastSampleAt.Store(nowNano)
request = r.sampleLocked(now)
}
if now.Sub(r.lastFlushAt) >= r.flushInterval || len(r.rows) >= rowCapacity || len(r.events) >= eventCapacity {
r.flushLocked(now)
}
r.access.Unlock()
if request != nil {
r.startProfile(request)
}
}
func (r *Recorder) consumeBreakLocked() {
record := r.pendingBreak.Swap(nil)
if record == nil {
return
}
event := r.newEvent(eventTypeBreak, record.at)
event.IdleMS = record.idleMS
event.WallIdleMS = record.wallIdleMS
event.AfterWake = record.afterWake
event.SinceWakeMS = record.sinceWakeMS
event.Direction = record.direction.String()
event.Size = record.size
event.NetworkType = r.networkType
event.By = record.by
r.events = append(r.events, event)
}
func (r *Recorder) resetPreviousLocked(now time.Time) {
metrics.Read(r.metricsSamples)
r.previous = previousSample{
at: now,
usage: readSystemUsage(),
gcSeconds: r.metricsSamples[0].Value.Float64(),
gcCycles: r.metricsSamples[2].Value.Uint64(),
interfaces: readInterfaceCounters(),
dnsQueries: r.dnsQueries.Load(),
connectionsOpened: r.connectionsOpened.Load(),
networkPathUpdates: r.networkPathUpdates.Load(),
traffic: r.snapshotTrafficLocked(),
}
r.previous.absoluteTime, r.previous.continuousTime = readClocks()
}
func (r *Recorder) sampleLocked(now time.Time) *profileRequest {
previous := r.previous
r.resetPreviousLocked(now)
current := &r.previous
row := timelineRow{
From: previous.at.UTC().Format(time.RFC3339),
To: now.UTC().Format(time.RFC3339),
MonoMS: int64(now.Sub(r.baseTime) / time.Millisecond),
AwakeMS: int64(now.Sub(previous.at) / time.Millisecond),
CPUGCMS: int64((current.gcSeconds - previous.gcSeconds) * 1000),
Goroutines: r.metricsSamples[1].Value.Uint64(),
GCCycles: current.gcCycles - previous.gcCycles,
GoMemoryBytes: r.metricsSamples[3].Value.Uint64(),
GoHeapLiveBytes: r.metricsSamples[4].Value.Uint64(),
DNSQueries: current.dnsQueries - previous.dnsQueries,
DNSDomains: r.collectDNSDomains(),
ConnectionsOpened: current.connectionsOpened - previous.connectionsOpened,
Traffic: trafficDeltas(current.traffic, previous.traffic),
NetworkType: r.networkType,
NetworkPathUpdates: current.networkPathUpdates - previous.networkPathUpdates,
}
if memory.TotalAvailable() {
row.MemoryBytes = memory.Total()
}
if current.usage.valid && previous.usage.valid {
row.CPUUserMS = (current.usage.userTime - previous.usage.userTime) / int64(time.Millisecond)
row.CPUSystemMS = (current.usage.systemTime - previous.usage.systemTime) / int64(time.Millisecond)
row.CPUPerformanceMS = (current.usage.performanceUserTime - previous.usage.performanceUserTime +
current.usage.performanceSystemTime - previous.usage.performanceSystemTime) / int64(time.Millisecond)
row.PackageIdleWakeups = current.usage.packageIdleWakeups - previous.usage.packageIdleWakeups
row.InterruptWakeups = current.usage.interruptWakeups - previous.usage.interruptWakeups
row.EnergyNanojoules = current.usage.energyNanojoules - previous.usage.energyNanojoules
row.PerformanceEnergyNanojoules = current.usage.performanceEnergyNanojoules - previous.usage.performanceEnergyNanojoules
row.DiskBytesWritten = current.usage.diskBytesWritten - previous.usage.diskBytesWritten
qos := qosBreakdown{
DefaultMS: (current.usage.qosDefaultTime - previous.usage.qosDefaultTime) / int64(time.Millisecond),
MaintenanceMS: (current.usage.qosMaintenanceTime - previous.usage.qosMaintenanceTime) / int64(time.Millisecond),
BackgroundMS: (current.usage.qosBackgroundTime - previous.usage.qosBackgroundTime) / int64(time.Millisecond),
UtilityMS: (current.usage.qosUtilityTime - previous.usage.qosUtilityTime) / int64(time.Millisecond),
LegacyMS: (current.usage.qosLegacyTime - previous.usage.qosLegacyTime) / int64(time.Millisecond),
UserInitiatedMS: (current.usage.qosUserInitiatedTime - previous.usage.qosUserInitiatedTime) / int64(time.Millisecond),
UserInteractiveMS: (current.usage.qosUserInteractiveTime - previous.usage.qosUserInteractiveTime) / int64(time.Millisecond),
}
if qos != (qosBreakdown{}) {
row.QoSMS = &qos
}
}
if current.absoluteTime != 0 && previous.absoluteTime != 0 && current.absoluteTime >= previous.absoluteTime {
sleptNano := (current.continuousTime - previous.continuousTime) - (current.absoluteTime - previous.absoluteTime)
wallNano := now.UnixNano() - previous.at.UnixNano()
if sleptNano > wallNano {
sleptNano = wallNano
}
if sleptNano > 0 {
row.SleptMS = sleptNano / int64(time.Millisecond)
}
}
if len(current.interfaces) > 0 && len(previous.interfaces) > 0 {
interfacePackets := make(map[string]uint64)
interfaceBytes := make(map[string]uint64)
for name, counters := range current.interfaces {
previousCounters, found := previous.interfaces[name]
if !found {
continue
}
packets := counters.inPackets - previousCounters.inPackets + counters.outPackets - previousCounters.outPackets
if packets > 0 {
interfacePackets[name] = packets
}
bytesDelta := counters.inBytes - previousCounters.inBytes + counters.outBytes - previousCounters.outBytes
if bytesDelta > 0 {
interfaceBytes[name] = bytesDelta
}
}
if len(interfacePackets) > 0 {
row.InterfacePackets = interfacePackets
}
if len(interfaceBytes) > 0 {
row.InterfaceBytes = interfaceBytes
}
}
r.updateDeviceStateLocked(now)
r.rows = append(r.rows, row)
return r.requestProfileLocked(now, profileRate(row.EnergyNanojoules, row.CPUUserMS+row.CPUSystemMS, now.Sub(previous.at)))
}
func (r *Recorder) collectDNSDomains() map[string]uint64 {
r.dnsAccess.Lock()
defer r.dnsAccess.Unlock()
if len(r.dnsDomains) == 0 {
return nil
}
type domainCount struct {
domain string
count uint64
}
counts := make([]domainCount, 0, len(r.dnsDomains))
for domain, count := range r.dnsDomains {
counts = append(counts, domainCount{domain, count})
}
clear(r.dnsDomains)
slices.SortFunc(counts, func(a domainCount, b domainCount) int {
return cmp.Compare(b.count, a.count)
})
if len(counts) > maxRowDNSDomains {
counts = counts[:maxRowDNSDomains]
}
result := make(map[string]uint64, len(counts))
for _, entry := range counts {
result[entry.domain] = entry.count
}
return result
}
func trafficDeltas(current map[trafficKey]trafficBytes, previous map[trafficKey]trafficBytes) trafficBreakdown {
var result trafficBreakdown
for key, currentBytes := range current {
previousBytes := previous[key]
delta := trafficDelta{
In: currentBytes.inBytes - previousBytes.inBytes,
Out: currentBytes.outBytes - previousBytes.outBytes,
Dials: currentBytes.dials - previousBytes.dials,
}
if delta == (trafficDelta{}) {
continue
}
if result == nil {
result = make(trafficBreakdown)
}
byTag := result[key.kind]
if byTag == nil {
byTag = make(map[string]*trafficDelta)
result[key.kind] = byTag
}
byTag[key.tag] = &delta
}
return result
}
func (r *Recorder) updateDeviceStateLocked(now time.Time) {
state, loaded := readDeviceState()
if !loaded {
return
}
if r.deviceStateValid && r.lastDeviceState == state {
return
}
r.lastDeviceState = state
r.deviceStateValid = true
event := r.newEvent(eventTypeDevice, now)
event.Device = &state
r.events = append(r.events, event)
}
func profileRate(energyNanojoules uint64, cpuMS int64, duration time.Duration) float64 {
if duration <= 0 {
return 0
}
work := float64(energyNanojoules)
if work == 0 {
work = float64(cpuMS)
}
return work / duration.Seconds()
}
func (r *Recorder) requestProfileLocked(now time.Time, rate float64) *profileRequest {
if r.closed || rate == 0 || r.profileFile != nil {
return nil
}
lowestIndex := -1
if len(r.profiles) >= maxProfiles {
lowestIndex = 0
for index := 1; index < len(r.profiles); index++ {
if r.profiles[index].rate < r.profiles[lowestIndex].rate {
lowestIndex = index
}
}
if rate <= r.profiles[lowestIndex].rate {
return nil
}
}
if !r.lastProfileAt.IsZero() && now.Sub(r.lastProfileAt) < profileMinInterval {
return nil
}
request := &profileRequest{
writePath: filepath.Join(r.draftPath, "cpu-"+now.UTC().Format("150405")+".pb"),
}
if lowestIndex >= 0 {
request.removePath = r.profiles[lowestIndex].path
r.profiles = slices.Delete(r.profiles, lowestIndex, lowestIndex+1)
}
r.profiles = append(r.profiles, profileEntry{rate: rate, path: request.writePath})
r.lastProfileAt = now
return request
}
func (r *Recorder) startProfile(request *profileRequest) {
if request.removePath != "" {
os.Remove(request.removePath)
}
file, err := os.OpenFile(request.writePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
if err != nil {
return
}
r.chown(request.writePath)
r.access.Lock()
defer r.access.Unlock()
if r.closed || r.profileFile != nil {
file.Close()
os.Remove(request.writePath)
return
}
err = pprof.StartCPUProfile(file)
if err != nil {
file.Close()
os.Remove(request.writePath)
return
}
r.profileFile = file
r.profileTimer = time.AfterFunc(profileMaxDuration, r.stopProfile)
}
func (r *Recorder) stopProfile() {
r.access.Lock()
defer r.access.Unlock()
r.stopProfileLocked()
}
func (r *Recorder) stopProfileLocked() {
if r.profileFile == nil {
return
}
r.profileTimer.Stop()
pprof.StopCPUProfile()
r.profileFile.Close()
r.profileFile = nil
}
func (r *Recorder) chown(path string) {
if r.ownerCallback != nil {
r.ownerCallback(path)
}
}
func (r *Recorder) flushLocked(now time.Time) {
err := appendRecords(r, filepath.Join(r.draftPath, timelineFileName), r.rows)
if err == nil {
r.rows = r.rows[:0]
} else {
r.logger.Error(E.Cause(err, "power report: write timeline"))
if len(r.rows) >= rowCapacity {
r.rows = r.rows[len(r.rows)-rowCapacity/2:]
}
}
err = appendRecords(r, filepath.Join(r.draftPath, eventsFileName), r.events)
if err == nil {
r.events = r.events[:0]
} else {
r.logger.Error(E.Cause(err, "power report: write events"))
if len(r.events) >= eventCapacity {
r.events = r.events[len(r.events)-eventCapacity/2:]
}
}
r.lastFlushAt = now
}
func appendRecords[T any](r *Recorder, path string, records []T) error {
if len(records) == 0 {
return nil
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
return err
}
defer file.Close()
r.chown(path)
encoder := json.NewEncoder(file)
for _, record := range records {
err = encoder.Encode(record)
if err != nil {
return err
}
}
return nil
}
func (r *Recorder) writeProfiles() {
if r.profileCallback != nil {
r.profileCallback(r.draftPath)
return
}
profilePath := filepath.Join(r.draftPath, goroutineProfileFileName)
file, err := os.OpenFile(profilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
if err != nil {
return
}
defer file.Close()
r.chown(profilePath)
pprof.Lookup("goroutine").WriteTo(file, 0)
}