diff options
| -rw-r--r-- | ride/parse_bench_test.go | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/ride/parse_bench_test.go b/ride/parse_bench_test.go new file mode 100644 index 0000000..cceab3f --- /dev/null +++ b/ride/parse_bench_test.go @@ -0,0 +1,239 @@ +package ride_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/martinlehoux/biking_home/ride" +) + +func benchmarkDataDir(tb testing.TB) string { + tb.Helper() + candidates := []string{ + filepath.Join("..", "data", "rides"), + filepath.Join("data", "rides"), + filepath.Join(".", "data", "rides"), + } + for _, c := range candidates { + if stat, err := os.Stat(c); err == nil && stat.IsDir() { + abs, _ := filepath.Abs(c) + if abs != "" { + return abs + } + return c + } + } + tb.Fatalf("data/rides directory not found (tried %v)", candidates) + return "" +} + +func loadBenchDatas(tb testing.TB, pattern string) ([][]byte, int64) { + tb.Helper() + dir := benchmarkDataDir(tb) + glob := filepath.Join(dir, pattern) + files, err := filepath.Glob(glob) + if err != nil { + tb.Fatalf("glob %s: %v", glob, err) + } + if len(files) == 0 { + tb.Fatalf("no files matching %s", glob) + } + datas := make([][]byte, 0, len(files)) + var total int64 + for _, f := range files { + data, err := os.ReadFile(f) + if err != nil { + tb.Fatalf("read %s: %v", f, err) + } + if len(data) == 0 { + tb.Fatalf("empty file %s", f) + } + datas = append(datas, data) + total += int64(len(data)) + } + return datas, total +} + +func filterValidDatas(tb testing.TB, datas [][]byte, parser ride.RideParser) ([][]byte, int64, int) { + tb.Helper() + valid := make([][]byte, 0, len(datas)) + var total int64 + skipped := 0 + for _, data := range datas { + if _, err := parser.Parse(bytes.NewReader(data)); err != nil { + skipped++ + continue + } + valid = append(valid, data) + total += int64(len(data)) + } + return valid, total, skipped +} + +func BenchmarkGPXParse(b *testing.B) { + datas, _ := loadBenchDatas(b, "*.gpx") + if len(datas) == 0 { + b.Skip("no gpx files") + } + parser := ride.GPXRideParser{} + datas, totalBytes, skipped := filterValidDatas(b, datas, parser) + if len(datas) == 0 { + b.Skip("no valid gpx files") + } + if skipped > 0 { + b.Logf("GPX corpus: %d valid, %d skipped (indoor/no track points), total %d bytes (avg %.0f bytes)", len(datas), skipped, totalBytes, float64(totalBytes)/float64(len(datas))) + } else { + b.Logf("GPX corpus: %d files, total %d bytes (avg %.0f bytes)", len(datas), totalBytes, float64(totalBytes)/float64(len(datas))) + } + avgBytes := totalBytes / int64(len(datas)) + if avgBytes > 0 { + b.SetBytes(avgBytes) + } + b.ReportAllocs() + b.ResetTimer() + idx := 0 + for b.Loop() { + data := datas[idx%len(datas)] + idx++ + if _, err := parser.Parse(bytes.NewReader(data)); err != nil { + b.Fatalf("GPX parse failed: %v", err) + } + } +} + +func BenchmarkFITParse(b *testing.B) { + datas, totalBytes := loadBenchDatas(b, "*.fit") + if len(datas) == 0 { + b.Skip("no fit files") + } + b.Logf("FIT corpus: %d files, total %d bytes (avg %.0f bytes)", len(datas), totalBytes, float64(totalBytes)/float64(len(datas))) + avgBytes := totalBytes / int64(len(datas)) + if avgBytes > 0 { + b.SetBytes(avgBytes) + } + b.ReportAllocs() + b.ResetTimer() + parser := ride.FITRideParser{} + idx := 0 + for b.Loop() { + data := datas[idx%len(datas)] + idx++ + if _, err := parser.Parse(bytes.NewReader(data)); err != nil { + b.Fatalf("FIT parse failed: %v", err) + } + } +} + +func BenchmarkGPXParseAndAnalyze(b *testing.B) { + datas, _ := loadBenchDatas(b, "*.gpx") + if len(datas) == 0 { + b.Skip("no gpx files") + } + parser := ride.GPXRideParser{} + datas, totalBytes, skipped := filterValidDatas(b, datas, parser) + if len(datas) == 0 { + b.Skip("no valid gpx files") + } + if skipped > 0 { + b.Logf("GPX corpus: %d valid, %d skipped, total %d bytes (avg %.0f bytes)", len(datas), skipped, totalBytes, float64(totalBytes)/float64(len(datas))) + } else { + b.Logf("GPX corpus: %d files, total %d bytes (avg %.0f bytes)", len(datas), totalBytes, float64(totalBytes)/float64(len(datas))) + } + avgBytes := totalBytes / int64(len(datas)) + if avgBytes > 0 { + b.SetBytes(avgBytes) + } + b.ReportAllocs() + b.ResetTimer() + idx := 0 + for b.Loop() { + data := datas[idx%len(datas)] + idx++ + r, err := parser.Parse(bytes.NewReader(data)) + if err != nil { + b.Fatalf("GPX parse failed: %v", err) + } + _ = ride.MovingTime(r) + _ = r.Cotacol() + } +} + +func BenchmarkFITParseAndAnalyze(b *testing.B) { + datas, totalBytes := loadBenchDatas(b, "*.fit") + if len(datas) == 0 { + b.Skip("no fit files") + } + b.Logf("FIT corpus: %d files, total %d bytes (avg %.0f bytes)", len(datas), totalBytes, float64(totalBytes)/float64(len(datas))) + avgBytes := totalBytes / int64(len(datas)) + if avgBytes > 0 { + b.SetBytes(avgBytes) + } + b.ReportAllocs() + b.ResetTimer() + parser := ride.FITRideParser{} + idx := 0 + for b.Loop() { + data := datas[idx%len(datas)] + idx++ + r, err := parser.Parse(bytes.NewReader(data)) + if err != nil { + b.Fatalf("FIT parse failed: %v", err) + } + _ = ride.MovingTime(r) + _ = r.Cotacol() + } +} + +// BenchmarkGPXParseAll and BenchmarkFITParseAll measure throughput when parsing +// the entire corpus as a single operation, which matches batch import. +func BenchmarkGPXParseAll(b *testing.B) { + datas, _ := loadBenchDatas(b, "*.gpx") + if len(datas) == 0 { + b.Skip("no gpx files") + } + parser := ride.GPXRideParser{} + datas, totalBytes, skipped := filterValidDatas(b, datas, parser) + if len(datas) == 0 { + b.Skip("no valid gpx files") + } + if skipped > 0 { + b.Logf("GPX corpus: %d valid, %d skipped, total %d bytes", len(datas), skipped, totalBytes) + } else { + b.Logf("GPX corpus: %d files, total %d bytes", len(datas), totalBytes) + } + if totalBytes > 0 { + b.SetBytes(totalBytes) + } + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + for _, data := range datas { + if _, err := parser.Parse(bytes.NewReader(data)); err != nil { + b.Fatalf("GPX parse failed: %v", err) + } + } + } +} + +func BenchmarkFITParseAll(b *testing.B) { + datas, totalBytes := loadBenchDatas(b, "*.fit") + if len(datas) == 0 { + b.Skip("no fit files") + } + b.Logf("FIT corpus: %d files, total %d bytes", len(datas), totalBytes) + if totalBytes > 0 { + b.SetBytes(totalBytes) + } + b.ReportAllocs() + b.ResetTimer() + parser := ride.FITRideParser{} + for b.Loop() { + for _, data := range datas { + if _, err := parser.Parse(bytes.NewReader(data)); err != nil { + b.Fatalf("FIT parse failed: %v", err) + } + } + } +} |
