Skip to content

Commit c649267

Browse files
authored
Merge pull request #41 from elecbug/elecbug/master
Update v0.12.2
2 parents 815c668 + 4b981f5 commit c649267

5 files changed

Lines changed: 163 additions & 38 deletions

File tree

.github/workflows/go.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ jobs:
1919
- name: Set up Go
2020
uses: actions/setup-go@v4
2121
with:
22-
go-version: '1.21.5'
22+
go-version: '1.25.0'
2323

2424
- name: Build
2525
run: go build -v ./...
26-
27-
- name: Test
28-
run: go test ./...

.vscode/settings.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

v2/graph/analyzer/analyzer.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ import (
88

99
// Analyzer represents a graph analyzer that can be computed based on a given graph.
1010
type Analyzer struct {
11-
baseGraph *graph.Graph // baseGraph is the original graph provided to the analyzer, used for reference and hashing.
12-
graphHash string // graphHash stores the hash of the base graph to detect changes and manage cache validity.
13-
allShortestPaths map[graph.NodeID]map[graph.NodeID][]graph.Path // allShortestPaths caches the results of shortest path computations between node pairs.
14-
mu sync.RWMutex // mu protects access to the allShortestPaths cache to ensure thread safety during concurrent reads/writes.
11+
baseGraph *graph.Graph // baseGraph is the original graph provided to the analyzer, used for reference and hashing.
12+
graphHash string // graphHash stores the hash of the base graph to detect changes and manage cache validity.
13+
allShortestPaths map[graph.NodeID]map[graph.NodeID][]graph.Path // allShortestPaths caches the results of shortest path computations between node pairs.
14+
mu sync.RWMutex // mu protects access to the allShortestPaths cache to ensure thread safety during concurrent reads/writes.
15+
parallelCoreCount int // parallelCoreCount determines how many CPU cores to utilize for parallel computations, if applicable.
1516
}
1617

1718
// NewAnalyzer creates a new Analyzer instance based on the provided graph.
18-
func NewAnalyzer(g *graph.Graph) *Analyzer {
19+
func NewAnalyzer(g *graph.Graph, parallelCoreCount int) *Analyzer {
1920
return &Analyzer{
20-
baseGraph: g,
21-
graphHash: "",
22-
allShortestPaths: make(map[graph.NodeID]map[graph.NodeID][]graph.Path),
21+
baseGraph: g,
22+
graphHash: "",
23+
allShortestPaths: make(map[graph.NodeID]map[graph.NodeID][]graph.Path),
24+
parallelCoreCount: parallelCoreCount,
2325
}
2426
}
2527

v2/graph/analyzer/analyzer_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
package analyzer_test
22

33
import (
4+
"fmt"
5+
"math/rand"
46
"testing"
7+
"time"
58

69
"github.com/elecbug/netkit/v2/graph"
710
"github.com/elecbug/netkit/v2/graph/analyzer"
11+
"github.com/elecbug/netkit/v2/graph/standard"
812
)
913

1014
// TestShortestPaths tests the ShortestPaths method of the Analyzer to ensure it correctly finds the shortest path between two nodes in a graph.
1115
func TestShortestPaths(t *testing.T) {
16+
fmt.Println("Test Shortest Paths")
17+
testComputeShortestPath(t)
18+
testPerformance(t)
19+
}
20+
21+
// testComputeShortestPath sets up a simple graph and tests the ComputeAllShortestPaths and ShortestPaths
22+
// methods of the Analyzer to verify that it correctly computes and caches shortest paths, and that
23+
// it updates the cache when the graph changes. It checks for correct path results and proper error handling when paths are removed.
24+
func testComputeShortestPath(t *testing.T) {
25+
fmt.Println("- Test Compute Shortest Paths")
1226
g := graph.New(true, true)
1327
g.AddNode("A")
1428
g.AddNode("B")
@@ -19,7 +33,7 @@ func TestShortestPaths(t *testing.T) {
1933
g.AddEdge("A", "C", graph.NewWeight(2))
2034
g.AddEdge("C", "D", graph.NewWeight(1))
2135

22-
a := analyzer.NewAnalyzer(g)
36+
a := analyzer.NewAnalyzer(g, 1)
2337

2438
paths, err := a.ShortestPaths("A", "D")
2539
if err != nil {
@@ -72,6 +86,69 @@ func TestShortestPaths(t *testing.T) {
7286
}
7387
}
7488

89+
func testPerformance(t *testing.T) {
90+
fmt.Println("- Test Performance")
91+
92+
g, err := standard.ErdosRenyiGraph(
93+
42,
94+
false,
95+
func(from, to graph.NodeID) *graph.Weight { return graph.NewWeight(rand.Float64() * 100) },
96+
1000,
97+
0.01,
98+
)
99+
if err != nil {
100+
t.Fatalf("failed to create graph: %v", err)
101+
}
102+
103+
a := analyzer.NewAnalyzer(g, 1)
104+
105+
startTime := time.Now()
106+
_, err = a.ShortestPaths("0", "999")
107+
if err != nil {
108+
t.Fatalf("unexpected error: %v", err)
109+
}
110+
duration := time.Since(startTime)
111+
fmt.Printf(" - Time taken to compute shortest paths: %v\n", duration)
112+
113+
a = analyzer.NewAnalyzer(g, 4)
114+
115+
startTime = time.Now()
116+
_, err = a.ShortestPaths("0", "999")
117+
if err != nil {
118+
t.Fatalf("unexpected error: %v", err)
119+
}
120+
duration = time.Since(startTime)
121+
fmt.Printf(" - Time taken to compute shortest paths with 4 cores: %v\n", duration)
122+
123+
a = analyzer.NewAnalyzer(g, 16)
124+
125+
startTime = time.Now()
126+
_, err = a.ShortestPaths("0", "999")
127+
if err != nil {
128+
t.Fatalf("unexpected error: %v", err)
129+
}
130+
duration = time.Since(startTime)
131+
fmt.Printf(" - Time taken to compute shortest paths with 16 cores: %v\n", duration)
132+
133+
a = analyzer.NewAnalyzer(g, 32)
134+
135+
startTime = time.Now()
136+
_, err = a.ShortestPaths("0", "999")
137+
if err != nil {
138+
t.Fatalf("unexpected error: %v", err)
139+
}
140+
duration = time.Since(startTime)
141+
fmt.Printf(" - Time taken to compute shortest paths with 32 cores: %v\n", duration)
142+
143+
startTime = time.Now()
144+
_, err = a.ShortestPaths("0", "999")
145+
if err != nil {
146+
t.Fatalf("unexpected error: %v", err)
147+
}
148+
duration = time.Since(startTime)
149+
fmt.Printf(" - Time taken to retrieve cached shortest paths: %v\n", duration)
150+
}
151+
75152
// equalPathSlices compares two graph.Path values by node sequence and total distance.
76153
func equalPathSlices(a, b graph.Path) bool {
77154
nodesA := a.Nodes()

v2/graph/analyzer/shortest_path.go

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package analyzer
22

33
import (
44
"container/heap"
5+
"context"
56
"fmt"
67
"math"
8+
"sync"
79

810
"github.com/elecbug/netkit/v2/graph"
911
)
@@ -19,7 +21,7 @@ func (a *Analyzer) ComputeAllShortestPaths() error {
1921
return nil
2022
}
2123

22-
paths, err := allShortestPaths(a.baseGraph)
24+
paths, err := allShortestPaths(a.baseGraph, a.parallelCoreCount)
2325
if err != nil {
2426
return err
2527
}
@@ -57,29 +59,86 @@ func (a *Analyzer) ShortestPaths(start, end graph.NodeID) ([]graph.Path, error)
5759
}
5860

5961
// allShortestPaths computes all shortest paths between reachable node pairs in the graph.
60-
func allShortestPaths(g *graph.Graph) (map[graph.NodeID]map[graph.NodeID][]graph.Path, error) {
62+
func allShortestPaths(
63+
g *graph.Graph, parallelCoreCount int,
64+
) (map[graph.NodeID]map[graph.NodeID][]graph.Path, error) {
65+
if parallelCoreCount <= 0 {
66+
parallelCoreCount = 1
67+
}
68+
6169
result := make(map[graph.NodeID]map[graph.NodeID][]graph.Path)
6270

63-
if !g.Weighted {
64-
for _, start := range g.Nodes() {
65-
pathsFromStart, err := allShortestPathsFromStart(g, start)
66-
if err != nil {
67-
return nil, err
68-
}
71+
nodes := g.Nodes()
6972

70-
result[start] = pathsFromStart
73+
ctx, cancel := context.WithCancel(context.Background())
74+
defer cancel()
75+
76+
core := make(chan struct{}, parallelCoreCount)
77+
78+
var wg sync.WaitGroup
79+
var mu sync.Mutex
80+
81+
var firstErr error
82+
var errOnce sync.Once
83+
84+
setErr := func(err error) {
85+
if err == nil {
86+
return
7187
}
7288

73-
return result, nil
89+
errOnce.Do(func() {
90+
firstErr = err
91+
cancel()
92+
})
7493
}
7594

76-
for _, start := range g.Nodes() {
77-
pathsFromStart, err := allWeightedShortestPathsFromStart(g, start)
78-
if err != nil {
79-
return nil, err
95+
Loop:
96+
for _, start := range nodes {
97+
select {
98+
case <-ctx.Done():
99+
break Loop
100+
default:
80101
}
81102

82-
result[start] = pathsFromStart
103+
core <- struct{}{}
104+
wg.Add(1)
105+
106+
go func(start graph.NodeID) {
107+
defer wg.Done()
108+
defer func() {
109+
<-core
110+
}()
111+
112+
select {
113+
case <-ctx.Done():
114+
return
115+
default:
116+
}
117+
118+
var pathsFromStart map[graph.NodeID][]graph.Path
119+
var err error
120+
121+
if !g.Weighted {
122+
pathsFromStart, err = allShortestPathsFromStart(g, start)
123+
} else {
124+
pathsFromStart, err = allWeightedShortestPathsFromStart(g, start)
125+
}
126+
127+
if err != nil {
128+
setErr(fmt.Errorf("failed to compute shortest paths from %s: %w", start, err))
129+
return
130+
}
131+
132+
mu.Lock()
133+
result[start] = pathsFromStart
134+
mu.Unlock()
135+
}(start)
136+
}
137+
138+
wg.Wait()
139+
140+
if firstErr != nil {
141+
return nil, firstErr
83142
}
84143

85144
return result, nil

0 commit comments

Comments
 (0)