Skip to content

Commit 919d6ed

Browse files
committed
Facilitate reporting internal structure metrics
1 parent 94a60c0 commit 919d6ed

File tree

6 files changed

+270
-7
lines changed

6 files changed

+270
-7
lines changed

cmd/routesum/main.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import (
1717
func main() {
1818
cpuProfile := flag.String("cpuprofile", "", "write cpu profile to file")
1919
memProfile := flag.String("memprofile", "", "write mem profile to file")
20+
showMemStats := flag.Bool(
21+
"show-mem-stats",
22+
false,
23+
"Whether or not to write memory usage stats to STDERR. (This functionity requires the use of `unsafe`, so may not be perfect.)", //nolint: lll
24+
)
2025
flag.Parse()
2126

2227
var cpuProfOut io.Writer
@@ -36,15 +41,20 @@ func main() {
3641
}
3742
}
3843

39-
if err := summarize(os.Stdin, os.Stdout, cpuProfOut, memProfOut); err != nil {
44+
var memStatsOut io.Writer
45+
if *showMemStats {
46+
memStatsOut = os.Stderr
47+
}
48+
49+
if err := summarize(os.Stdin, os.Stdout, memStatsOut, cpuProfOut, memProfOut); err != nil {
4050
fmt.Fprintf(os.Stderr, "summarize: %s\n", err.Error())
4151
os.Exit(1)
4252
}
4353
}
4454

4555
func summarize(
4656
in io.Reader,
47-
out, cpuProfOut io.Writer,
57+
out, memStatsOut, cpuProfOut io.Writer,
4858
memProfOut io.WriteCloser,
4959
) error {
5060
if cpuProfOut != nil {
@@ -76,6 +86,23 @@ func summarize(
7686
}
7787
}
7888

89+
if memStatsOut != nil {
90+
numInternalNodes, numLeafNodes, internalNodesTotalSize, leafNodesTotalSize := rs.MemUsage()
91+
fmt.Fprintf(memStatsOut,
92+
`Num internal nodes: %d
93+
Num leaf nodes: %d
94+
Size of all internal nodes: %d
95+
Size of all leaf nodes: %d
96+
Total size of data structure: %d
97+
`,
98+
numInternalNodes,
99+
numLeafNodes,
100+
internalNodesTotalSize,
101+
leafNodesTotalSize,
102+
internalNodesTotalSize+leafNodesTotalSize,
103+
)
104+
}
105+
79106
for _, s := range rs.SummaryStrings() {
80107
if _, err := out.Write([]byte(s + "\n")); err != nil {
81108
return fmt.Errorf("write output: %w", err)

cmd/routesum/main_test.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"io"
5+
"regexp"
46
"strings"
57
"testing"
68

@@ -9,12 +11,42 @@ import (
911
)
1012

1113
func TestSummarize(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
showMemStats bool
17+
expected *regexp.Regexp
18+
}{
19+
{
20+
name: "without memory statistics",
21+
showMemStats: false,
22+
expected: regexp.MustCompile(`^$`),
23+
},
24+
{
25+
name: "with memory statistics",
26+
showMemStats: true,
27+
expected: regexp.MustCompile(`Num internal nodes`),
28+
},
29+
}
30+
1231
inStr := "\n192.0.2.0\n192.0.2.1\n"
13-
in := strings.NewReader(inStr)
14-
var out strings.Builder
1532

16-
err := summarize(in, &out, nil, nil)
17-
require.NoError(t, err, "summarize does not throw an error")
33+
for _, test := range tests {
34+
t.Run(test.name, func(t *testing.T) {
35+
in := strings.NewReader(inStr)
36+
var out strings.Builder
37+
38+
var memStatsBuilder strings.Builder
39+
var memStatsOut io.Writer
40+
41+
if test.showMemStats {
42+
memStatsOut = &memStatsBuilder
43+
}
44+
45+
err := summarize(in, &out, memStatsOut, nil, nil)
46+
require.NoError(t, err, "summarize does not throw an error")
1847

19-
assert.Equal(t, "192.0.2.0/31\n", out.String(), "read expected output")
48+
assert.Equal(t, "192.0.2.0/31\n", out.String(), "read expected output")
49+
assert.Regexp(t, test.expected, memStatsBuilder.String(), "read expected memory stats")
50+
})
51+
}
2052
}

pkg/routesum/routesum.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,13 @@ func ipv6FromBits(bits bitslice.BitSlice) netaddr.IP {
142142
copy(byteArray[:], bytes[0:16])
143143
return netaddr.IPv6Raw(byteArray)
144144
}
145+
146+
// MemUsage provides information about memory usage.
147+
func (rs *RouteSum) MemUsage() (uint, uint, uintptr, uintptr) {
148+
ipv4NumInternalNodes, ipv4NumLeafNodes, ipv4InternalNodesTotalSize, ipv4LeafNodesTotalSize := rs.ipv4.MemUsage()
149+
ipv6NumInternalNodes, ipv6NumLeafNodes, ipv6InternalNodesTotalSize, ipv6LeafNodesTotalSize := rs.ipv6.MemUsage()
150+
return ipv4NumInternalNodes + ipv6NumInternalNodes,
151+
ipv4NumLeafNodes + ipv6NumLeafNodes,
152+
ipv4InternalNodesTotalSize + ipv6InternalNodesTotalSize,
153+
ipv4LeafNodesTotalSize + ipv6LeafNodesTotalSize
154+
}

pkg/routesum/routesum_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,76 @@ func TestSummarize(t *testing.T) { //nolint: funlen
455455
})
456456
}
457457
}
458+
459+
func TestRSTrieMemUsage(t *testing.T) { //nolint: funlen
460+
tests := []struct {
461+
name string
462+
entries []string
463+
expectedNumInternalNodes uint
464+
expectedNumLeafNodes uint
465+
}{
466+
{
467+
name: "new trie",
468+
expectedNumInternalNodes: 0,
469+
expectedNumLeafNodes: 0,
470+
},
471+
{
472+
name: "one item, IPv4",
473+
entries: []string{
474+
"192.0.2.1",
475+
},
476+
expectedNumInternalNodes: 0,
477+
expectedNumLeafNodes: 1,
478+
},
479+
{
480+
name: "two items, IPv4, summarized",
481+
entries: []string{
482+
"192.0.2.1",
483+
"192.0.2.0",
484+
},
485+
expectedNumInternalNodes: 0,
486+
expectedNumLeafNodes: 1,
487+
},
488+
{
489+
name: "two items, IPv4, unsummarized",
490+
entries: []string{
491+
"192.0.2.1",
492+
"192.0.2.2",
493+
},
494+
expectedNumInternalNodes: 1,
495+
expectedNumLeafNodes: 2,
496+
},
497+
{
498+
name: "one item, IPv6",
499+
entries: []string{
500+
"2001:db8::1",
501+
},
502+
expectedNumInternalNodes: 0,
503+
expectedNumLeafNodes: 1,
504+
},
505+
{
506+
name: "one IPv4 address, one IPv6 address",
507+
entries: []string{
508+
"192.0.2.0",
509+
"2001:db8::1",
510+
},
511+
expectedNumInternalNodes: 0,
512+
expectedNumLeafNodes: 2,
513+
},
514+
}
515+
516+
for _, test := range tests {
517+
t.Run(test.name, func(t *testing.T) {
518+
rs := NewRouteSum()
519+
520+
for _, entry := range test.entries {
521+
err := rs.InsertFromString(entry)
522+
require.NoError(t, err)
523+
}
524+
525+
numInternalNodes, numLeafNodes, _, _ := rs.MemUsage()
526+
assert.Equal(t, test.expectedNumInternalNodes, numInternalNodes, "num internal nodes")
527+
assert.Equal(t, test.expectedNumLeafNodes, numLeafNodes, "num leaf nodes")
528+
})
529+
}
530+
}

