mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Add power report service
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
access sync.Mutex
|
||||
recorder atomic.Pointer[Recorder]
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(options Options) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.recorder.Load() != nil {
|
||||
return nil
|
||||
}
|
||||
recorder := NewRecorder(options)
|
||||
err := recorder.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.recorder.Store(recorder)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
recorder := m.recorder.Swap(nil)
|
||||
if recorder == nil {
|
||||
return nil
|
||||
}
|
||||
return recorder.Close()
|
||||
}
|
||||
|
||||
func (m *Manager) Recorder() *Recorder {
|
||||
return m.recorder.Load()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func PromoteDraft(basePath string) {
|
||||
promoteDirectory(filepath.Join(basePath, DraftDirectoryName), filepath.Join(basePath, ReportsDirectoryName))
|
||||
}
|
||||
|
||||
func finalizeDraft(draftPath string) {
|
||||
promoteDirectory(draftPath, filepath.Join(filepath.Dir(draftPath), ReportsDirectoryName))
|
||||
}
|
||||
|
||||
func promoteDirectory(draftPath string, reportsPath string) {
|
||||
info, err := os.Stat(draftPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
entries, err := os.ReadDir(draftPath)
|
||||
if err != nil || len(entries) == 0 {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
err = os.MkdirAll(reportsPath, 0o777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
destName := info.ModTime().UTC().Format("2006-01-02T15-04-05")
|
||||
destPath := filepath.Join(reportsPath, destName)
|
||||
for i := 1; ; i++ {
|
||||
_, err = os.Stat(destPath)
|
||||
if os.IsNotExist(err) {
|
||||
break
|
||||
}
|
||||
if i > 1000 {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
destPath = filepath.Join(reportsPath, destName+"-"+strconv.Itoa(i))
|
||||
}
|
||||
err = os.Rename(draftPath, destPath)
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package powerreport
|
||||
|
||||
type Direction uint8
|
||||
|
||||
const (
|
||||
DirectionOutbound Direction = iota
|
||||
DirectionInbound
|
||||
)
|
||||
|
||||
func (d Direction) String() string {
|
||||
if d == DirectionInbound {
|
||||
return "in"
|
||||
}
|
||||
return "out"
|
||||
}
|
||||
|
||||
type Attribution struct {
|
||||
Inbound string `json:"inbound,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
Outbound string `json:"outbound,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
}
|
||||
|
||||
type timelineRow struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
CPUUserMS int64 `json:"cpuUserMS,omitempty"`
|
||||
CPUSystemMS int64 `json:"cpuSystemMS,omitempty"`
|
||||
CPUPerformanceMS int64 `json:"cpuPerformanceMS,omitempty"`
|
||||
CPUGCMS int64 `json:"cpuGCMS,omitempty"`
|
||||
QoSMS *qosBreakdown `json:"qosMS,omitempty"`
|
||||
PackageIdleWakeups uint64 `json:"packageIdleWakeups,omitempty"`
|
||||
InterruptWakeups uint64 `json:"interruptWakeups,omitempty"`
|
||||
EnergyNanojoules uint64 `json:"energyNJ,omitempty"`
|
||||
PerformanceEnergyNanojoules uint64 `json:"performanceEnergyNJ,omitempty"`
|
||||
DiskBytesWritten uint64 `json:"diskWriteBytes,omitempty"`
|
||||
SleptMS int64 `json:"sleptMS,omitempty"`
|
||||
Goroutines uint64 `json:"goroutines,omitempty"`
|
||||
DNSQueries uint64 `json:"dnsQueries,omitempty"`
|
||||
ConnectionsOpened uint64 `json:"connectionsOpened,omitempty"`
|
||||
InterfacePackets map[string]uint64 `json:"interfacePackets,omitempty"`
|
||||
NetworkType string `json:"network,omitempty"`
|
||||
}
|
||||
|
||||
type qosBreakdown struct {
|
||||
DefaultMS int64 `json:"default,omitempty"`
|
||||
MaintenanceMS int64 `json:"maintenance,omitempty"`
|
||||
BackgroundMS int64 `json:"background,omitempty"`
|
||||
UtilityMS int64 `json:"utility,omitempty"`
|
||||
LegacyMS int64 `json:"legacy,omitempty"`
|
||||
UserInitiatedMS int64 `json:"userInitiated,omitempty"`
|
||||
UserInteractiveMS int64 `json:"userInteractive,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
eventTypeBreak = "break"
|
||||
eventTypeNetwork = "network"
|
||||
)
|
||||
|
||||
type eventRecord struct {
|
||||
Type string `json:"t"`
|
||||
At string `json:"at"`
|
||||
IdleMS int64 `json:"idleMS,omitempty"`
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
NetworkType string `json:"network,omitempty"`
|
||||
By *Attribution `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
type systemUsage struct {
|
||||
valid bool
|
||||
userTime int64
|
||||
systemTime int64
|
||||
performanceUserTime int64
|
||||
performanceSystemTime int64
|
||||
qosDefaultTime int64
|
||||
qosMaintenanceTime int64
|
||||
qosBackgroundTime int64
|
||||
qosUtilityTime int64
|
||||
qosLegacyTime int64
|
||||
qosUserInitiatedTime int64
|
||||
qosUserInteractiveTime int64
|
||||
packageIdleWakeups uint64
|
||||
interruptWakeups uint64
|
||||
diskBytesWritten uint64
|
||||
energyNanojoules uint64
|
||||
performanceEnergyNanojoules uint64
|
||||
}
|
||||
|
||||
type interfaceCounters struct {
|
||||
inPackets uint32
|
||||
outPackets uint32
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/metrics"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
BasePath string
|
||||
Logger logger.Logger
|
||||
Metadata any
|
||||
OwnerCallback func(path string)
|
||||
LogCallback func() []byte
|
||||
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
|
||||
gateNano int64
|
||||
sampleNano int64
|
||||
flushInterval time.Duration
|
||||
fallbackInterval time.Duration
|
||||
baseTime time.Time
|
||||
|
||||
_ [64]byte
|
||||
lastActivity atomic.Int64
|
||||
_ [64]byte
|
||||
|
||||
lastSampleAt atomic.Int64
|
||||
pendingBreak atomic.Pointer[breakRecord]
|
||||
notify chan struct{}
|
||||
|
||||
dnsQueries atomic.Uint64
|
||||
connectionsOpened atomic.Uint64
|
||||
|
||||
access sync.Mutex
|
||||
networkType string
|
||||
rows []timelineRow
|
||||
events []eventRecord
|
||||
previous previousSample
|
||||
lastFlushAt time.Time
|
||||
started bool
|
||||
closed bool
|
||||
metricsSamples []metrics.Sample
|
||||
|
||||
done chan struct{}
|
||||
workerDone chan struct{}
|
||||
}
|
||||
|
||||
type breakRecord struct {
|
||||
at time.Time
|
||||
idleMS int64
|
||||
direction Direction
|
||||
size int
|
||||
by *Attribution
|
||||
}
|
||||
|
||||
type previousSample struct {
|
||||
at time.Time
|
||||
usage systemUsage
|
||||
gcSeconds float64
|
||||
absoluteTime int64
|
||||
continuousTime int64
|
||||
interfaces map[string]interfaceCounters
|
||||
dnsQueries uint64
|
||||
connectionsOpened uint64
|
||||
}
|
||||
|
||||
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,
|
||||
gateNano: int64(gateInterval),
|
||||
sampleNano: int64(sampleInterval),
|
||||
flushInterval: flushInterval,
|
||||
fallbackInterval: fallbackInterval,
|
||||
baseTime: time.Now(),
|
||||
notify: make(chan struct{}, 1),
|
||||
metricsSamples: []metrics.Sample{
|
||||
{Name: "/cpu/classes/gc/total:cpu-seconds"},
|
||||
{Name: "/sched/goroutines:goroutines"},
|
||||
},
|
||||
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
|
||||
}
|
||||
PromoteDraft(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
|
||||
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.consumeBreakLocked()
|
||||
r.sampleLocked(now)
|
||||
r.flushLocked(now)
|
||||
r.access.Unlock()
|
||||
r.writeGoroutineProfile()
|
||||
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) {
|
||||
nowNano := int64(time.Since(r.baseTime))
|
||||
lastNano := r.lastActivity.Load()
|
||||
if nowNano-lastNano < activityRefreshNano {
|
||||
return
|
||||
}
|
||||
previousNano := r.lastActivity.Swap(nowNano)
|
||||
if previousNano != 0 && nowNano-previousNano >= r.gateNano {
|
||||
r.pendingBreak.Store(&breakRecord{
|
||||
at: time.Now(),
|
||||
idleMS: (nowNano - previousNano) / int64(time.Millisecond),
|
||||
direction: direction,
|
||||
size: size,
|
||||
by: by,
|
||||
})
|
||||
r.notifyWorker()
|
||||
} else if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
|
||||
r.notifyWorker()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) CountDNSQuery() {
|
||||
r.dnsQueries.Add(1)
|
||||
}
|
||||
|
||||
func (r *Recorder) CountConnectionOpened() {
|
||||
r.connectionsOpened.Add(1)
|
||||
}
|
||||
|
||||
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, eventRecord{
|
||||
Type: eventType,
|
||||
At: now.UTC().Format(time.RFC3339),
|
||||
})
|
||||
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
|
||||
r.events = append(r.events, eventRecord{
|
||||
Type: eventTypeNetwork,
|
||||
At: now.UTC().Format(time.RFC3339),
|
||||
NetworkType: networkType,
|
||||
})
|
||||
r.access.Unlock()
|
||||
r.notifyWorker()
|
||||
}
|
||||
|
||||
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()
|
||||
defer r.access.Unlock()
|
||||
if !r.started || r.closed {
|
||||
return
|
||||
}
|
||||
r.consumeBreakLocked()
|
||||
nowNano := int64(now.Sub(r.baseTime))
|
||||
if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
|
||||
r.lastSampleAt.Store(nowNano)
|
||||
r.sampleLocked(now)
|
||||
}
|
||||
if now.Sub(r.lastFlushAt) >= r.flushInterval || len(r.rows) >= rowCapacity || len(r.events) >= eventCapacity {
|
||||
r.flushLocked(now)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) consumeBreakLocked() {
|
||||
record := r.pendingBreak.Swap(nil)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
r.events = append(r.events, eventRecord{
|
||||
Type: eventTypeBreak,
|
||||
At: record.at.UTC().Format(time.RFC3339),
|
||||
IdleMS: record.idleMS,
|
||||
Direction: record.direction.String(),
|
||||
Size: record.size,
|
||||
NetworkType: r.networkType,
|
||||
By: record.by,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Recorder) resetPreviousLocked(now time.Time) {
|
||||
metrics.Read(r.metricsSamples)
|
||||
r.previous = previousSample{
|
||||
at: now,
|
||||
usage: readSystemUsage(),
|
||||
gcSeconds: r.metricsSamples[0].Value.Float64(),
|
||||
interfaces: readInterfaceCounters(),
|
||||
dnsQueries: r.dnsQueries.Load(),
|
||||
connectionsOpened: r.connectionsOpened.Load(),
|
||||
}
|
||||
r.previous.absoluteTime, r.previous.continuousTime = readClocks()
|
||||
}
|
||||
|
||||
func (r *Recorder) sampleLocked(now time.Time) {
|
||||
previous := r.previous
|
||||
r.resetPreviousLocked(now)
|
||||
current := &r.previous
|
||||
row := timelineRow{
|
||||
From: previous.at.UTC().Format(time.RFC3339),
|
||||
To: now.UTC().Format(time.RFC3339),
|
||||
CPUGCMS: int64((current.gcSeconds - previous.gcSeconds) * 1000),
|
||||
Goroutines: r.metricsSamples[1].Value.Uint64(),
|
||||
DNSQueries: current.dnsQueries - previous.dnsQueries,
|
||||
ConnectionsOpened: current.connectionsOpened - previous.connectionsOpened,
|
||||
NetworkType: r.networkType,
|
||||
}
|
||||
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.Sub(previous.at).Nanoseconds()
|
||||
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)
|
||||
for name, counters := range current.interfaces {
|
||||
previousCounters, found := previous.interfaces[name]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
delta := uint64(counters.inPackets-previousCounters.inPackets) + uint64(counters.outPackets-previousCounters.outPackets)
|
||||
if delta > 0 {
|
||||
interfacePackets[name] = delta
|
||||
}
|
||||
}
|
||||
if len(interfacePackets) > 0 {
|
||||
row.InterfacePackets = interfacePackets
|
||||
}
|
||||
}
|
||||
r.rows = append(r.rows, row)
|
||||
}
|
||||
|
||||
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) writeGoroutineProfile() {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package powerreport
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
|
||||
|
||||
type processIOCounters struct {
|
||||
readOperationCount uint64
|
||||
writeOperationCount uint64
|
||||
otherOperationCount uint64
|
||||
readTransferCount uint64
|
||||
writeTransferCount uint64
|
||||
otherTransferCount uint64
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprocessiocounters
|
||||
//sys getProcessIoCounters(process windows.Handle, ioCounters *processIOCounters) (err error) = kernel32.GetProcessIoCounters
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryunbiasedinterrupttime
|
||||
//sys queryUnbiasedInterruptTime(unbiasedTime *uint64) (err error) = kernel32.QueryUnbiasedInterruptTime
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryinterrupttime
|
||||
//sys queryInterruptTime(interruptTime *uint64) = api-ms-win-core-realtime-l1-1-1.QueryInterruptTime
|
||||
@@ -0,0 +1,123 @@
|
||||
package powerreport
|
||||
|
||||
/*
|
||||
#include <ifaddrs.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_var.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// The iOS SDK does not ship libproc.h; the symbol is exported by libSystem
|
||||
// on all darwin platforms.
|
||||
int proc_pid_rusage(int pid, int flavor, rusage_info_t *buffer);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
timebaseOnce sync.Once
|
||||
timebaseNumer int64
|
||||
timebaseDenom int64
|
||||
)
|
||||
|
||||
func machTimebase() (int64, int64) {
|
||||
timebaseOnce.Do(func() {
|
||||
var timebase C.struct_mach_timebase_info
|
||||
C.mach_timebase_info(&timebase)
|
||||
timebaseNumer = int64(timebase.numer)
|
||||
timebaseDenom = int64(timebase.denom)
|
||||
})
|
||||
return timebaseNumer, timebaseDenom
|
||||
}
|
||||
|
||||
func machToNano(value uint64) int64 {
|
||||
numer, denom := machTimebase()
|
||||
if denom == 0 {
|
||||
return int64(value)
|
||||
}
|
||||
return int64(value) * numer / denom
|
||||
}
|
||||
|
||||
// The time fields of rusage_info are in mach_absolute_time units on arm64,
|
||||
// not nanoseconds; the header does not document this.
|
||||
func readSystemUsage() systemUsage {
|
||||
var info C.struct_rusage_info_v6
|
||||
result := C.proc_pid_rusage(C.int(os.Getpid()), C.RUSAGE_INFO_V6, (*C.rusage_info_t)(unsafe.Pointer(&info)))
|
||||
if result == 0 {
|
||||
return systemUsage{
|
||||
valid: true,
|
||||
userTime: machToNano(uint64(info.ri_user_time)),
|
||||
systemTime: machToNano(uint64(info.ri_system_time)),
|
||||
performanceUserTime: machToNano(uint64(info.ri_user_ptime)),
|
||||
performanceSystemTime: machToNano(uint64(info.ri_system_ptime)),
|
||||
qosDefaultTime: machToNano(uint64(info.ri_cpu_time_qos_default)),
|
||||
qosMaintenanceTime: machToNano(uint64(info.ri_cpu_time_qos_maintenance)),
|
||||
qosBackgroundTime: machToNano(uint64(info.ri_cpu_time_qos_background)),
|
||||
qosUtilityTime: machToNano(uint64(info.ri_cpu_time_qos_utility)),
|
||||
qosLegacyTime: machToNano(uint64(info.ri_cpu_time_qos_legacy)),
|
||||
qosUserInitiatedTime: machToNano(uint64(info.ri_cpu_time_qos_user_initiated)),
|
||||
qosUserInteractiveTime: machToNano(uint64(info.ri_cpu_time_qos_user_interactive)),
|
||||
packageIdleWakeups: uint64(info.ri_pkg_idle_wkups),
|
||||
interruptWakeups: uint64(info.ri_interrupt_wkups),
|
||||
diskBytesWritten: uint64(info.ri_diskio_byteswritten),
|
||||
energyNanojoules: uint64(info.ri_energy_nj),
|
||||
performanceEnergyNanojoules: uint64(info.ri_penergy_nj),
|
||||
}
|
||||
}
|
||||
var infoV4 C.struct_rusage_info_v4
|
||||
result = C.proc_pid_rusage(C.int(os.Getpid()), C.RUSAGE_INFO_V4, (*C.rusage_info_t)(unsafe.Pointer(&infoV4)))
|
||||
if result != 0 {
|
||||
return systemUsage{}
|
||||
}
|
||||
return systemUsage{
|
||||
valid: true,
|
||||
userTime: machToNano(uint64(infoV4.ri_user_time)),
|
||||
systemTime: machToNano(uint64(infoV4.ri_system_time)),
|
||||
qosDefaultTime: machToNano(uint64(infoV4.ri_cpu_time_qos_default)),
|
||||
qosMaintenanceTime: machToNano(uint64(infoV4.ri_cpu_time_qos_maintenance)),
|
||||
qosBackgroundTime: machToNano(uint64(infoV4.ri_cpu_time_qos_background)),
|
||||
qosUtilityTime: machToNano(uint64(infoV4.ri_cpu_time_qos_utility)),
|
||||
qosLegacyTime: machToNano(uint64(infoV4.ri_cpu_time_qos_legacy)),
|
||||
qosUserInitiatedTime: machToNano(uint64(infoV4.ri_cpu_time_qos_user_initiated)),
|
||||
qosUserInteractiveTime: machToNano(uint64(infoV4.ri_cpu_time_qos_user_interactive)),
|
||||
packageIdleWakeups: uint64(infoV4.ri_pkg_idle_wkups),
|
||||
interruptWakeups: uint64(infoV4.ri_interrupt_wkups),
|
||||
diskBytesWritten: uint64(infoV4.ri_diskio_byteswritten),
|
||||
}
|
||||
}
|
||||
|
||||
func readClocks() (absoluteTime int64, continuousTime int64) {
|
||||
return machToNano(uint64(C.mach_absolute_time())), machToNano(uint64(C.mach_continuous_time()))
|
||||
}
|
||||
|
||||
func readInterfaceCounters() map[string]interfaceCounters {
|
||||
var list *C.struct_ifaddrs
|
||||
if C.getifaddrs(&list) != 0 {
|
||||
return nil
|
||||
}
|
||||
defer C.freeifaddrs(list)
|
||||
result := make(map[string]interfaceCounters)
|
||||
for entry := list; entry != nil; entry = entry.ifa_next {
|
||||
if entry.ifa_addr == nil || entry.ifa_addr.sa_family != C.AF_LINK || entry.ifa_data == nil {
|
||||
continue
|
||||
}
|
||||
name := C.GoString(entry.ifa_name)
|
||||
if !strings.HasPrefix(name, "en") && !strings.HasPrefix(name, "pdp_ip") {
|
||||
continue
|
||||
}
|
||||
data := (*C.struct_if_data)(entry.ifa_data)
|
||||
result[name] = interfaceCounters{
|
||||
inPackets: uint32(data.ifi_ipackets),
|
||||
outPackets: uint32(data.ifi_opackets),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build darwin && !cgo
|
||||
|
||||
package powerreport
|
||||
|
||||
func readSystemUsage() systemUsage {
|
||||
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
|
||||
}
|
||||
|
||||
func readClocks() (absoluteTime int64, continuousTime int64) {
|
||||
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
|
||||
}
|
||||
|
||||
func readInterfaceCounters() map[string]interfaceCounters {
|
||||
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func readSystemUsage() systemUsage {
|
||||
var rusage unix.Rusage
|
||||
err := unix.Getrusage(unix.RUSAGE_SELF, &rusage)
|
||||
if err != nil {
|
||||
return systemUsage{}
|
||||
}
|
||||
return systemUsage{
|
||||
valid: true,
|
||||
userTime: rusage.Utime.Nano(),
|
||||
systemTime: rusage.Stime.Nano(),
|
||||
diskBytesWritten: readWriteBytes(),
|
||||
}
|
||||
}
|
||||
|
||||
func readWriteBytes() uint64 {
|
||||
content, err := os.ReadFile("/proc/self/io")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for line := range strings.SplitSeq(string(content), "\n") {
|
||||
value, found := strings.CutPrefix(line, "write_bytes: ")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
parsed, parseErr := strconv.ParseUint(value, 10, 64)
|
||||
if parseErr != nil {
|
||||
return 0
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func readClocks() (absoluteTime int64, continuousTime int64) {
|
||||
var monotonicTime unix.Timespec
|
||||
err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &monotonicTime)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
var bootTime unix.Timespec
|
||||
err = unix.ClockGettime(unix.CLOCK_BOOTTIME, &bootTime)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return monotonicTime.Nano(), bootTime.Nano()
|
||||
}
|
||||
|
||||
func readInterfaceCounters() map[string]interfaceCounters {
|
||||
content, err := os.ReadFile("/proc/net/dev")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
if len(lines) <= 2 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]interfaceCounters)
|
||||
for _, line := range lines[2:] {
|
||||
name, counters, found := strings.Cut(line, ":")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "lo" || strings.HasPrefix(name, "tun") || strings.HasPrefix(name, "utun") || strings.HasPrefix(name, "dummy") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(counters)
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
inPackets, inErr := strconv.ParseUint(fields[1], 10, 64)
|
||||
outPackets, outErr := strconv.ParseUint(fields[9], 10, 64)
|
||||
if inErr != nil || outErr != nil {
|
||||
continue
|
||||
}
|
||||
result[name] = interfaceCounters{
|
||||
inPackets: uint32(inPackets),
|
||||
outPackets: uint32(outPackets),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !darwin && !linux && !windows
|
||||
|
||||
package powerreport
|
||||
|
||||
func readSystemUsage() systemUsage {
|
||||
return systemUsage{}
|
||||
}
|
||||
|
||||
func readClocks() (absoluteTime int64, continuousTime int64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func readInterfaceCounters() map[string]interfaceCounters {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func filetimeDuration(value windows.Filetime) int64 {
|
||||
return (int64(value.HighDateTime)<<32 | int64(value.LowDateTime)) * 100
|
||||
}
|
||||
|
||||
func readSystemUsage() systemUsage {
|
||||
var creationTime, exitTime, kernelTime, userTime windows.Filetime
|
||||
err := windows.GetProcessTimes(windows.CurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime)
|
||||
if err != nil {
|
||||
return systemUsage{}
|
||||
}
|
||||
usage := systemUsage{
|
||||
valid: true,
|
||||
userTime: filetimeDuration(userTime),
|
||||
systemTime: filetimeDuration(kernelTime),
|
||||
}
|
||||
var ioCounters processIOCounters
|
||||
err = getProcessIoCounters(windows.CurrentProcess(), &ioCounters)
|
||||
if err == nil {
|
||||
usage.diskBytesWritten = ioCounters.writeTransferCount
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
func readClocks() (absoluteTime int64, continuousTime int64) {
|
||||
if procQueryInterruptTime.Find() != nil {
|
||||
return 0, 0
|
||||
}
|
||||
var unbiasedTime uint64
|
||||
err := queryUnbiasedInterruptTime(&unbiasedTime)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
var interruptTime uint64
|
||||
queryInterruptTime(&interruptTime)
|
||||
return int64(unbiasedTime) * 100, int64(interruptTime) * 100
|
||||
}
|
||||
|
||||
func readInterfaceCounters() map[string]interfaceCounters {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package powerreport
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modapi_ms_win_core_realtime_l1_1_1 = windows.NewLazySystemDLL("api-ms-win-core-realtime-l1-1-1.dll")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procQueryInterruptTime = modapi_ms_win_core_realtime_l1_1_1.NewProc("QueryInterruptTime")
|
||||
procGetProcessIoCounters = modkernel32.NewProc("GetProcessIoCounters")
|
||||
procQueryUnbiasedInterruptTime = modkernel32.NewProc("QueryUnbiasedInterruptTime")
|
||||
)
|
||||
|
||||
func queryInterruptTime(interruptTime *uint64) {
|
||||
syscall.SyscallN(procQueryInterruptTime.Addr(), uintptr(unsafe.Pointer(interruptTime)))
|
||||
return
|
||||
}
|
||||
|
||||
func getProcessIoCounters(process windows.Handle, ioCounters *processIOCounters) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procGetProcessIoCounters.Addr(), uintptr(process), uintptr(unsafe.Pointer(ioCounters)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func queryUnbiasedInterruptTime(unbiasedTime *uint64) (err error) {
|
||||
r1, _, e1 := syscall.SyscallN(procQueryUnbiasedInterruptTime.Addr(), uintptr(unsafe.Pointer(unbiasedTime)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user