sockstats: remove per-interface stats from Get

They're not needed for the sockstats logger, and they're somewhat
expensive to return (since they involve the creation of a map per
label). We now have a separate GetInterfaces() method that returns
them instead (which we can still use in the PeerAPI debug endpoint).

If changing sockstatlog to sample at 10,000 Hz (instead of the default
of 10Hz), the CPU usage would go up to 59% on a iPhone XS. Removing the
per-interface stats drops it to 20% (a no-op implementation of Get that
returns a fixed value is 16%).

Updates tailscale/corp#9230
Updates #3363

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
pull/7570/head
Mihai Parparita 1 year ago committed by Mihai Parparita
parent 9ebab961c9
commit 97b6d3e917

@ -865,7 +865,7 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintln(w, "<!DOCTYPE html><h1>Socket Stats</h1>") fmt.Fprintln(w, "<!DOCTYPE html><h1>Socket Stats</h1>")
stats, validation := sockstats.GetWithValidation() stats, interfaceStats, validation := sockstats.Get(), sockstats.GetInterfaces(), sockstats.GetValidation()
if stats == nil { if stats == nil {
fmt.Fprintln(w, "No socket stats available") fmt.Fprintln(w, "No socket stats available")
return return
@ -876,7 +876,7 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "<th>Label</th>") fmt.Fprintln(w, "<th>Label</th>")
fmt.Fprintln(w, "<th>Tx</th>") fmt.Fprintln(w, "<th>Tx</th>")
fmt.Fprintln(w, "<th>Rx</th>") fmt.Fprintln(w, "<th>Rx</th>")
for _, iface := range stats.Interfaces { for _, iface := range interfaceStats.Interfaces {
fmt.Fprintf(w, "<th>Tx (%s)</th>", html.EscapeString(iface)) fmt.Fprintf(w, "<th>Tx (%s)</th>", html.EscapeString(iface))
fmt.Fprintf(w, "<th>Rx (%s)</th>", html.EscapeString(iface)) fmt.Fprintf(w, "<th>Rx (%s)</th>", html.EscapeString(iface))
} }
@ -907,11 +907,13 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
txTotal += stat.TxBytes txTotal += stat.TxBytes
rxTotal += stat.RxBytes rxTotal += stat.RxBytes
for _, iface := range stats.Interfaces { if interfaceStat, ok := interfaceStats.Stats[label]; ok {
fmt.Fprintf(w, "<td align=right>%d</td>", stat.TxBytesByInterface[iface]) for _, iface := range interfaceStats.Interfaces {
fmt.Fprintf(w, "<td align=right>%d</td>", stat.RxBytesByInterface[iface]) fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.TxBytesByInterface[iface])
txTotalByInterface[iface] += stat.TxBytesByInterface[iface] fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.RxBytesByInterface[iface])
rxTotalByInterface[iface] += stat.RxBytesByInterface[iface] txTotalByInterface[iface] += interfaceStat.TxBytesByInterface[iface]
rxTotalByInterface[iface] += interfaceStat.RxBytesByInterface[iface]
}
} }
if validationStat, ok := validation.Stats[label]; ok && (validationStat.RxBytes > 0 || validationStat.TxBytes > 0) { if validationStat, ok := validation.Stats[label]; ok && (validationStat.RxBytes > 0 || validationStat.TxBytes > 0) {
@ -932,7 +934,7 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "<th>Total</th>") fmt.Fprintln(w, "<th>Total</th>")
fmt.Fprintf(w, "<th>%d</th>", txTotal) fmt.Fprintf(w, "<th>%d</th>", txTotal)
fmt.Fprintf(w, "<th>%d</th>", rxTotal) fmt.Fprintf(w, "<th>%d</th>", rxTotal)
for _, iface := range stats.Interfaces { for _, iface := range interfaceStats.Interfaces {
fmt.Fprintf(w, "<th>%d</th>", txTotalByInterface[iface]) fmt.Fprintf(w, "<th>%d</th>", txTotalByInterface[iface])
fmt.Fprintf(w, "<th>%d</th>", rxTotalByInterface[iface]) fmt.Fprintf(w, "<th>%d</th>", rxTotalByInterface[iface])
} }

@ -34,9 +34,6 @@ func TestDelta(t *testing.T) {
Stats: map[sockstats.Label]sockstats.SockStat{ Stats: map[sockstats.Label]sockstats.SockStat{
sockstats.LabelDERPHTTPClient: { sockstats.LabelDERPHTTPClient: {
TxBytes: 10, TxBytes: 10,
TxBytesByInterface: map[string]uint64{
"en0": 10,
},
}, },
}, },
}, },
@ -44,9 +41,6 @@ func TestDelta(t *testing.T) {
Stats: map[sockstats.Label]sockstats.SockStat{ Stats: map[sockstats.Label]sockstats.SockStat{
sockstats.LabelDERPHTTPClient: { sockstats.LabelDERPHTTPClient: {
TxBytes: 10, TxBytes: 10,
TxBytesByInterface: map[string]uint64{
"en0": 10,
},
}, },
}, },
}, },
@ -59,13 +53,9 @@ func TestDelta(t *testing.T) {
Stats: map[sockstats.Label]sockstats.SockStat{ Stats: map[sockstats.Label]sockstats.SockStat{
sockstats.LabelDERPHTTPClient: { sockstats.LabelDERPHTTPClient: {
TxBytes: 10, TxBytes: 10,
TxBytesByInterface: map[string]uint64{
"en0": 10,
}, },
}, },
}, },
Interfaces: []string{"en0"},
},
wantStats: map[sockstats.Label]deltaStat{ wantStats: map[sockstats.Label]deltaStat{
sockstats.LabelDERPHTTPClient: {10, 0}, sockstats.LabelDERPHTTPClient: {10, 0},
}, },
@ -77,30 +67,16 @@ func TestDelta(t *testing.T) {
sockstats.LabelDERPHTTPClient: { sockstats.LabelDERPHTTPClient: {
TxBytes: 10, TxBytes: 10,
RxBytes: 10, RxBytes: 10,
TxBytesByInterface: map[string]uint64{
"en0": 10,
},
RxBytesByInterface: map[string]uint64{
"en0": 10,
}, },
}, },
}, },
Interfaces: []string{"en0"},
},
b: &sockstats.SockStats{ b: &sockstats.SockStats{
Stats: map[sockstats.Label]sockstats.SockStat{ Stats: map[sockstats.Label]sockstats.SockStat{
sockstats.LabelDERPHTTPClient: { sockstats.LabelDERPHTTPClient: {
TxBytes: 10, TxBytes: 10,
RxBytes: 30, RxBytes: 30,
TxBytesByInterface: map[string]uint64{
"en0": 10,
},
RxBytesByInterface: map[string]uint64{
"en0": 30,
},
}, },
}, },
Interfaces: []string{"en0"},
}, },
wantStats: map[sockstats.Label]deltaStat{ wantStats: map[sockstats.Label]deltaStat{
sockstats.LabelDERPHTTPClient: {0, 20}, sockstats.LabelDERPHTTPClient: {0, 20},

@ -15,23 +15,17 @@ import (
) )
// SockStats contains statistics for sockets instrumented with the // SockStats contains statistics for sockets instrumented with the
// WithSockStats() function, along with the interfaces that we have // WithSockStats() function
// per-interface statistics for.
type SockStats struct { type SockStats struct {
Stats map[Label]SockStat Stats map[Label]SockStat
Interfaces []string
CurrentInterfaceCellular bool CurrentInterfaceCellular bool
} }
// SockStat contains the sent and received bytes for a socket instrumented with // SockStat contains the sent and received bytes for a socket instrumented with
// the WithSockStats() function. The bytes are also broken down by interface, // the WithSockStats() function.
// though this may be a subset of the total if interfaces were added after the
// instrumented socket was created.
type SockStat struct { type SockStat struct {
TxBytes uint64 TxBytes uint64
RxBytes uint64 RxBytes uint64
TxBytesByInterface map[string]uint64
RxBytesByInterface map[string]uint64
} }
// Label is an identifier for a socket that stats are collected for. A finite // Label is an identifier for a socket that stats are collected for. A finite
@ -67,6 +61,28 @@ func Get() *SockStats {
return get() return get()
} }
// InterfaceSockStats contains statistics for sockets instrumented with the
// WithSockStats() function, broken down by interface. The statistics may be a
// subset of the total if interfaces were added after the instrumented socket
// was created.
type InterfaceSockStats struct {
Stats map[Label]InterfaceSockStat
Interfaces []string
}
// InterfaceSockStat contains the per-interface sent and received bytes for a
// socket instrumented with the WithSockStats() function.
type InterfaceSockStat struct {
TxBytesByInterface map[string]uint64
RxBytesByInterface map[string]uint64
}
// GetWithInterfaces is a variant of Get that returns the current socket
// statistics broken down by interface. It is slightly more expensive than Get.
func GetInterfaces() *InterfaceSockStats {
return getInterfaces()
}
// ValidationSockStats contains external validation numbers for sockets // ValidationSockStats contains external validation numbers for sockets
// instrumented with WithSockStats. It may be a subset of the all sockets, // instrumented with WithSockStats. It may be a subset of the all sockets,
// depending on what externa measurement mechanisms the platform supports. // depending on what externa measurement mechanisms the platform supports.
@ -81,11 +97,11 @@ type ValidationSockStat struct {
RxBytes uint64 RxBytes uint64
} }
// GetWithValidation is a variant of GetWith that returns both the current stats // GetValidation is a variant of Get that returns external validation numbers
// and external validation numbers for the stats. It is more expensive than // for stats. It is more expensive than Get and should be used in debug
// Get and should be used in debug interfaces only. // interfaces only.
func GetWithValidation() (*SockStats, *ValidationSockStats) { func GetValidation() *ValidationSockStats {
return get(), getValidation() return getValidation()
} }
// LinkMonitor is the interface for the parts of wgengine/mointor's Mon that we // LinkMonitor is the interface for the parts of wgengine/mointor's Mon that we