pkg/routesum/rstrie/rstrie.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package rstrie
44
import (
55
"bytes"
66
"container/list"
7+
"unsafe"
78

89
"github.com/PatrickCronin/routesum/pkg/routesum/bitslice"
910
)
@@ -194,3 +195,61 @@ func (t *RSTrie) Contents() []bitslice.BitSlice {
194195

195196
return contents
196197
}
198+
199+
func (t *RSTrie) visitAll(cb func(*node)) {
200+
// If the trie is empty
201+
if t.root == nil {
202+
return
203+
}
204+
205+
// Otherwise
206+
remainingSteps := list.New()
207+
remainingSteps.PushFront(traversalStep{
208+
n: t.root,
209+
precedingRouteBits: bitslice.BitSlice{},
210+
})
211+
212+
for remainingSteps.Len() > 0 {
213+
curNode := remainingSteps.Remove(remainingSteps.Front()).(traversalStep)
214+
215+
// Act on this node
216+
cb(curNode.n)
217+
218+
// Traverse the remainder of the nodes
219+
if !curNode.n.isLeaf() {
220+
curNodeRouteBits := bitslice.BitSlice{}
221+
curNodeRouteBits = append(curNodeRouteBits, curNode.precedingRouteBits...)
222+
curNodeRouteBits = append(curNodeRouteBits, curNode.n.bits...)
223+
224+
remainingSteps.PushFront(traversalStep{
225+
n: curNode.n.children[1],
226+
precedingRouteBits: curNodeRouteBits,
227+
})
228+
remainingSteps.PushFront(traversalStep{
229+
n: curNode.n.children[0],
230+
precedingRouteBits: curNodeRouteBits,
231+
})
232+
}
233+
}
234+
}
235+
236+
// MemUsage returns information about an RSTrie's current size in memory.
237+
func (t *RSTrie) MemUsage() (uint, uint, uintptr, uintptr) {
238+
var numInternalNodes, numLeafNodes uint
239+
var internalNodesTotalSize, leafNodesTotalSize uintptr
240+
241+
tallyNode := func(n *node) {
242+
baseNodeSize := unsafe.Sizeof(node{}) + uintptr(cap(n.bits))*unsafe.Sizeof([1]byte{}) //nolint: exhaustruct, gosec, lll
243+
if n.isLeaf() {
244+
numLeafNodes++
245+
leafNodesTotalSize += baseNodeSize
246+
return
247+
}
248+
249+
numInternalNodes++
250+
internalNodesTotalSize += baseNodeSize + unsafe.Sizeof([2]*node{}) //nolint: gosec
251+
}
252+
t.visitAll(tallyNode)
253+
254+
return numInternalNodes, numLeafNodes, internalNodesTotalSize, leafNodesTotalSize
255+
}

