Files
charleswiltgen__axiom/tools/xcprof/network_test.go
T
Charles Wiltgen 427d4922d1 feat(xcprof): parse network family; report memory/energy as not_exportable
Parse the network-connection-stat export (socket-level connection stats)
into per-connection totals — process, protocol, remote, bytes/packets in
and out — aggregated by connection serial across interval rows. New
network.go reuses the cpu-profile id/ref resolution but maps generic cells
positionally against the schema's column mnemonics.

Verified against real Xcode 26 exports: the memory (Allocations/Leaks) and
macOS energy (Power Profiler) instruments are not surfaced by xctrace
export at all (their data lives in the trace event store / the instrument
is iOS-only), so the support matrix now reports them not_exportable with a
pointer to Instruments.app — replacing the Phase 1 guessed schema names.

Also fix the record network preset to use the Network Connections
instrument (which yields network-connection-stat), not HTTP Traffic
(URLSession only) — the same class as the cpu CPU-Profiler finding.
2026-06-05 12:22:27 -07:00

123 lines
4.2 KiB
Go

package main
import (
"os"
"testing"
)
// loadNetFixture reads the trimmed real network-connection-stat export (16
// interval rows over 13 connections, captured on macOS 26.5 / Instruments 16.0).
func loadNetFixture(t *testing.T) []byte {
t.Helper()
data, err := os.ReadFile("testdata/network-connection-stat.xml")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
return data
}
func TestParseNetworkStatAggregatesByConnection(t *testing.T) {
// Expected values were computed independently from the fixture (see the
// Python ground-truth pass in the Phase 2d work): 13 distinct connection
// serials, summed rx/tx across each serial's interval rows.
rep, err := parseNetworkStat(loadNetFixture(t), 15)
if err != nil {
t.Fatalf("parseNetworkStat: %v", err)
}
if rep.Connections != 13 {
t.Errorf("Connections = %d, want 13", rep.Connections)
}
if rep.TotalRxBytes != 302474 {
t.Errorf("TotalRxBytes = %d, want 302474", rep.TotalRxBytes)
}
if rep.TotalTxBytes != 4639 {
t.Errorf("TotalTxBytes = %d, want 4639", rep.TotalTxBytes)
}
}
func TestParseNetworkStatTopTalkerResolvesRefs(t *testing.T) {
// The hottest connection by bytes is our own curl-local fetch of apple.com's
// CDN. Its process is carried by a ref to an earlier row's id; getting the
// name + pid right proves cross-row ref resolution works (the same dedup the
// cpu-profile parser handles).
rep, err := parseNetworkStat(loadNetFixture(t), 15)
if err != nil {
t.Fatalf("parseNetworkStat: %v", err)
}
if len(rep.TopByBytes) == 0 {
t.Fatal("TopByBytes is empty")
}
got := rep.TopByBytes[0]
want := NetConnection{
Process: "curl-local",
PID: 59752,
Protocol: "tcp4",
Interface: "Ethernet",
Local: "10.0.0.114:50479",
Remote: "23.61.213.25:443",
RxBytes: 264996,
TxBytes: 589,
RxPackets: 30,
TxPackets: 7,
Intervals: 1,
}
if got != want {
t.Errorf("TopByBytes[0]\n got = %+v\nwant = %+v", got, want)
}
}
func TestParseNetworkStatTopNLimits(t *testing.T) {
rep, err := parseNetworkStat(loadNetFixture(t), 3)
if err != nil {
t.Fatalf("parseNetworkStat: %v", err)
}
if len(rep.TopByBytes) != 3 {
t.Errorf("TopByBytes length = %d, want 3 (limited)", len(rep.TopByBytes))
}
// Connections counts all distinct serials regardless of the top-N display cap.
if rep.Connections != 13 {
t.Errorf("Connections = %d, want 13", rep.Connections)
}
}
func TestParseNetworkStatCountsUnattributedTraffic(t *testing.T) {
// A row with byte counters but no connection serial (sentinel) can't be
// attributed to a connection; it must be counted, not silently dropped, so a
// shortfall in the totals is visible.
in := []byte(`<?xml version="1.0"?><trace-query-result><node xpath='x'>` +
`<schema name="network-connection-stat">` +
`<col><mnemonic>connection-serial</mnemonic></col>` +
`<col><mnemonic>rx-bytes</mnemonic></col></schema>` +
`<row><sentinel/><network-size-in-bytes id="1" fmt="100 Bytes">100</network-size-in-bytes></row>` +
`</node></trace-query-result>`)
rep, err := parseNetworkStat(in, 15)
if err != nil {
t.Fatalf("parseNetworkStat: %v", err)
}
if rep.Connections != 0 {
t.Errorf("Connections = %d, want 0 (row had no serial)", rep.Connections)
}
if rep.UnattributedRows != 1 {
t.Errorf("UnattributedRows = %d, want 1", rep.UnattributedRows)
}
if rep.TotalRxBytes != 0 {
t.Errorf("TotalRxBytes = %d, want 0 (unattributed traffic is not summed into a connection)", rep.TotalRxBytes)
}
}
func TestParseNetworkStatEmptyTable(t *testing.T) {
// A schema-only export (instrument recorded, no traffic) must parse to an
// empty-but-valid report, never an error — the honesty contract distinguishes
// "measured, nothing happened" from "couldn't measure".
empty := []byte(`<?xml version="1.0"?><trace-query-result><node xpath='x'>` +
`<schema name="network-connection-stat"><col><mnemonic>start-time</mnemonic></col>` +
`<col><mnemonic>connection-serial</mnemonic></col></schema></node></trace-query-result>`)
rep, err := parseNetworkStat(empty, 15)
if err != nil {
t.Fatalf("parseNetworkStat(empty): %v", err)
}
if rep.Connections != 0 || len(rep.TopByBytes) != 0 {
t.Errorf("empty table: got Connections=%d Top=%d, want 0/0", rep.Connections, len(rep.TopByBytes))
}
}