Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
GO_VERSION: [ "1.19","1.20","1.21" ]
GO_VERSION:
- '1.24' # named in go.mod
- 'oldstable'
- 'stable'
steps:
- name: "Fetch source code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand Down Expand Up @@ -49,7 +52,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
GO_VERSION: [ "1.19","1.20","1.21" ]
GO_VERSION:
- '1.24' # named in go.mod
- 'oldstable'
- 'stable'
steps:
- name: "Fetch source code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand Down Expand Up @@ -90,7 +96,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
GO_VERSION: [ "1.19","1.20","1.21" ]
GO_VERSION:
- '1.24' # named in go.mod
- 'oldstable'
- 'stable'
steps:
- name: "Fetch source code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand Down Expand Up @@ -122,7 +131,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
GO_VERSION: [ "1.19","1.20","1.21" ]
GO_VERSION:
- '1.24' # named in go.mod
- 'oldstable'
- 'stable'
steps:
- name: "Fetch source code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand Down
314 changes: 167 additions & 147 deletions coordinate/performance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,179 +7,199 @@ import (
"math"
"testing"
"time"

"github.com/hashicorp/serf/testutil/retry"
)

func TestPerformance_Line(t *testing.T) {
const spacing = 10 * time.Millisecond
const nodes, cycles = 10, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}
truth := GenerateLine(nodes, spacing)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 {
t.Fatalf("performance stats are out of spec: %v", stats)
}
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const spacing = 10 * time.Millisecond
const nodes, cycles = 10, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}
truth := GenerateLine(nodes, spacing)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 {
r.Fatalf("performance stats are out of spec: %v", stats)
}
})
}

func TestPerformance_Grid(t *testing.T) {
const spacing = 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}
truth := GenerateGrid(nodes, spacing)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 {
t.Fatalf("performance stats are out of spec: %v", stats)
}
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const spacing = 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}
truth := GenerateGrid(nodes, spacing)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 {
r.Fatalf("performance stats are out of spec: %v", stats)
}
})
}

func TestPerformance_Split(t *testing.T) {
const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}
truth := GenerateSplit(nodes, lan, wan)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 {
t.Fatalf("performance stats are out of spec: %v", stats)
}
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}
truth := GenerateSplit(nodes, lan, wan)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 {
r.Fatalf("performance stats are out of spec: %v", stats)
}
})
}

func TestPerformance_Height(t *testing.T) {
const radius = 100 * time.Millisecond
const nodes, cycles = 25, 1000

// Constrain us to two dimensions so that we can just exactly represent
// the circle.
config := DefaultConfig()
config.Dimensionality = 2
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}

// Generate truth where the first coordinate is in the "middle" because
// it's equidistant from all the nodes, but it will have an extra radius
// added to the distance, so it should come out above all the others.
truth := GenerateCircle(nodes, radius)
Simulate(clients, truth, cycles)

// Make sure the height looks reasonable with the regular nodes all in a
// plane, and the center node up above.
for i := range clients {
coord := clients[i].GetCoordinate()
if i == 0 {
if coord.Height < 0.97*radius.Seconds() {
t.Fatalf("height is out of spec: %9.6f", coord.Height)
}
} else {
if coord.Height > 0.03*radius.Seconds() {
t.Fatalf("height is out of spec: %9.6f", coord.Height)
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const radius = 100 * time.Millisecond
const nodes, cycles = 25, 1000

// Constrain us to two dimensions so that we can just exactly represent
// the circle.
config := DefaultConfig()
config.Dimensionality = 2
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}

// Generate truth where the first coordinate is in the "middle" because
// it's equidistant from all the nodes, but it will have an extra radius
// added to the distance, so it should come out above all the others.
truth := GenerateCircle(nodes, radius)
Simulate(clients, truth, cycles)

// Make sure the height looks reasonable with the regular nodes all in a
// plane, and the center node up above.
for i := range clients {
coord := clients[i].GetCoordinate()
if i == 0 {
if coord.Height < 0.97*radius.Seconds() {
r.Fatalf("height is out of spec: %9.6f", coord.Height)
}
} else {
if coord.Height > 0.03*radius.Seconds() {
r.Fatalf("height is out of spec: %9.6f", coord.Height)
}
}
}
}
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 {
t.Fatalf("performance stats are out of spec: %v", stats)
}
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 {
r.Fatalf("performance stats are out of spec: %v", stats)
}
})
}