pkg/routesum/rstrie/rstrie_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package rstrie
22

33
import (
4+
"net/netip"
45
"testing"
56

67
"github.com/PatrickCronin/routesum/pkg/routesum/bitslice"
78
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestCommonPrefixLen(t *testing.T) {
@@ -224,3 +226,63 @@ func TestRSTrieContents(t *testing.T) { //nolint: funlen
224226
})
225227
}
226228
}
229+
230+
func TestRSTrieMemUsage(t *testing.T) {
231+
tests := []struct {
232+
name string
233+
entries []string
234+
expectedNumInternalNodes uint
235+
expectedNumLeafNodes uint
236+
}{
237+
{
238+
name: "new trie",
239+
expectedNumInternalNodes: 0,
240+
expectedNumLeafNodes: 0,
241+
},
242+
{
243+
name: "one item",
244+
entries: []string{
245+
"192.0.2.1",
246+
},
247+
expectedNumInternalNodes: 0,
248+
expectedNumLeafNodes: 1,
249+
},
250+
{
251+
name: "two items, summarized",
252+
entries: []string{
253+
"192.0.2.1",
254+
"192.0.2.0",
255+
},
256+
expectedNumInternalNodes: 0,
257+
expectedNumLeafNodes: 1,
258+
},
259+
{
260+
name: "two items, unsummarized",
261+
entries: []string{
262+
"192.0.2.1",
263+
"192.0.2.2",
264+
},
265+
expectedNumInternalNodes: 1,
266+
expectedNumLeafNodes: 2,
267+
},
268+
}
269+
270+
for _, test := range tests {
271+
t.Run(test.name, func(t *testing.T) {
272+
trie := NewRSTrie()
273+
274+
for _, entry := range test.entries {
275+
ip := netip.MustParseAddr(entry)
276+
ipBytes, err := ip.MarshalBinary()
277+
require.NoError(t, err)
278+
ipBits, err := bitslice.NewFromBytes(ipBytes)
279+
require.NoError(t, err)
280+
trie.InsertRoute(ipBits)
281+
}
282+
283+
numInternalNodes, numLeafNodes, _, _ := trie.MemUsage()
284+
assert.Equal(t, test.expectedNumInternalNodes, numInternalNodes, "num internal nodes")
285+
assert.Equal(t, test.expectedNumLeafNodes, numLeafNodes, "num leaf nodes")
286+
})
287+
}
288+
}

0 commit comments

Comments
 (0)