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) } } } }