func TestPerformance_Drift(t *testing.T) {
const dist = 500 * time.Millisecond
const nodes = 4
config := DefaultConfig()
config.Dimensionality = 2
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}

// Do some icky surgery on the clients to put them into a square, up in
// the first quadrant.
clients[0].coord.Vec = []float64{0.0, 0.0}
clients[1].coord.Vec = []float64{0.0, dist.Seconds()}
clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}
clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}

// Make a corresponding truth matrix. The nodes are laid out like this
// so the distances are all equal, except for the diagonal:
//
// (1) <- dist -> (2)
//
// | <- dist |
// | |
// | dist -> |
//
// (0) <- dist -> (3)
//
truth := make([][]time.Duration, nodes)
for i := range truth {
truth[i] = make([]time.Duration, nodes)
}
for i := 0; i < nodes; i++ {
for j := i + 1; j < nodes; j++ {
rtt := dist
if (i%2 == 0) && (j%2 == 0) {
rtt = time.Duration(math.Sqrt2 * float64(rtt))
}
truth[i][j], truth[j][i] = rtt, rtt
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const dist = 500 * time.Millisecond
const nodes = 4
config := DefaultConfig()
config.Dimensionality = 2
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}
}

calcCenterError := func() float64 {
min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate()
for i := 1; i < nodes; i++ {
coord := clients[i].GetCoordinate()
for j, v := range coord.Vec {
min.Vec[j] = math.Min(min.Vec[j], v)
max.Vec[j] = math.Max(max.Vec[j], v)
// Do some icky surgery on the clients to put them into a square, up in
// the first quadrant.
clients[0].coord.Vec = []float64{0.0, 0.0}
clients[1].coord.Vec = []float64{0.0, dist.Seconds()}
clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}
clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}

// Make a corresponding truth matrix. The nodes are laid out like this
// so the distances are all equal, except for the diagonal:
//
// (1) <- dist -> (2)
//
// | <- dist |
// | |
// | dist -> |
//
// (0) <- dist -> (3)
//
truth := make([][]time.Duration, nodes)
for i := range truth {
truth[i] = make([]time.Duration, nodes)
}
for i := 0; i < nodes; i++ {
for j := i + 1; j < nodes; j++ {
rtt := dist
if (i%2 == 0) && (j%2 == 0) {
rtt = time.Duration(math.Sqrt2 * float64(rtt))
}
truth[i][j], truth[j][i] = rtt, rtt
}
}

mid := make([]float64, config.Dimensionality)
for i := range mid {
mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2
calcCenterError := func() float64 {
min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate()
for i := 1; i < nodes; i++ {
coord := clients[i].GetCoordinate()
for j, v := range coord.Vec {
min.Vec[j] = math.Min(min.Vec[j], v)
max.Vec[j] = math.Max(max.Vec[j], v)
}
}

mid := make([]float64, config.Dimensionality)
for i := range mid {
mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2
}
return magnitude(mid)
}
return magnitude(mid)
}

// Let the simulation run for a while to stabilize, then snap a baseline
// for the center error.
Simulate(clients, truth, 1000)
baseline := calcCenterError()
// Let the simulation run for a while to stabilize, then snap a baseline
// for the center error.
Simulate(clients, truth, 1000)
baseline := calcCenterError()

// Now run for a bunch more cycles and see if gravity pulls the center
// in the right direction.
Simulate(clients, truth, 10000)
if error := calcCenterError(); error > 0.8*baseline {
t.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error)
}
// Now run for a bunch more cycles and see if gravity pulls the center
// in the right direction.
Simulate(clients, truth, 10000)
if error := calcCenterError(); error > 0.8*baseline {
r.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error)
}
})
}

func TestPerformance_Random(t *testing.T) {
const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
t.Fatal(err)
}
truth := GenerateRandom(nodes, mean, deviation)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 {
t.Fatalf("performance stats are out of spec: %v", stats)
}
retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) {

const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond
const nodes, cycles = 25, 1000
config := DefaultConfig()
clients, err := GenerateClients(nodes, config)
if err != nil {
r.Fatal(err)
}
truth := GenerateRandom(nodes, mean, deviation)
Simulate(clients, truth, cycles)
stats := Evaluate(clients, truth)
if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 {
r.Fatalf("performance stats are out of spec: %v", stats)
}
})
}
Loading
Loading