// SPDX-License-Identifier: GPL-3.0-or-later package engine import ( "context" "fmt" "os" "path/filepath" "testing" "time" "krino/internal/plan" ) // BenchmarkPlan measures krino's own cost of planning: walking a tree, // matching rules and building action chains (Engine.Plan, which wraps // Engine.Match and plan.Build). It does NOT measure krino's real-world // throughput - spec ยง13 is explicit that there is no performance target // for 0.0.1, because a full run's wall time is dominated by the external // extractors (pdftotext and friends), not by krino itself. This benchmark // therefore uses a config with no (content ...) test, so no extractor // ever runs and the result is identical on any machine, with or without // poppler installed. // // The tree is built once in b.TempDir(), before the timer starts; each // iteration re-plans the same on-disk tree with a fresh plan.Claims, so // iterations are independent and repeatable. func BenchmarkPlan(b *testing.B) { root := b.TempDir() scanRoot := filepath.Join(root, "Filed") buildBenchTree(b, scanRoot) mainFile := filepath.Join(root, "krino.conf") if err := os.WriteFile(mainFile, []byte(`(include "dl")`), 0o644); err != nil { b.Fatal(err) } dirsDir := filepath.Join(root, "dirs") if err := os.MkdirAll(dirsDir, 0o755); err != nil { b.Fatal(err) } // Type-only rules (no content test), one per group present in the // generated tree, mirroring examples/by-type.conf; "dat" files match // none of them and take the unmatched path through Match. dirConf := fmt.Sprintf(` (path %q) (recursive yes) (min-age 0s) (rule "images" (when (type image)) (move "Sorted/Images") (stop)) (rule "documents" (when (type document)) (move "Sorted/Documents") (stop)) (rule "spreadsheets" (when (type spreadsheet)) (move "Sorted/Spreadsheets") (stop)) (rule "archives" (when (type archive)) (move "Sorted/Archives") (stop)) (rule "media" (when (or (type audio) (type video))) (move "Sorted/Media") (stop)) `, scanRoot) if err := os.WriteFile(filepath.Join(dirsDir, "dl.conf"), []byte(dirConf), 0o644); err != nil { b.Fatal(err) } e, errs := Load(mainFile, "dl") if len(errs) > 0 { b.Fatalf("config errors: %v", errs) } ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := e.Plan(ctx, e.Dirs[0], plan.NewClaims()); err != nil { b.Fatal(err) } } } // benchTreeDirs * benchFilesPerDir files are generated, spread over // nested directories so the walk itself is exercised, not just a single // flat directory read. const ( benchTreeDirs = 12 benchFilesPerDir = 150 ) // buildBenchTree creates a synthetic tree under root for BenchmarkPlan: // nested "Sub" directories holding files that cycle through extensions // spanning several of Appendix A's type groups, plus one extension // ("dat") that matches no rule. Names are neutral (Sub, a.) - // never anything from a real folder, per the leak-check patterns. func buildBenchTree(b *testing.B, root string) { b.Helper() exts := []string{"pdf", "jpg", "xlsx", "zip", "mp3", "dat"} old := time.Now().Add(-time.Hour) n := 0 for d := 0; d < benchTreeDirs; d++ { dir := filepath.Join(root, fmt.Sprintf("Sub%d", d), "Nested") if err := os.MkdirAll(dir, 0o755); err != nil { b.Fatal(err) } for f := 0; f < benchFilesPerDir; f++ { ext := exts[n%len(exts)] p := filepath.Join(dir, fmt.Sprintf("a%04d.%s", n, ext)) if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { b.Fatal(err) } if err := os.Chtimes(p, old, old); err != nil { b.Fatal(err) } n++ } } }