- commit
- 9a0dfceafddabb841acd1a9be600e2f7aa136be7
- parent
- 34eef0930d55c840e2e495b444a9822e8c65abdd
- author
- Dami <dami.etoile@gmail.com>
- kind
- test risk score 0.1 of 3, Jev's estimate
| A | internal/ | 223 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 8 | + |
| A | internal/ | 59 | ++++++++ |
@@ -0,0 +1,223 @@
1+//go:build stress2+3+// Stress benchmarks of sources: a Parquet file and a SQLite table of ten4+// million rows, generated once into $STRESS_DIR/generated (or a5+// temporary directory), read in place: opening, summing a column as SUM6+// does, a page deep in the file, and building and paging a sorted and a7+// filtered view, each with the peak heap it took. Timed under the bench8+// lock (/tmp/012-bench.lock), as the speed gate is.9+package fileio10+11+import (12+ "context"13+ "database/sql"14+ "fmt"15+ "math/rand/v2"16+ "os"17+ "path/filepath"18+ "testing"19+ "time"20+21+ "github.com/parquet-go/parquet-go"22+23+ "github.com/FelineStateMachine/012/internal/sheet"24+)25+26+// bigRows is the rows of the generated sources.27+const bigRows = 10_000_00028+29+// bigRow is a row of them: an id, one of eight categories, an amount30+// and a date.31+type bigRow struct {32+ ID int64 `parquet:"id"`33+ Cat string `parquet:"cat,dict"`34+ Amount float64 `parquet:"amount"`35+ Day int32 `parquet:"day,date"`36+}37+38+var bigCats = []string{"north", "south", "east", "west", "central", "coast", "hills", "islands"}39+40+func bigRowAt(i int) bigRow {41+ r := rand.New(rand.NewPCG(uint64(i), 12))42+ return bigRow{ID: int64(i), Cat: bigCats[r.IntN(len(bigCats))], Amount: float64(r.IntN(1_000_000)) / 100,43+ Day: int32(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()/86400) + int32(r.IntN(2000))}44+}45+46+// generated is the directory the sources are kept in between runs.47+func generated(b *testing.B) string {48+ if d := os.Getenv("STRESS_DIR"); d != "" {49+ dir := filepath.Join(d, "generated")50+ if err := os.MkdirAll(dir, 0o755); err == nil {51+ return dir52+ }53+ }54+ return b.TempDir()55+}56+57+// bigParquet writes the Parquet source unless it's there.58+func bigParquet(b *testing.B, dir string) string {59+ name := filepath.Join(dir, fmt.Sprintf("sales-%d.parquet", bigRows))60+ if _, err := os.Stat(name); err == nil {61+ return name62+ }63+ tmp := name + ".part"64+ f, err := os.Create(tmp)65+ if err != nil {66+ b.Fatal(err)67+ }68+ w := parquet.NewGenericWriter[bigRow](f, parquet.MaxRowsPerRowGroup(1<<20))69+ batch := make([]bigRow, 1<<16)70+ for i := 0; i < bigRows; i += len(batch) {71+ n := min(len(batch), bigRows-i)72+ for j := range n {73+ batch[j] = bigRowAt(i + j)74+ }75+ if _, err := w.Write(batch[:n]); err != nil {76+ b.Fatal(err)77+ }78+ }79+ if err := w.Close(); err != nil {80+ b.Fatal(err)81+ }82+ f.Close()83+ if err := os.Rename(tmp, name); err != nil {84+ b.Fatal(err)85+ }86+ return name87+}88+89+// bigSQLite writes the SQLite source unless it's there.90+func bigSQLite(b *testing.B, dir string) string {91+ name := filepath.Join(dir, fmt.Sprintf("sales-%d.sqlite", bigRows))92+ if _, err := os.Stat(name); err == nil {93+ return name94+ }95+ tmp := name + ".part"96+ os.Remove(tmp)97+ db, err := sql.Open("sqlite", tmp+"?_pragma=journal_mode(OFF)&_pragma=synchronous(OFF)")98+ if err != nil {99+ b.Fatal(err)100+ }101+ defer db.Close()102+ tx, err := db.Begin()103+ if err != nil {104+ b.Fatal(err)105+ }106+ if _, err := tx.Exec("CREATE TABLE sales (id INTEGER, cat TEXT, amount REAL, day DATE)"); err != nil {107+ b.Fatal(err)108+ }109+ ins, err := tx.Prepare("INSERT INTO sales VALUES (?, ?, ?, ?)")110+ if err != nil {111+ b.Fatal(err)112+ }113+ for i := range bigRows {114+ r := bigRowAt(i)115+ day := time.Unix(int64(r.Day)*86400, 0).UTC().Format(time.DateOnly)116+ if _, err := ins.Exec(r.ID, r.Cat, r.Amount, day); err != nil {117+ b.Fatal(err)118+ }119+ }120+ if err := tx.Commit(); err != nil {121+ b.Fatal(err)122+ }123+ db.Close()124+ if err := os.Rename(tmp, name); err != nil {125+ b.Fatal(err)126+ }127+ return name128+}129+130+// BenchmarkSource reads ten million rows of each kind of source.131+func BenchmarkSource(b *testing.B) {132+ dir := generated(b)133+ files := map[string]SourceSpec{134+ "parquet": {Path: bigParquet(b, dir)},135+ "sqlite": {Path: bigSQLite(b, dir)},136+ }137+ defer benchLock(b)()138+ for _, kind := range []string{"parquet", "sqlite"} {139+ spec := files[kind]140+ spec.TempDir = b.TempDir()141+ b.Run(kind, func(b *testing.B) { benchSource(b, spec) })142+ }143+}144+145+// sortByAmount and northOnly are the views measured.146+var (147+ sortByAmount = SourceOrder{Sort: []SourceSort{{Col: 2, Desc: true}}}148+ northOnly = SourceOrder{Filter: []SourceFilter{{Col: 1, Cond: sheet.Condition{Op: sheet.CondExactly, Arg: "north"}}}}149+)150+151+func benchSource(b *testing.B, spec SourceSpec) {152+ ctx := context.Background()153+ b.Run("open", func(b *testing.B) {154+ measure(b, func() {155+ src, err := OpenSource(ctx, spec)156+ if err != nil {157+ b.Fatal(err)158+ }159+ if src.Rows() != bigRows {160+ b.Fatalf("%d rows", src.Rows())161+ }162+ src.Close()163+ })164+ })165+ src, err := OpenSource(ctx, spec)166+ if err != nil {167+ b.Fatal(err)168+ }169+ defer src.Close()170+ b.Run("sum-column", func(b *testing.B) {171+ measure(b, func() {172+ total := 0.0173+ err := src.Scan(ctx, 0, []int{2}, func(_ int64, v []sheet.LiveCell) bool {174+ total += v[0].V.Num175+ return true176+ })177+ if err != nil {178+ b.Fatal(err)179+ }180+ })181+ b.ReportMetric(float64(bigRows)*float64(b.N)/b.Elapsed().Seconds(), "rows/s")182+ })183+ for _, o := range []struct {184+ name string185+ order SourceOrder186+ }{{"plain", SourceOrder{}}, {"sorted", sortByAmount}, {"filtered", northOnly}} {187+ var v SourceView188+ b.Run("view-"+o.name, func(b *testing.B) {189+ measure(b, func() {190+ if v != nil {191+ v.Close()192+ }193+ if v, err = src.View(ctx, o.order); err != nil {194+ b.Fatal(err)195+ }196+ })197+ b.ReportMetric(float64(v.Rows()), "rows")198+ })199+ b.Run("page-"+o.name, func(b *testing.B) {200+ r := rand.New(rand.NewPCG(1, 2))201+ measure(b, func() {202+ from := v.Rows()/2 + r.Int64N(v.Rows()/2-60)203+ nums, _, err := v.Page(ctx, from, 60)204+ if err != nil || len(nums) != 60 {205+ b.Fatalf("page at %d: %d rows, %v", from, len(nums), err)206+ }207+ })208+ })209+ v.Close()210+ }211+}212+213+// measure runs fn in the benchmark's loop, reporting the peak heap it214+// took above what was live before.215+func measure(b *testing.B, fn func()) {216+ before := liveHeap()217+ stop := samplePeak()218+ for b.Loop() {219+ fn()220+ }221+ peak := stop()222+ b.ReportMetric(float64(peak-min(peak, before))/(1<<20), "peak-MB")223+}@@ -0,0 +1,59 @@
1+//go:build stress && unix2+3+package fileio4+5+import (6+ "errors"7+ "io/fs"8+ "os"9+ "syscall"10+ "testing"11+ "time"12+)13+14+// benchLockPath is the lock timed runs on this machine share, as the15+// speed gate takes it (internal/ui/speedlock_unix_test.go): a file16+// locked with flock(2), or a directory made and removed after where17+// there is no flock command (macOS).18+const benchLockPath = "/tmp/012-bench.lock"19+20+// benchLock waits up to ten minutes for the lock and returns its21+// release; past that, or when it can't be taken, it times without it.22+func benchLock(tb testing.TB) func() {23+ deadline := time.Now().Add(10 * time.Minute)24+ for {25+ err := os.Mkdir(benchLockPath, 0o755)26+ if err == nil {27+ return func() { os.Remove(benchLockPath) }28+ }29+ info, serr := os.Stat(benchLockPath)30+ switch {31+ case !errors.Is(err, fs.ErrExist) || serr != nil:32+ tb.Logf("timing without the lock: %v", err)33+ return func() {}34+ case info.Mode().IsRegular():35+ return flockBench(tb)36+ case time.Now().After(deadline):37+ tb.Logf("timing without the lock: %s held too long", benchLockPath)38+ return func() {}39+ }40+ time.Sleep(200 * time.Millisecond)41+ }42+}43+44+// flockBench takes the lock as a file, with flock(2).45+func flockBench(tb testing.TB) func() {46+ f, err := os.OpenFile(benchLockPath, os.O_RDWR, 0)47+ if err != nil {48+ tb.Logf("timing without the lock: %v", err)49+ return func() {}50+ }51+ if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {52+ f.Close()53+ tb.Fatalf("locking %s: %v", benchLockPath, err)54+ }55+ return func() {56+ syscall.Flock(int(f.Fd()), syscall.LOCK_UN)57+ f.Close()58+ }59+}