aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 14:17:54 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 14:17:54 +0200
commitb66850cc8c584cc00bcd796eb3aa238bcf87394a (patch)
tree3f922d30071e2187e0c2f123e00f57e250792da6 /internal
parentf74a02254ac15a84d38c9a254ead293ebaad2377 (diff)
downloadkrino-b66850cc8c584cc00bcd796eb3aa238bcf87394a.tar.gz
krino-b66850cc8c584cc00bcd796eb3aa238bcf87394a.zip
a file's name is folded once, not once per name test
Folding is the expensive half of a name test on a name with diacritics, and every name test of every rule folded the same name again: on Polish names it was most of the matching work. The per-file facts memoise it, which is where one file's work belongs - the object is per file and per goroutine, so no lock. 4000 Polish names, twelve rules with name tests: 0.33s -> 0.13s
Diffstat (limited to 'internal')
-rw-r--r--internal/cond/eval.go8
-rw-r--r--internal/cond/eval_test.go3
-rw-r--r--internal/engine/engine_test.go2
-rw-r--r--internal/engine/facts.go19
4 files changed, 31 insertions, 1 deletions
diff --git a/internal/cond/eval.go b/internal/cond/eval.go
index 2ae31fb..7d8e00d 100644
--- a/internal/cond/eval.go
+++ b/internal/cond/eval.go
@@ -28,6 +28,12 @@ type Facts interface {
// an earlier rule could not be decided (its condition was unknown): with
// no match and an undecided rule, (matched) is unknown.
Matched() (matched, undecided bool)
+ // Folded is norm.FoldMapped(subj), memoised per file. Folding is the
+ // expensive half of a name test on a name with diacritics, and the
+ // same name is folded again by every name test of every rule; the
+ // implementation holds one file at a time, so a small memo there
+ // removes the repetition entirely.
+ Folded(subj string) norm.Folded
}
// Result is the outcome of evaluating a Cond against one file's Facts.
@@ -243,7 +249,7 @@ func (c *Cond) evalLeaf(n *node, f Facts) (ok bool, reason, warn string, caps []
// back from the original, so {1} keeps the name's diacritics.
folded := norm.Folded{Text: subj}
if c.opt.Fold {
- folded = norm.FoldMapped(subj)
+ folded = f.Folded(subj)
}
for _, p := range n.patterns {
loc := p.re.FindStringSubmatchIndex(folded.Text)
diff --git a/internal/cond/eval_test.go b/internal/cond/eval_test.go
index 5074557..7f06afd 100644
--- a/internal/cond/eval_test.go
+++ b/internal/cond/eval_test.go
@@ -39,6 +39,9 @@ func (f *fake) Size() int64 { return f.size }
func (f *fake) ModTime() time.Time { return now.Add(-f.age) }
func (f *fake) Now() time.Time { return now }
func (f *fake) Matched() (bool, bool) { return f.matched, f.matchedUnknown }
+
+// Folded has no memo here: a fake stands in for one file in one test.
+func (f *fake) Folded(subj string) norm.Folded { return norm.FoldMapped(subj) }
func (f *fake) ContentContains(opt Options, keywords []string) (int, error) {
f.contentCalls++
if f.rawErr != nil {
diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go
index 89ea6f0..85679dc 100644
--- a/internal/engine/engine_test.go
+++ b/internal/engine/engine_test.go
@@ -12,6 +12,7 @@ import (
"git.labunix.xyz/krino/internal/cond"
"git.labunix.xyz/krino/internal/config"
+ "git.labunix.xyz/krino/internal/norm"
)
// sandbox gives a test its own HOME with no XDG overrides and returns it.
@@ -55,6 +56,7 @@ func (f fakeFacts) Now() time.Time { return
func (f fakeFacts) ContentContains(cond.Options, []string) (int, error) { return -1, nil }
func (f fakeFacts) Duplicate([]string) (string, bool, error) { return "", false, nil }
func (f fakeFacts) Matched() (bool, bool) { return false, false }
+func (f fakeFacts) Folded(subj string) norm.Folded { return norm.FoldMapped(subj) }
var _ cond.Facts = fakeFacts{}
diff --git a/internal/engine/facts.go b/internal/engine/facts.go
index 6540fdd..d20a34d 100644
--- a/internal/engine/facts.go
+++ b/internal/engine/facts.go
@@ -179,6 +179,10 @@ type facts struct {
// absolute, for a front end that offers to act on the other copy.
dupOriginal string
+ // folded memoises the folded form of this file's name and path, which
+ // every name test of every rule would otherwise recompute.
+ folded map[string]norm.Folded
+
contentDone bool // extraction was attempted
contentErr error // why it failed
partialErr error // extract.ErrPartial: a keyword not found may be in the unread part
@@ -192,6 +196,21 @@ func newFacts(run *matchRun, file scan.File) *facts {
return &facts{run: run, file: file}
}
+// Folded is the file's name or path folded for comparison, worked out once
+// however many name tests ask for it. facts belongs to one file and one
+// goroutine, so no lock is needed.
+func (f *facts) Folded(subj string) norm.Folded {
+ if v, ok := f.folded[subj]; ok {
+ return v
+ }
+ v := norm.FoldMapped(subj)
+ if f.folded == nil {
+ f.folded = make(map[string]norm.Folded, 2)
+ }
+ f.folded[subj] = v
+ return v
+}
+
func (f *facts) Name() string { return f.file.Name }
func (f *facts) Rel() string { return f.file.Rel }
func (f *facts) Size() int64 { return f.file.Size }