├── .gitignore
├── cpu1.svg
├── cpu2.svg
├── cpu3.svg
├── cpu4.svg
├── cpu5.svg
├── cpu6.svg
├── cpu7.svg
├── cpu8.svg
├── cpu9.svg
├── list.go
├── node.go
├── node_test.go
└── testdata
└── edges.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.prof
2 | *.test
3 |
--------------------------------------------------------------------------------
/cpu6.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
1631 |
--------------------------------------------------------------------------------
/cpu7.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
732 |
--------------------------------------------------------------------------------
/cpu8.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
756 |
--------------------------------------------------------------------------------
/cpu9.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
471 |
--------------------------------------------------------------------------------
/list.go:
--------------------------------------------------------------------------------
1 | package graphblog
2 |
3 | type listElt struct {
4 | next *listElt
5 | node *node
6 | }
7 |
8 | type list struct {
9 | head *listElt
10 | tail *listElt
11 |
12 | free *listElt
13 | }
14 |
15 | func (l *list) getHead() *node {
16 | elt := l.head
17 | if elt == nil {
18 | return nil
19 | }
20 |
21 | // Remove elt from the list
22 | l.head = elt.next
23 | if l.head == nil {
24 | l.tail = nil
25 | }
26 | // Add elt to the free list
27 | elt.next = l.free
28 | l.free = elt
29 |
30 | n := elt.node
31 | elt.node = nil
32 | return n
33 | }
34 |
35 | func (l *list) pushBack(n *node) {
36 | // Get a free listElt to use to point to this node
37 | elt := l.free
38 | if elt == nil {
39 | elt = &listElt{}
40 | } else {
41 | l.free = elt.next
42 | elt.next = nil
43 | }
44 |
45 | // Add the element to the tail of the list
46 | elt.node = n
47 | if l.tail == nil {
48 | l.tail = elt
49 | l.head = elt
50 | } else {
51 | l.tail.next = elt
52 | l.tail = elt
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/node.go:
--------------------------------------------------------------------------------
1 | package graphblog
2 |
3 | import (
4 | "runtime"
5 | "sync"
6 | )
7 |
8 | type nodeId int32
9 | type nodeName string
10 |
11 | type symbolTable map[nodeName]nodeId
12 |
13 | func (s symbolTable) getId(name nodeName) nodeId {
14 | id, ok := s[name]
15 | if !ok {
16 | id = nodeId(len(s))
17 | s[name] = id
18 | }
19 | return id
20 | }
21 |
22 | type graph struct {
23 | symbolTable
24 | nodes
25 | }
26 |
27 | func New(numNodes int) *graph {
28 | g := &graph{
29 | symbolTable: make(symbolTable, numNodes),
30 | nodes: make(nodes, numNodes),
31 | }
32 | g.nodes.init()
33 | return g
34 | }
35 |
36 | func (g *graph) addEdge(a, b nodeName) {
37 | aid := g.symbolTable.getId(a)
38 | bid := g.symbolTable.getId(b)
39 |
40 | g.nodes.addEdge(aid, bid)
41 | }
42 |
43 | type node struct {
44 | id nodeId
45 |
46 | // adjacent edges
47 | adj []nodeId
48 | }
49 |
50 | func (n *node) add(adjNode *node) {
51 | for _, id := range n.adj {
52 | if id == adjNode.id {
53 | return
54 | }
55 | }
56 | n.adj = append(n.adj, adjNode.id)
57 | }
58 |
59 | type nodes []node
60 |
61 | func (nl nodes) init() {
62 | for i := range nl {
63 | nl[i].id = nodeId(i)
64 | }
65 | }
66 |
67 | func (nl nodes) get(id nodeId) *node {
68 | return &nl[id]
69 | }
70 |
71 | func (nl nodes) addEdge(a, b nodeId) {
72 | an := nl.get(a)
73 | bn := nl.get(b)
74 |
75 | an.add(bn)
76 | bn.add(an)
77 | }
78 |
79 | // diameter is the maximum length of a shortest path in the network
80 | func (nl nodes) diameter() int {
81 |
82 | cpus := runtime.NumCPU()
83 | numNodes := len(nl)
84 | nodesPerCpu := numNodes / cpus
85 |
86 | results := make([]int, cpus)
87 | wg := &sync.WaitGroup{}
88 | wg.Add(cpus)
89 | start := 0
90 | for cpu := 0; cpu < cpus; cpu++ {
91 | end := start + nodesPerCpu
92 | if cpu == cpus-1 {
93 | end = numNodes
94 | }
95 |
96 | go func(cpu int, start, end nodeId) {
97 | defer wg.Done()
98 | var diameter int
99 | q := &list{}
100 | depths := make([]bfsNode, numNodes)
101 | for id := start; id < end; id++ {
102 | // Need to reset the bfsData between runs
103 | for i := range depths {
104 | depths[i] = -1
105 | }
106 |
107 | df := nl.longestShortestPath(nodeId(id), q, depths)
108 | if df > diameter {
109 | diameter = df
110 | }
111 | }
112 | results[cpu] = diameter
113 | }(cpu, nodeId(start), nodeId(end))
114 | start += nodesPerCpu
115 | }
116 |
117 | wg.Wait()
118 |
119 | diameter := 0
120 | for _, result := range results {
121 | if result > diameter {
122 | diameter = result
123 | }
124 | }
125 | return diameter
126 | }
127 |
128 | // bfs tracking data
129 | type bfsNode int16
130 |
131 | func (nodes nodes) longestShortestPath(start nodeId, q *list, depths []bfsNode) int {
132 |
133 | n := nodes.get(start)
134 | depths[n.id] = 0
135 | q.pushBack(n)
136 |
137 | for {
138 | newN := q.getHead()
139 | if newN == nil {
140 | break
141 | }
142 | n = newN
143 |
144 | for _, id := range n.adj {
145 | if depths[id] == -1 {
146 | depths[id] = depths[n.id] + 1
147 | q.pushBack(nodes.get(id))
148 | }
149 | }
150 | }
151 |
152 | return int(depths[n.id])
153 | }
154 |
--------------------------------------------------------------------------------
/node_test.go:
--------------------------------------------------------------------------------
1 | package graphblog
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "bufio"
8 |
9 | "strings"
10 |
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | type edge struct{ a, b nodeName }
15 | type edgeList []edge
16 |
17 | func (e edgeList) build(g *graph) {
18 | for _, edge := range e {
19 | g.addEdge(edge.a, edge.b)
20 | }
21 | }
22 |
23 | func TestDiameter(t *testing.T) {
24 |
25 | tests := []struct {
26 | name string
27 | edgeList edgeList
28 | expDiameter int
29 | }{
30 | {
31 | name: "empty",
32 | },
33 | {
34 | name: "1 edge",
35 | edgeList: edgeList{{"a", "b"}},
36 | expDiameter: 1,
37 | },
38 | {
39 | name: "3 in line",
40 | edgeList: edgeList{{"a", "b"}, {"b", "c"}},
41 | expDiameter: 2,
42 | },
43 | {
44 | name: "4 in line",
45 | edgeList: edgeList{{"a", "b"}, {"b", "c"}, {"c", "d"}},
46 | expDiameter: 3,
47 | },
48 | {
49 | name: "Triangle",
50 | edgeList: edgeList{{"a", "b"}, {"b", "c"}, {"a", "c"}},
51 | expDiameter: 1,
52 | },
53 | {
54 | name: "Square",
55 | edgeList: edgeList{{"a", "b"}, {"b", "c"}, {"c", "d"}, {"a", "d"}},
56 | expDiameter: 2,
57 | },
58 | {
59 | name: "2 loops",
60 | edgeList: edgeList{{"a", "b"}, {"b", "c"}, {"c", "a"}, {"c", "d"}, {"d", "e"}, {"e", "c"}},
61 | expDiameter: 2,
62 | },
63 | }
64 |
65 | for _, test := range tests {
66 | t.Run(test.name, func(t *testing.T) {
67 | g := New(100)
68 | test.edgeList.build(g)
69 | dia := g.diameter()
70 | if dia != test.expDiameter {
71 | t.Errorf("Diameter not as expected. Have %d, expected %d", dia, test.expDiameter)
72 | }
73 | })
74 | }
75 | }
76 |
77 | func BenchmarkDiameter(b *testing.B) {
78 | g := New(10000)
79 | // Load the test data
80 | f, err := os.Open("testdata/edges.txt")
81 | assert.NoError(b, err)
82 | s := bufio.NewScanner(f)
83 | for s.Scan() {
84 | line := s.Text()
85 | edge := strings.Fields(line)
86 | assert.Len(b, edge, 2)
87 | g.addEdge(nodeName(edge[0]), nodeName(edge[1]))
88 | }
89 | assert.NoError(b, err)
90 |
91 | b.Run("diameter", func(b *testing.B) {
92 | b.ReportAllocs()
93 | for i := 0; i < b.N; i++ {
94 | d := g.diameter()
95 | assert.Equal(b, 82, d)
96 | }
97 | })
98 | }
99 |
--------------------------------------------------------------------------------