Skip to content

Commit

Permalink
Initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonyringoet committed May 13, 2023
1 parent 5547be5 commit b29790d
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work

# binaries
load_*
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
# load
A toy implementation of a load testing tool

> A toy implementation of a load testing tool.
Each worker creates it's own http client for all requests. This allows for all requests in that worker to reuse connections efficiently.

## Running

```bash
go run main.go -url http://localhost:8080 -workers 10 -duration 20s -verbose

go run main.go -help
```

```
-duration duration
Test duration (default 10s)
-url string
URL to load test (default "http://example.com")
-verbose
Verbose output
-workers int
Number of workers (default 1)
```

## Building

To build the `load` CLI tool, run the following command:

```sh
go build -o load main.go

# build for mac+win+linux in one go
./build.sh
```

This will generate an executable named `load_platform_arch` in the current directory.
13 changes: 13 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

# Build for Windows
echo "Building for Windows"
GOOS=windows GOARCH=amd64 go build -o load_windows_amd64 main.go

# Build for macOS
echo "Building for macOS"
GOOS=darwin GOARCH=amd64 go build -o load_darwin_amd64 main.go

# Build for Linux
echo "Building for Linux"
GOOS=linux GOARCH=amd64 go build -o load_linux_amd64 main.go
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/anthonyringoet/load

go 1.20
Empty file added go.sum
Empty file.
169 changes: 169 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package main

import (
"flag"
"fmt"
"log"
"net/http"
"sort"
"sync"
"time"
)

var (
times []time.Duration
successes int
failures int
running bool
wg sync.WaitGroup
)

func sendRequests(url string, verbose bool) {
defer wg.Done()

// Create an HTTP client
client := &http.Client{}

for running {
start := time.Now()

// Create a new request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatal(err)
}

// Set the User-Agent header
req.Header.Set("User-Agent", "github.com/anthonyringoet/load/0.0.1")

// Send the request using the same client
resp, err := client.Do(req)
if err != nil {
failures++
if verbose {
fmt.Printf("Failed to send request: %s\n", err)
}
continue
}

elapsed := time.Since(start)
times = append(times, elapsed)

resp.Body.Close()

if resp.StatusCode >= 200 && resp.StatusCode < 300 {
successes++
if verbose {
fmt.Printf("✅ success. statuscode=%d http_version=%s duration=%dms\n", resp.StatusCode, resp.Proto, elapsed.Milliseconds())
}
} else {
failures++
if verbose {
fmt.Printf("❌ fail. statuscode=%d http_version=%s duration=%dms\n", resp.StatusCode, resp.Proto, elapsed.Milliseconds())
}
}
}
}

// calculateStats takes a slice of time.Duration values representing latencies,
// sorts them, and then calculates and prints various statistics:
//
// - Min latency: The smallest latency in the slice.
// - Max latency: The largest latency in the slice.
// - Median latency: The middle value in the sorted slice. If the slice has an even
// number of values, this is the lower of the two middle values.
// - 90th percentile latency: The value below which 90% of the latencies fall.
// - 95th percentile latency: The value below which 95% of the latencies fall.
// - 99th percentile latency: The value below which 99% of the latencies fall.
// - Average latency: The sum of all latencies divided by the number of latencies.
//
// The function does not return a value; instead, it prints the calculated statistics
// to standard output.
//
// The input slice is sorted in-place, so the order of values in the slice will be
// changed by this function.
func calculateStats(times []time.Duration) {
if len(times) == 0 {
return
}

sort.Slice(times, func(i, j int) bool {
return times[i] < times[j]
})

min := times[0]
max := times[len(times)-1]

median := times[len(times)/2]

p90 := times[int(float64(len(times))*0.9)]
p95 := times[int(float64(len(times))*0.95)]
p99 := times[int(float64(len(times))*0.99)]

// calculate average
var total time.Duration
for _, t := range times {
total += t
}

average := total / time.Duration(len(times))

fmt.Printf("Min latency: %s\n", min)
fmt.Printf("Max latency: %s\n", max)
fmt.Printf("Median latency: %s\n", median)
fmt.Printf("90th percentile latency: %s\n", p90)
fmt.Printf("95th percentile latency: %s\n", p95)
fmt.Printf("99th percentile latency: %s\n", p99)
fmt.Printf("Average latency: %s\n", average)
}

func logProgress(duration *time.Duration) {
// simple progress updates
for i := 1; i <= int((*duration).Seconds()); i++ {
time.Sleep(1 * time.Second)
if running {
fmt.Printf("%d/%d seconds elapsed\n", i, int((*duration).Seconds()))
}
}
}

func main() {
url := flag.String("url", "http://example.com", "URL to load test")
workers := flag.Int("workers", 1, "Number of workers")
duration := flag.Duration("duration", 10*time.Second, "Test duration")
verbose := flag.Bool("verbose", false, "Verbose output")
flag.Parse()

if *duration < 0 {
fmt.Println("Duration cannot be negative. Defaulting to 10 seconds")
*duration = 10 * time.Second
}
if *workers < 0 {
fmt.Println("Workers cannot be negative. Defaulting to 1 worker")
*workers = 1
}

fmt.Printf("Load testing %s with %d workers for %s\n\n", *url, *workers, *duration)

running = true

if !*verbose {
go logProgress(duration)
}

for i := 0; i < *workers; i++ {
wg.Add(1)
go sendRequests(*url, *verbose)
}

time.Sleep(*duration)

running = false
wg.Wait()

fmt.Printf("\n2xx requests: %d, non 2xx requests: %d\n", successes, failures)
fmt.Printf("Total requests: %d\n", successes+failures)
fmt.Printf("Avg requests/worker: %d\n\n", (successes+failures)/(*workers))
calculateStats(times)
fmt.Println("\nLoad testing finished")
}

0 comments on commit b29790d

Please sign in to comment.