Add power report service

This commit is contained in:
世界
2026-08-30 17:41:46 +08:00
parent 6d431a2c43
commit 82098a8760
32 changed files with 1772 additions and 86 deletions
+44
View File
@@ -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()
}
+48
View File
@@ -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)
}
}
+94
View File
@@ -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
}
+472
View File
@@ -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)
}
+21
View File
@@ -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
+123
View File
@@ -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")
}
+92
View File
@@ -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
}
+15
View File
@@ -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
}
+46
View File
@@ -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
}
+68
View File
@@ -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
}