Skip to content

Commit

Permalink
feat: add square wave latency summand (#17)
Browse files Browse the repository at this point in the history
* feat: add square wave summand

* test: ensure that speedbump has started before dialing TCP in echo server test

* docs: add square wave latency flags in cli arg reference

* format: fix LatencyCfg struct initialization indent

* refactor: simplify square wave formula
  • Loading branch information
szkf committed Aug 2, 2022
1 parent 6c9dc70 commit a556c99
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 23 deletions.
28 changes: 16 additions & 12 deletions README.md
@@ -1,4 +1,5 @@
# speedbump - TCP proxy with variable latency

<div align="center">
<img alt="speedbump logo" src="https://github.com/kffl/speedbump/raw/HEAD/assets/speedbump.gif" width="480" height="auto"/>
</div>
Expand Down Expand Up @@ -32,6 +33,7 @@ Spawn a new instance with a base latency of 300ms and a sawtooth wave latency su
```
speedbump --latency=300ms --saw-amplitude=200ms --saw-period=2m --port=2000 localhost:80
```

<div align="center">
<img alt="speedbump sawtooth wave graph" src="https://github.com/kffl/speedbump/raw/HEAD/assets/sawtooth.svg" width="800" height="auto"/>
</div>
Expand All @@ -54,17 +56,19 @@ usage: speedbump [<flags>] <destination>
TCP proxy for simulating variable network latency.
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
--port=8000 Port number to listen on.
--buffer=64KB Size of the buffer used for TCP reads.
--queue-size=1024 Size of the delay queue storing read buffers.
--latency=5ms Base latency added to proxied traffic.
--log-level=INFO Log level. Possible values: DEBUG, TRACE, INFO, WARN, ERROR.
--sine-amplitude=0 Amplitude of the latency sine wave.
--sine-period=0 Period of the latency sine wave.
--saw-amplitude=0 Amplitude of the latency sawtooth wave.
--saw-period=0 Period of the latency sawtooth wave.
--version Show application version.
--help Show context-sensitive help (also try --help-long and --help-man).
--port=8000 Port number to listen on.
--buffer=64KB Size of the buffer used for TCP reads.
--queue-size=1024 Size of the delay queue storing read buffers.
--latency=5ms Base latency added to proxied traffic.
--log-level=INFO Log level. Possible values: DEBUG, TRACE, INFO, WARN, ERROR.
--sine-amplitude=0 Amplitude of the latency sine wave.
--sine-period=0 Period of the latency sine wave.
--saw-amplitude=0 Amplitude of the latency sawtooth wave.
--saw-period=0 Period of the latency sawtooth wave.
--square-amplitude=0 Amplitude of the latency square wave.
--square-period=0 Period of the latency square wave.
--version Show application version.
Args:
<destination> TCP proxy destination in host:post format.
Expand All @@ -78,4 +82,4 @@ Speedbump can be used as a Go library via its `lib` package. Check `lib` [README

Copyright Paweł Kuffel 2022, licensed under Apache 2.0 License.

Speedbump logo contains the Go Gopher mascot which was originally designed by Renee French (http://reneefrench.blogspot.com/) and licensed under Creative Commons 3.0 Attributions license.
Speedbump logo contains the Go Gopher mascot which was originally designed by Renee French (http://reneefrench.blogspot.com/) and licensed under Creative Commons 3.0 Attributions license.
18 changes: 13 additions & 5 deletions args.go
Expand Up @@ -34,6 +34,12 @@ func parseArgs(args []string) (*lib.SpeedbumpCfg, error) {
sawPeriod = app.Flag("saw-period", "Period of the latency sawtooth wave.").
PlaceHolder("0").
Duration()
squareAmplitude = app.Flag("square-amplitude", "Amplitude of the latency square wave.").
PlaceHolder("0").
Duration()
suqarePeriod = app.Flag("square-period", "Period of the latency square wave.").
PlaceHolder("0").
Duration()
destAddr = app.Arg("destination", "TCP proxy destination in host:post format.").
Required().
String()
Expand All @@ -52,11 +58,13 @@ func parseArgs(args []string) (*lib.SpeedbumpCfg, error) {
BufferSize: int(*bufferSize),
QueueSize: *queueSize,
Latency: &lib.LatencyCfg{
Base: *latency,
SineAmplitude: *sineAmplitude,
SinePeriod: *sinePeriod,
SawAmplitute: *sawAmplitute,
SawPeriod: *sawPeriod,
Base: *latency,
SineAmplitude: *sineAmplitude,
SinePeriod: *sinePeriod,
SawAmplitute: *sawAmplitute,
SawPeriod: *sawPeriod,
SquareAmplitude: *squareAmplitude,
SquarePeriod: *suqarePeriod,
},
LogLevel: *logLevel,
}
Expand Down
4 changes: 4 additions & 0 deletions args_test.go
Expand Up @@ -30,6 +30,8 @@ func TestParseArgsAll(t *testing.T) {
"--latency=100ms",
"--sine-amplitude=50ms",
"--sine-period=1m",
"--square-amplitude=123ms",
"--square-period=3m",
"host:777",
},
)
Expand All @@ -42,4 +44,6 @@ func TestParseArgsAll(t *testing.T) {
assert.Equal(t, time.Minute, cfg.Latency.SinePeriod)
assert.Equal(t, time.Duration(0), cfg.Latency.SawAmplitute)
assert.Equal(t, time.Duration(0), cfg.Latency.SawPeriod)
assert.Equal(t, time.Millisecond*123, cfg.Latency.SquareAmplitude)
assert.Equal(t, time.Minute*3, cfg.Latency.SquarePeriod)
}
18 changes: 13 additions & 5 deletions lib/latency_generator.go
Expand Up @@ -9,11 +9,13 @@ type LatencyGenerator interface {
}

type LatencyCfg struct {
Base time.Duration
SineAmplitude time.Duration
SinePeriod time.Duration
SawAmplitute time.Duration
SawPeriod time.Duration
Base time.Duration
SineAmplitude time.Duration
SinePeriod time.Duration
SawAmplitute time.Duration
SawPeriod time.Duration
SquareAmplitude time.Duration
SquarePeriod time.Duration
}

type latencySummand interface {
Expand All @@ -39,6 +41,12 @@ func newSimpleLatencyGenerator(start time.Time, cfg *LatencyCfg) simpleLatencyGe
cfg.SawPeriod,
})
}
if cfg.SquareAmplitude > 0 && cfg.SquarePeriod > 0 {
summands = append(summands, squareLatencySummand{
cfg.SquareAmplitude,
cfg.SquarePeriod,
})
}
return simpleLatencyGenerator{
start: start,
summands: summands,
Expand Down
15 changes: 14 additions & 1 deletion lib/speedbump_test.go
Expand Up @@ -140,7 +140,20 @@ func TestSpeedbumpWithEchoServer(t *testing.T) {
assert.Nil(t, err)

tcpAddr, _ := net.ResolveTCPAddr("tcp", "localhost:8000")
conn, _ := net.DialTCP("tcp", nil, tcpAddr)

var conn *net.TCPConn

// Wait for the speedbump instance to start listening
// since it is started in a separate goroutine, we don't know
// if it has already started listening by this point
for {
conn, err = net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
time.Sleep(10 * time.Millisecond)
} else {
break
}
}

firstOpStart := time.Now()

Expand Down
15 changes: 15 additions & 0 deletions lib/square.go
@@ -0,0 +1,15 @@
package lib

import "time"


type squareLatencySummand struct {
amplitude time.Duration
period time.Duration
}

func (s squareLatencySummand) getLatency(elapsed time.Duration) time.Duration {
return time.Duration(
(4 * (elapsed / s.period) - 2 * ((2 * elapsed) / s.period) + 1) * s.amplitude,
)
}
23 changes: 23 additions & 0 deletions lib/square_test.go
@@ -0,0 +1,23 @@
package lib

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestSquareLatencySummand(t *testing.T) {
s := squareLatencySummand{
amplitude: time.Second * 2,
period: time.Minute,
}

assert.Equal(t, s.getLatency(time.Duration(0)), time.Second * 2)
assert.Equal(t, s.getLatency(time.Second * 15), time.Second * 2)
assert.Equal(t, s.getLatency(time.Second * 30), time.Second * -2)
assert.Equal(t, s.getLatency(time.Second * 45), time.Second * -2)
assert.Equal(t, s.getLatency(time.Second * 60), time.Second * 2)
assert.Equal(t, s.getLatency(time.Second * 84), time.Second * 2)
assert.Equal(t, s.getLatency(time.Second * 90), time.Second * -2)
}

0 comments on commit a556c99

Please sign in to comment.