mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// liveNetWindow is how far back the "last minute" byte totals reach.
|
|
const liveNetWindow = time.Minute
|
|
|
|
// liveNetMaxGap bounds how far apart two samples may be before a rate computed
|
|
// across them stops being "live". The overview SSE ticks every two seconds, so
|
|
// a gap beyond this means the tab was closed or the device was idle; treat it
|
|
// as a fresh baseline instead of averaging a long dead interval.
|
|
const liveNetMaxGap = 15 * time.Second
|
|
|
|
// netIfSample is one cumulative counter reading for an interface.
|
|
type netIfSample struct {
|
|
at time.Time
|
|
rxCum uint64
|
|
txCum uint64
|
|
}
|
|
|
|
// liveNetDevice holds the per-device sampling state used to derive rates and
|
|
// trailing-window totals from cumulative interface counters.
|
|
type liveNetDevice struct {
|
|
prev netIfSample
|
|
hasPrev bool
|
|
window []netIfSample
|
|
}
|
|
|
|
// liveNetResult is one rendered snapshot of a device's live network state.
|
|
type liveNetResult struct {
|
|
ipv4 string
|
|
rxRate float64 // bytes/sec over the trailing sample interval
|
|
txRate float64
|
|
minuteRx int64 // bytes over the trailing liveNetWindow
|
|
minuteTx int64
|
|
status string // "", "waiting_sample", or "stale"
|
|
}
|
|
|
|
// liveNetTracker derives live rates and last-minute totals from cumulative
|
|
// /sys interface counters. It is driven on demand by the overview builders, so
|
|
// no separate goroutine is required; the SSE overview cadence keeps it warm.
|
|
type liveNetTracker struct {
|
|
mu sync.Mutex
|
|
devices map[string]*liveNetDevice
|
|
}
|
|
|
|
func newLiveNetTracker() *liveNetTracker {
|
|
return &liveNetTracker{devices: map[string]*liveNetDevice{}}
|
|
}
|
|
|
|
// sample reads the interface's current counters and addresses and returns the
|
|
// device's live network state. Interface addresses resolve even on the first
|
|
// call; rates and totals need a second reading, reported as waiting_sample.
|
|
func (t *liveNetTracker) sample(deviceID, iface string, now time.Time) liveNetResult {
|
|
ipv4 := netIfAddrs(iface)
|
|
rxCum, txCum, err := netIfCounters(iface)
|
|
if err != nil {
|
|
// The interface briefly disappears while QMI reconnects. Drop the
|
|
// baseline so the next good read starts fresh rather than counting the
|
|
// reconnect as one giant delta.
|
|
t.mu.Lock()
|
|
delete(t.devices, deviceID)
|
|
t.mu.Unlock()
|
|
return liveNetResult{ipv4: ipv4, status: "stale"}
|
|
}
|
|
rxRate, txRate, minuteRx, minuteTx, status := t.record(deviceID, rxCum, txCum, now)
|
|
return liveNetResult{
|
|
ipv4: ipv4,
|
|
rxRate: rxRate, txRate: txRate,
|
|
minuteRx: minuteRx, minuteTx: minuteTx,
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
// record folds one cumulative counter reading into the device's sampling state
|
|
// and returns the derived rates and trailing-window totals. It is pure (no
|
|
// interface I/O) so the rate/window logic is unit-testable.
|
|
func (t *liveNetTracker) record(deviceID string, rxCum, txCum uint64, now time.Time) (rxRate, txRate float64, minuteRx, minuteTx int64, status string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
d := t.devices[deviceID]
|
|
if d == nil {
|
|
d = &liveNetDevice{}
|
|
t.devices[deviceID] = d
|
|
}
|
|
current := netIfSample{at: now, rxCum: rxCum, txCum: txCum}
|
|
|
|
// First sighting, a counter reset (interface reconnected), or a gap too
|
|
// long to average honestly: establish a baseline and wait for the next
|
|
// reading before reporting a rate.
|
|
if !d.hasPrev || rxCum < d.prev.rxCum || txCum < d.prev.txCum || now.Sub(d.prev.at) > liveNetMaxGap {
|
|
d.prev = current
|
|
d.hasPrev = true
|
|
d.window = []netIfSample{current}
|
|
return 0, 0, 0, 0, "waiting_sample"
|
|
}
|
|
|
|
if elapsed := now.Sub(d.prev.at).Seconds(); elapsed > 0 {
|
|
rxRate = float64(rxCum-d.prev.rxCum) / elapsed
|
|
txRate = float64(txCum-d.prev.txCum) / elapsed
|
|
}
|
|
d.prev = current
|
|
d.window = append(d.window, current)
|
|
|
|
// Drop samples outside the trailing window, then measure totals against
|
|
// the oldest surviving reading.
|
|
cutoff := now.Add(-liveNetWindow)
|
|
kept := d.window[:0]
|
|
for _, s := range d.window {
|
|
if !s.at.Before(cutoff) {
|
|
kept = append(kept, s)
|
|
}
|
|
}
|
|
d.window = kept
|
|
minuteRx = int64(rxCum - d.window[0].rxCum)
|
|
minuteTx = int64(txCum - d.window[0].txCum)
|
|
return rxRate, txRate, minuteRx, minuteTx, ""
|
|
}
|
|
|
|
// netIfAddrs returns the interface's first global IPv4 address. It uses only
|
|
// the net package, so it compiles on every platform; on hosts without the
|
|
// interface it returns an empty string.
|
|
func netIfAddrs(iface string) (ipv4 string) {
|
|
if iface == "" {
|
|
return ""
|
|
}
|
|
netIf, err := net.InterfaceByName(iface)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
addrs, err := netIf.Addrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, addr := range addrs {
|
|
var ip net.IP
|
|
switch a := addr.(type) {
|
|
case *net.IPNet:
|
|
ip = a.IP
|
|
case *net.IPAddr:
|
|
ip = a.IP
|
|
}
|
|
if ip == nil || ip.IsLoopback() {
|
|
continue
|
|
}
|
|
if v4 := ip.To4(); v4 != nil {
|
|
return v4.String()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// formatLiveBytes mirrors the SPA's formatBytes so the live strings match the
|
|
// chart's formatting: 1024-based units, rounded once the value reaches 100.
|
|
func formatLiveBytes(value float64) string {
|
|
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
|
|
value = 0
|
|
}
|
|
units := []string{"B", "KB", "MB", "GB", "TB"}
|
|
size := value
|
|
unit := 0
|
|
for size >= 1024 && unit < len(units)-1 {
|
|
size /= 1024
|
|
unit++
|
|
}
|
|
if size >= 100 {
|
|
return fmt.Sprintf("%.0f %s", size, units[unit])
|
|
}
|
|
return fmt.Sprintf("%.1f %s", size, units[unit])
|
|
}
|