mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-20 00:04:24 +00:00
Improve power report attribution and sampling
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}}))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
int boxPowerLowPowerMode(void);
|
||||
|
||||
int boxPowerThermalState(void);
|
||||
|
||||
int boxPowerSource(int *level);
|
||||
@@ -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, ¤t);
|
||||
}
|
||||
if (maximumValue != NULL) {
|
||||
CFNumberGetValue(maximumValue, kCFNumberIntType, &maximum);
|
||||
}
|
||||
if (maximum > 0) {
|
||||
*level = current * 100 / maximum;
|
||||
}
|
||||
}
|
||||
}
|
||||
CFRelease(sources);
|
||||
CFRelease(info);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !darwin || !cgo
|
||||
|
||||
package powerreport
|
||||
|
||||
func readDeviceState() (deviceState, bool) {
|
||||
return deviceState{}, false
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user