commit 81b25d1421a9549012b50632056863ba6adcd842 Author: sneak Date: Sat May 18 18:37:05 2024 -0700 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24dc26c --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work +go.work.sum + +# Dependency directories +vendor/ + +# Go build artifacts +bin/ +build/ +dist/ + +# Configuration files for editors and tools +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +*.log + +# Temporary files +*.tmp +*.temp +*.bak + +# Test binary, coverage, and results +*.test +*.cover +*.cov +*.profile +*.prof + +# OS-specific files +.DS_Store +Thumbs.db + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2843a6d --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of +this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4825ebc --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +default: test + +test: + go test -v -count=1 ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b39b3b --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# TimingBench + +`TimingBench` is a Go module for benchmarking the execution time of a +function. It runs the function a specified number of times and returns the +minimum, maximum, mean, and median execution times, along with other +relevant statistics. It also supports context for cancellation. + +## Features + +- Measure min, max, mean, and median execution times of a function. +- Support for context-based cancellation. +- Provides detailed timing statistics. + +## Installation + +To install the `timingbench` package, use the following command: + +```sh +go get git.eeqj.de/sneak/timingbench +``` + +## Usage + +Here's a simple example of how to use the `timingbench` module: + +```go +package main + +import ( + "context" + "fmt" + "math/rand" + "time" + "git.eeqj.de/sneak/timingbench" +) + +// Example function to benchmark +func sampleFunction() error { + time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) + return nil +} + +func main() { + // Seed the random number generator. + rand.Seed(time.Now().UnixNano()) + + // Define the number of iterations. + iterations := 10 + + // Create a context with a timeout for cancellation. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Measure the execution times of the sample function. + result, err := timingbench.TimeFunction(ctx, sampleFunction, iterations) + if err != nil { + fmt.Printf("Error measuring function: %v\n", err) + return + } + + // Print the timing results. + fmt.Println(result.String()) +} +``` + +## TimingResult + +The `TimingResult` struct holds the timing statistics for the function executions: + +```go +type TimingResult struct { + Min time.Duration // Minimum execution time + Max time.Duration // Maximum execution time + Mean time.Duration // Mean execution time + Median time.Duration // Median execution time + StartTime time.Time // Start time of the benchmarking + EndTime time.Time // End time of the benchmarking + Duration time.Duration // Total duration of the benchmarking + Iterations int // Number of iterations +} +``` + +## License + +This project is licensed under the WTFPL License. See the +[LICENSE](./LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please send patches via email. + +## Author + +`timingbench` was written by [sneak](mailto:sneak@sneak.berlin) ([website](https://sneak.berlin)). diff --git a/bench.go b/bench.go new file mode 100644 index 0000000..d57efbf --- /dev/null +++ b/bench.go @@ -0,0 +1,77 @@ +package timingbench + +import ( + "context" + "errors" + "fmt" + "sort" + "time" + + "gonum.org/v1/gonum/stat" +) + +// TimingResult holds the timing results for a function execution. +type TimingResult struct { + Min time.Duration + Max time.Duration + Mean time.Duration + Median time.Duration + StartTime time.Time + EndTime time.Time + Duration time.Duration + Iterations int +} + +// String returns a formatted string representation of the TimingResult. +func (r TimingResult) String() string { + return fmt.Sprintf( + "Start Time: %v, End Time: %v, Duration: %v, Iterations: %d, Min: %v, Max: %v, Mean: %v, Median: %v", + r.StartTime, r.EndTime, r.Duration, r.Iterations, r.Min, r.Max, r.Mean, r.Median, + ) +} + +// TimeFunction runs the given function fn a specified number of times and returns the timing results. +// It supports context for cancellation. +func TimeFunction(ctx context.Context, fn func() error, iterations int) (TimingResult, error) { + if iterations <= 0 { + return TimingResult{}, errors.New("iterations must be greater than 0") + } + + times := make([]float64, 0, iterations) + startTime := time.Now().UTC() + + for i := 0; i < iterations; i++ { + select { + case <-ctx.Done(): + return TimingResult{}, ctx.Err() + default: + iterStart := time.Now().UTC() + if err := fn(); err != nil { + return TimingResult{}, err + } + elapsed := time.Since(iterStart) + times = append(times, float64(elapsed)) + } + } + + endTime := time.Now().UTC() + totalDuration := endTime.Sub(startTime) + + sort.Float64s(times) + + min := time.Duration(times[0]) + max := time.Duration(times[len(times)-1]) + mean := time.Duration(stat.Mean(times, nil)) + median := time.Duration(stat.Quantile(0.5, stat.Empirical, times, nil)) + + return TimingResult{ + Min: min, + Max: max, + Mean: mean, + Median: median, + StartTime: startTime, + EndTime: endTime, + Duration: totalDuration, + Iterations: iterations, + }, nil +} diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..3454086 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,129 @@ +package timingbench + +import ( + "context" + "math/rand" + "testing" + "time" +) + +// isPrime checks if a number is a prime number. +func isPrime(n int) bool { + if n <= 1 { + return false + } + for i := 2; i*i <= n; i++ { + if n%i == 0 { + return false + } + } + return true +} + +// randomSleep introduces a random delay between 1 and 20 milliseconds. +func randomSleep() { + time.Sleep(time.Duration(rand.Intn(20)+1) * time.Millisecond) +} + +// generatePrimes generates prime numbers up to a given limit. +func generatePrimes(limit int) []int { + primes := []int{} + for i := 2; i <= limit; i++ { + if isPrime(i) { + primes = append(primes, i) + } + } + return primes +} + +// sortRandomSlice generates a slice with random integers and sorts it. +func sortRandomSlice(size int) { + slice := make([]int, size) + for i := range slice { + slice[i] = rand.Intn(10000) + } + quickSort(slice) +} + +// quickSort sorts a slice of integers using the quicksort algorithm. +func quickSort(arr []int) { + if len(arr) < 2 { + return + } + + left, right := 0, len(arr)-1 + + pivotIndex := rand.Int() % len(arr) + + arr[pivotIndex], arr[right] = arr[right], arr[pivotIndex] + + for i := range arr { + if arr[i] < arr[right] { + + arr[i], arr[left] = arr[left], arr[i] + left++ + } + } + + arr[left], arr[right] = arr[right], arr[left] + + quickSort(arr[:left]) + quickSort(arr[left+1:]) +} + +// multiplyMatrices performs matrix multiplication of two randomly generated matrices. +func multiplyMatrices(size int) { + matrixA := make([][]int, size) + matrixB := make([][]int, size) + result := make([][]int, size) + + for i := 0; i < size; i++ { + matrixA[i] = make([]int, size) + matrixB[i] = make([]int, size) + result[i] = make([]int, size) + for j := 0; j < size; j++ { + matrixA[i][j] = rand.Intn(100) + matrixB[i][j] = rand.Intn(100) + } + } + + for i := 0; i < size; i++ { + for j := 0; j < size; j++ { + sum := 0 + for k := 0; k < size; k++ { + sum += matrixA[i][k] * matrixB[k][j] + } + result[i][j] = sum + } + } +} + +// sampleFunction performs a mix of different operations to introduce variability in runtime. +func sampleFunction() error { + randomSleep() + generatePrimes(500 + rand.Intn(1500)) + sortRandomSlice(500 + rand.Intn(1500)) + multiplyMatrices(20 + rand.Intn(10)) + return nil +} + +func TestTimeFunction(t *testing.T) { + // Seed the random number generator. + rand.Seed(time.Now().UnixNano()) + + // Define the number of iterations. + iterations := 1000 + + // Create a context with a timeout for cancellation. + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + // Measure the execution times of the sample function. + result, err := TimeFunction(ctx, sampleFunction, iterations) + if err != nil { + t.Fatalf("Error measuring function: %v", err) + } + + // Print the timing results. + t.Logf(result.String()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..01ad99f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.eeqj.de/sneak/timingbench + +go 1.22.2 + +require gonum.org/v1/gonum v0.15.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2ea68bd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=