@ -19,6 +19,10 @@ func get() *SockStats {
return nil return nil
} }
func getInterfaces() *InterfaceSockStats {
return nil
}
func getValidation() *ValidationSockStats { func getValidation() *ValidationSockStats {
return nil return nil
} }

@ -157,20 +157,37 @@ func get() *SockStats {
defer sockStats.mu.Unlock() defer sockStats.mu.Unlock()
r := &SockStats{ r := &SockStats{
Stats: make(map[Label]SockStat), Stats: make(map[Label]SockStat, len(sockStats.countersByLabel)),
Interfaces: make([]string, 0, len(sockStats.usedInterfaces)),
CurrentInterfaceCellular: sockStats.currentInterfaceCellular.Load(), CurrentInterfaceCellular: sockStats.currentInterfaceCellular.Load(),
} }
for label, counters := range sockStats.countersByLabel {
r.Stats[label] = SockStat{
TxBytes: counters.txBytes.Load(),
RxBytes: counters.rxBytes.Load(),
}
}
return r
}
func getInterfaces() *InterfaceSockStats {
sockStats.mu.Lock()
defer sockStats.mu.Unlock()
interfaceCount := len(sockStats.usedInterfaces)
r := &InterfaceSockStats{
Stats: make(map[Label]InterfaceSockStat, len(sockStats.countersByLabel)),
Interfaces: make([]string, 0, interfaceCount),
}
for iface := range sockStats.usedInterfaces { for iface := range sockStats.usedInterfaces {
r.Interfaces = append(r.Interfaces, sockStats.knownInterfaces[iface]) r.Interfaces = append(r.Interfaces, sockStats.knownInterfaces[iface])
} }
for label, counters := range sockStats.countersByLabel { for label, counters := range sockStats.countersByLabel {
s := SockStat{ s := InterfaceSockStat{
TxBytes: counters.txBytes.Load(), TxBytesByInterface: make(map[string]uint64, interfaceCount),
RxBytes: counters.rxBytes.Load(), RxBytesByInterface: make(map[string]uint64, interfaceCount),
TxBytesByInterface: make(map[string]uint64),
RxBytesByInterface: make(map[string]uint64),
} }
for iface, a := range counters.rxBytesByInterface { for iface, a := range counters.rxBytesByInterface {
ifName := sockStats.knownInterfaces[iface] ifName := sockStats.knownInterfaces[iface]

Loading…
Cancel
Save