summaryrefslogtreecommitdiff
path: root/internal/engine/plan_bench_test.go
blob: 1b4f51c2095e643d36210b07a9d268f77b527537 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// SPDX-License-Identifier: GPL-3.0-or-later

package engine

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"git.labunix.xyz/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<N>.<ext>) -
// 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++
		}
	}
}