aboutsummaryrefslogtreecommitdiff
path: root/ride
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-29 07:24:55 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-29 07:24:55 +0200
commit29a56e6fcc92388c218185682a554bc54f6c98bf (patch)
tree311e62510a589de2b1114f590d21ee4e02743948 /ride
parent827b55f610119d553b965ceb4d3631f0481bbb3b (diff)
Add GPX vs FIT parsing benchmark over full ride corpus
Diffstat (limited to 'ride')
-rw-r--r--ride/parse_bench_test.go239
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)
+ }
+ }
+ }
+}