Improve power report attribution and sampling

This commit is contained in:
世界
2026-10-02 18:00:09 +08:00
parent fd9a076d25
commit eb7deeae8a
16 changed files with 588 additions and 43 deletions
+10
View File
@@ -189,6 +189,16 @@ func DNSTransportTagFromContext(ctx context.Context) (string, bool) {
return transportTag, loaded
}
func ContextForMultiplexSession(ctx context.Context) context.Context {
var sessionContext InboundContext
metadata := ContextFrom(ctx)
if metadata != nil {
sessionContext.Outbound = metadata.Outbound
}
ctx = ContextWithDNSTransportTag(ctx, "")
return WithContext(ctx, &sessionContext)
}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext)
}
+12
View File
@@ -431,9 +431,15 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr,
if recorder != nil {
recorder.CountConnectionOpened()
attribution := d.dialAttribution(ctx, destination)
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
conn = bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
outboundCounter.CountIn(n)
dnsCounter.CountIn(n)
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
outboundCounter.CountOut(n)
dnsCounter.CountOut(n)
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}})
}
@@ -453,9 +459,15 @@ func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socks
if recorder != nil {
recorder.CountConnectionOpened()
attribution := d.dialAttribution(ctx, destination)
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
conn = bufio.NewNetPacketConn(bufio.NewCounterPacketConn(bufio.NewPacketConn(conn), []N.CountFunc{func(n int64) {
outboundCounter.CountIn(n)
dnsCounter.CountIn(n)
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
outboundCounter.CountOut(n)
dnsCounter.CountOut(n)
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}}))
}
+2 -2
View File
@@ -51,9 +51,9 @@ type clientDialer struct {
}
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
return d.Dialer.DialContext(adapter.ContextForMultiplexSession(ctx), network, destination)
}
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
return d.Dialer.ListenPacket(adapter.ContextForMultiplexSession(ctx), destination)
}
+21
View File
@@ -14,9 +14,11 @@ import (
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
@@ -30,6 +32,7 @@ type Instance struct {
trafficManager *trafficcontrol.Manager
cacheFile adapter.CacheFile
pauseManager pause.Manager
pauseCallback *list.Element[pause.Callback]
urlTestHistoryStorage *urltest.HistoryStorage
outboundManager adapter.OutboundManager
endpointManager adapter.EndpointManager
@@ -135,6 +138,7 @@ func (s *StartedService) newInstance(ctx context.Context, profileContent string,
i.clashMode = service.PtrFromContext[clashmode.Manager](ctx)
i.trafficManager = service.PtrFromContext[trafficcontrol.Manager](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx)
i.registerPauseCallback(ctx)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
i.outboundManager = service.FromContext[adapter.OutboundManager](ctx)
i.endpointManager = service.FromContext[adapter.EndpointManager](ctx)
@@ -164,10 +168,27 @@ func (i *Instance) Start() error {
func (i *Instance) Close() error {
i.cancel()
if i.pauseCallback != nil {
i.pauseManager.UnregisterCallback(i.pauseCallback)
i.pauseCallback = nil
}
i.urlTestHistoryStorage.Close()
return i.instance.Close()
}
func (i *Instance) registerPauseCallback(ctx context.Context) {
powerManager := service.FromContext[*powerreport.Manager](ctx)
if i.pauseManager == nil || powerManager == nil {
return
}
i.pauseCallback = i.pauseManager.RegisterCallback(func(event int) {
recorder := powerManager.Recorder()
if recorder != nil {
recorder.RecordPauseEvent(event)
}
})
}
func (i *Instance) Box() *box.Box {
return i.instance
}
+5 -1
View File
@@ -1059,7 +1059,11 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx
if r.powerManager != nil {
recorder := r.powerManager.Recorder()
if recorder != nil {
recorder.CountDNSQuery()
var domain string
if len(message.Question) == 1 {
domain = message.Question[0].Name
}
recorder.CountDNSQuery(domain)
}
}
if len(message.Question) != 1 {
+1 -1
View File
@@ -131,7 +131,7 @@ func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
}
func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) {
return h.dialer.DialTLSContext(ctx, h.server)
return h.dialer.DialTLSContext(adapter.ContextForMultiplexSession(ctx), h.server)
}
func (h *Outbound) MultiplexEnabled() bool {
+41
View File
@@ -0,0 +1,41 @@
package powerreport
/*
#cgo LDFLAGS: -framework Foundation
#cgo ios LDFLAGS: -framework UIKit
#cgo !ios LDFLAGS: -framework CoreFoundation -framework IOKit
#include "device_darwin.h"
*/
import "C"
func readDeviceState() (deviceState, bool) {
var batteryLevel C.int
state := deviceState{
LowPowerMode: C.boxPowerLowPowerMode() != 0,
ThermalState: thermalStateName(int(C.boxPowerThermalState())),
}
switch C.boxPowerSource(&batteryLevel) {
case 1:
state.PowerSource = "battery"
case 2:
state.PowerSource = "ac"
}
state.BatteryLevel = int(batteryLevel)
return state, true
}
func thermalStateName(state int) string {
switch state {
case 0:
return "nominal"
case 1:
return "fair"
case 2:
return "serious"
case 3:
return "critical"
default:
return ""
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
int boxPowerLowPowerMode(void);
int boxPowerThermalState(void);
int boxPowerSource(int *level);
+121
View File
@@ -0,0 +1,121 @@
#import <Foundation/Foundation.h>
#import <TargetConditionals.h>
#include "device_darwin.h"
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#include <math.h>
#elif !TARGET_OS_IPHONE
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
#endif
int boxPowerLowPowerMode(void) {
@autoreleasepool {
return [[NSProcessInfo processInfo] isLowPowerModeEnabled] ? 1 : 0;
}
}
int boxPowerThermalState(void) {
@autoreleasepool {
return (int) [[NSProcessInfo processInfo] thermalState];
}
}
#if TARGET_OS_IOS
__attribute__((constructor)) static void boxPowerInitBatteryMonitoring(void) {
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
}
});
}
int boxPowerSource(int *level) {
@autoreleasepool {
*level = 0;
__block int source = 0;
__block float batteryLevel = -1;
dispatch_block_t read = ^{
UIDevice *device = [UIDevice currentDevice];
switch (device.batteryState) {
case UIDeviceBatteryStateUnplugged:
source = 1;
break;
case UIDeviceBatteryStateCharging:
case UIDeviceBatteryStateFull:
source = 2;
break;
default:
break;
}
batteryLevel = device.batteryLevel;
};
if ([NSThread isMainThread]) {
read();
} else {
dispatch_sync(dispatch_get_main_queue(), read);
}
if (batteryLevel >= 0) {
*level = (int) lroundf(batteryLevel * 100);
}
return source;
}
}
#elif TARGET_OS_IPHONE
int boxPowerSource(int *level) {
*level = 0;
return 0;
}
#else
int boxPowerSource(int *level) {
*level = 0;
CFTypeRef info = IOPSCopyPowerSourcesInfo();
if (info == NULL) {
return 0;
}
CFArrayRef sources = IOPSCopyPowerSourcesList(info);
if (sources == NULL) {
CFRelease(info);
return 0;
}
int result = 0;
if (CFArrayGetCount(sources) > 0) {
CFDictionaryRef source = IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(sources, 0));
if (source != NULL) {
CFStringRef state = CFDictionaryGetValue(source, CFSTR(kIOPSPowerSourceStateKey));
if (state != NULL) {
if (CFStringCompare(state, CFSTR(kIOPSACPowerValue), 0) == kCFCompareEqualTo) {
result = 2;
} else if (CFStringCompare(state, CFSTR(kIOPSBatteryPowerValue), 0) == kCFCompareEqualTo) {
result = 1;
}
}
int current = 0;
int maximum = 0;
CFNumberRef currentValue = CFDictionaryGetValue(source, CFSTR(kIOPSCurrentCapacityKey));
CFNumberRef maximumValue = CFDictionaryGetValue(source, CFSTR(kIOPSMaxCapacityKey));
if (currentValue != NULL) {
CFNumberGetValue(currentValue, kCFNumberIntType, &current);
}
if (maximumValue != NULL) {
CFNumberGetValue(maximumValue, kCFNumberIntType, &maximum);
}
if (maximum > 0) {
*level = current * 100 / maximum;
}
}
}
CFRelease(sources);
CFRelease(info);
return result;
}
#endif
+7
View File
@@ -0,0 +1,7 @@
//go:build !darwin || !cgo
package powerreport
func readDeviceState() (deviceState, bool) {
return deviceState{}, false
}
+40 -5
View File
@@ -64,10 +64,30 @@ type timelineRow struct {
DNSQueries uint64 `json:"dnsQueries,omitempty"`
ConnectionsOpened uint64 `json:"connectionsOpened,omitempty"`
InterfacePackets map[string]uint64 `json:"interfacePackets,omitempty"`
InterfaceBytes map[string]uint64 `json:"interfaceBytes,omitempty"`
Traffic trafficBreakdown `json:"traffic,omitempty"`
DNSDomains map[string]uint64 `json:"dnsDomains,omitempty"`
NetworkType string `json:"network,omitempty"`
NetworkPathUpdates uint64 `json:"pathUpdates,omitempty"`
}
type trafficBreakdown map[TrafficKind]map[string]*trafficDelta
type trafficDelta struct {
In uint64 `json:"in,omitempty"`
Out uint64 `json:"out,omitempty"`
}
type trafficKey struct {
kind TrafficKind
tag string
}
type trafficBytes struct {
inBytes uint64
outBytes uint64
}
type qosBreakdown struct {
DefaultMS int64 `json:"default,omitempty"`
MaintenanceMS int64 `json:"maintenance,omitempty"`
@@ -79,9 +99,14 @@ type qosBreakdown struct {
}
const (
eventTypeBreak = "break"
eventTypeNetwork = "network"
eventTypePath = "path"
eventTypeBreak = "break"
eventTypeNetwork = "network"
eventTypePath = "path"
eventTypeDevice = "device"
eventTypeDevicePause = "device-pause"
eventTypeDeviceWake = "device-wake"
eventTypeNetworkPause = "network-pause"
eventTypeNetworkWake = "network-wake"
)
type eventRecord struct {
@@ -93,6 +118,14 @@ type eventRecord struct {
NetworkType string `json:"network,omitempty"`
NetworkPath string `json:"path,omitempty"`
By *Attribution `json:"by,omitempty"`
Device *deviceState `json:"device,omitempty"`
}
type deviceState struct {
PowerSource string `json:"powerSource,omitempty"`
BatteryLevel int `json:"batteryLevel,omitempty"`
LowPowerMode bool `json:"lowPowerMode,omitempty"`
ThermalState string `json:"thermalState,omitempty"`
}
type systemUsage struct {
@@ -116,6 +149,8 @@ type systemUsage struct {
}
type interfaceCounters struct {
inPackets uint32
outPackets uint32
inPackets uint64
outPackets uint64
inBytes uint64
outBytes uint64
}
+210 -17
View File
@@ -1,11 +1,13 @@
package powerreport
import (
"cmp"
"encoding/json"
"os"
"path/filepath"
"runtime/metrics"
"runtime/pprof"
"slices"
"sync"
"sync/atomic"
"time"
@@ -13,6 +15,7 @@ import (
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 (
@@ -34,6 +37,11 @@ const (
rowCapacity = 4096
eventCapacity = 8192
maxDNSDomains = 512
maxRowDNSDomains = 8
maxProfiles = 3
profileMinInterval = 2 * time.Minute
)
type Options struct {
@@ -74,16 +82,26 @@ type Recorder struct {
connectionsOpened atomic.Uint64
networkPathUpdates atomic.Uint64
access sync.Mutex
networkType string
networkPath string
rows []timelineRow
events []eventRecord
previous previousSample
lastFlushAt time.Time
started bool
closed bool
metricsSamples []metrics.Sample
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
done chan struct{}
workerDone chan struct{}
@@ -108,6 +126,17 @@ type previousSample struct {
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 {
@@ -144,6 +173,8 @@ func NewRecorder(options Options) *Recorder {
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"},
@@ -181,6 +212,7 @@ func (r *Recorder) Start() error {
r.lastSampleAt.Store(int64(now.Sub(r.baseTime)))
r.lastFlushAt = now
r.started = true
r.updateDeviceStateLocked(now)
go r.worker()
return nil
}
@@ -248,8 +280,30 @@ func (r *Recorder) Touch(direction Direction, size int, by *Attribution) {
}
}
func (r *Recorder) CountDNSQuery() {
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() {
@@ -332,19 +386,24 @@ func (r *Recorder) worker() {
func (r *Recorder) process() {
now := time.Now()
r.access.Lock()
defer r.access.Unlock()
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)
r.sampleLocked(now)
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.writeSampleProfile(request)
}
}
func (r *Recorder) consumeBreakLocked() {
@@ -374,11 +433,12 @@ func (r *Recorder) resetPreviousLocked(now time.Time) {
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) {
func (r *Recorder) sampleLocked(now time.Time) *profileRequest {
previous := r.previous
r.resetPreviousLocked(now)
current := &r.previous
@@ -391,7 +451,9 @@ func (r *Recorder) sampleLocked(now time.Time) {
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,
}
@@ -433,21 +495,152 @@ func (r *Recorder) sampleLocked(now time.Time) {
}
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
}
delta := uint64(counters.inPackets-previousCounters.inPackets) + uint64(counters.outPackets-previousCounters.outPackets)
if delta > 0 {
interfacePackets[name] = delta
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, &row, 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,
}
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
r.events = append(r.events, eventRecord{
Type: eventTypeDevice,
At: now.UTC().Format(time.RFC3339),
Device: &state,
})
}
func (r *Recorder) requestProfileLocked(now time.Time, row *timelineRow, duration time.Duration) *profileRequest {
if r.closed || duration <= 0 {
return nil
}
work := float64(row.EnergyNanojoules)
if work == 0 {
work = float64(row.CPUUserMS + row.CPUSystemMS)
}
if work == 0 {
return nil
}
rate := work / duration.Seconds()
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, "goroutine-"+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) writeSampleProfile(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
}
defer file.Close()
r.chown(request.writePath)
pprof.Lookup("goroutine").WriteTo(file, 0)
}
func (r *Recorder) chown(path string) {
+26 -15
View File
@@ -1,7 +1,6 @@
package powerreport
/*
#include <ifaddrs.h>
#include <mach/mach_time.h>
#include <net/if.h>
#include <net/if_var.h>
@@ -16,9 +15,11 @@ int proc_pid_rusage(int pid, int flavor, rusage_info_t *buffer);
import "C"
import (
"encoding/binary"
"os"
"strings"
"sync"
"syscall"
"unsafe"
)
@@ -99,25 +100,35 @@ func readClocks() (absoluteTime int64, continuousTime int64) {
}
func readInterfaceCounters() map[string]interfaceCounters {
var list *C.struct_ifaddrs
if C.getifaddrs(&list) != 0 {
table, err := syscall.RouteRIB(syscall.NET_RT_IFLIST2, 0)
if err != nil {
return nil
}
defer C.freeifaddrs(list)
var message C.struct_if_msghdr2
messageSize := int(unsafe.Sizeof(message))
messageBytes := unsafe.Slice((*byte)(unsafe.Pointer(&message)), messageSize)
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
for len(table) >= 4 {
messageLength := int(binary.NativeEndian.Uint16(table))
if messageLength < 4 || messageLength > len(table) {
break
}
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),
if table[3] == syscall.RTM_IFINFO2 && messageLength >= messageSize {
copy(messageBytes, table[:messageSize])
var nameBuffer [C.IF_NAMESIZE]C.char
if C.if_indextoname(C.uint(message.ifm_index), &nameBuffer[0]) != nil {
name := C.GoString(&nameBuffer[0])
if strings.HasPrefix(name, "en") || strings.HasPrefix(name, "pdp_ip") {
result[name] = interfaceCounters{
inPackets: uint64(message.ifm_data.ifi_ipackets),
outPackets: uint64(message.ifm_data.ifi_opackets),
inBytes: uint64(message.ifm_data.ifi_ibytes),
outBytes: uint64(message.ifm_data.ifi_obytes),
}
}
}
}
table = table[messageLength:]
}
return result
}
+9 -2
View File
@@ -83,9 +83,16 @@ func readInterfaceCounters() map[string]interfaceCounters {
if inErr != nil || outErr != nil {
continue
}
inBytes, inBytesErr := strconv.ParseUint(fields[0], 10, 64)
outBytes, outBytesErr := strconv.ParseUint(fields[8], 10, 64)
if inBytesErr != nil || outBytesErr != nil {
continue
}
result[name] = interfaceCounters{
inPackets: uint32(inPackets),
outPackets: uint32(outPackets),
inPackets: inPackets,
outPackets: outPackets,
inBytes: inBytes,
outBytes: outBytes,
}
}
return result
+73
View File
@@ -0,0 +1,73 @@
package powerreport
import "sync/atomic"
type TrafficKind string
const (
TrafficOutbound TrafficKind = "outbound"
TrafficEndpoint TrafficKind = "endpoint"
TrafficDNSTransport TrafficKind = "dns"
)
const maxTrafficCounters = 512
type TrafficCounter struct {
inBytes atomic.Uint64
outBytes atomic.Uint64
}
func (c *TrafficCounter) CountIn(n int64) {
if c == nil || n <= 0 {
return
}
c.inBytes.Add(uint64(n))
}
func (c *TrafficCounter) CountOut(n int64) {
if c == nil || n <= 0 {
return
}
c.outBytes.Add(uint64(n))
}
func (r *Recorder) TrafficCounter(kind TrafficKind, tag string) *TrafficCounter {
if r == nil || tag == "" {
return nil
}
key := trafficKey{kind: kind, tag: tag}
r.trafficAccess.RLock()
counter := r.traffic[key]
r.trafficAccess.RUnlock()
if counter != nil {
return counter
}
r.trafficAccess.Lock()
defer r.trafficAccess.Unlock()
counter = r.traffic[key]
if counter != nil {
return counter
}
if len(r.traffic) >= maxTrafficCounters {
return nil
}
counter = new(TrafficCounter)
r.traffic[key] = counter
return counter
}
func (r *Recorder) snapshotTrafficLocked() map[trafficKey]trafficBytes {
r.trafficAccess.RLock()
defer r.trafficAccess.RUnlock()
if len(r.traffic) == 0 {
return nil
}
snapshot := make(map[trafficKey]trafficBytes, len(r.traffic))
for key, counter := range r.traffic {
snapshot[key] = trafficBytes{
inBytes: counter.inBytes.Load(),
outBytes: counter.outBytes.Load(),
}
}
return snapshot
}
+3
View File
@@ -158,9 +158,12 @@ func (e *Endpoint) Start(postStart bool) error {
recorder := powerManager.Recorder()
if recorder != nil {
attribution := &powerreport.Attribution{Endpoint: e.options.Tag}
counter := recorder.TrafficCounter(powerreport.TrafficEndpoint, e.options.Tag)
standardBind.SetIOActivityFuncs(func(size int) {
counter.CountIn(int64(size))
recorder.Touch(powerreport.DirectionInbound, size, attribution)
}, func(size int) {
counter.CountOut(int64(size))
recorder.Touch(powerreport.DirectionOutbound, size, attribution)
})
}