Evaluate path predicates incrementally from cached directory paths

This commit is contained in:
Andy Dixon
2026-10-02 08:40:10 +01:00
parent 0aa93a00d9
commit 11a0e401ee
4 changed files with 33 additions and 10 deletions
+3 -3
View File
@@ -95,15 +95,15 @@ once per rendered frame, so a resize storm cannot queue up work.
|---|---:| |---|---:|
| `size > 1MB` | 31 ms | | `size > 1MB` | 31 ms |
| `*.dat AND size > 500k` | 119 ms | | `*.dat AND size > 500k` | 119 ms |
| `path contains sub3` (reconstructs every path) | 244 ms | | `path contains sub3` | 112 ms |
Filters run on a background goroutine with cancellation and a 120 ms Filters run on a background goroutine with cancellation and a 120 ms
keystroke debounce; the UI never waits for them. keystroke debounce; the UI never waits for them.
## Known costs / future work ## Known costs / future work
* Path predicates rebuild each node's path; an incremental evaluation along * Path predicates now build each path from the cached parent directory path
the tree would make them ~10× cheaper. (244 ms → 112 ms); globs are dominated by `filepath.Match`.
* Node records could shrink (owner/group/mode/link count are only needed for * Node records could shrink (owner/group/mode/link count are only needed for
display and some filters) if memory on 10 M+ entry trees matters. display and some filters) if memory on 10 M+ entry trees matters.
* Cold-cache HDD benchmarks still need to be run with root access. * Cold-cache HDD benchmarks still need to be run with root access.
+1 -1
View File
@@ -43,7 +43,7 @@ Status markers: `[x]` done, `[~]` partial, `[ ]` not started.
- [ ] Windows allocated-size via GetCompressedFileSizeW (opt-in) - [ ] Windows allocated-size via GetCompressedFileSizeW (opt-in)
- [ ] configurable keybindings - [ ] configurable keybindings
- [ ] cold-cache HDD scan benchmarks (need root to drop caches) - [ ] cold-cache HDD scan benchmarks (need root to drop caches)
- [ ] incremental path-predicate evaluation for filters - [x] incremental path-predicate evaluation for filters
- [ ] permanent deletion (deliberately not offered yet; trash only) - [ ] permanent deletion (deliberately not offered yet; trash only)
## 1.0 ## 1.0
+8 -1
View File
@@ -43,13 +43,20 @@ func Apply(ctx context.Context, t *inventory.Tree, q *Query, m inventory.SizeMod
r := &Result{Query: q, Mode: m, Sizes: make([]int64, n), Matched: make([]bool, n)} r := &Result{Query: q, Mode: m, Sizes: make([]int64, n), Matched: make([]bool, n)}
covered := make([]bool, n) covered := make([]bool, n)
c := &Ctx{Tree: t, Now: time.Now().UnixNano()} c := &Ctx{Tree: t, Now: time.Now().UnixNano()}
if q.UsesPath {
c.dirPaths = map[inventory.NodeID]string{}
}
for i := range n { for i := range n {
if i&0xffff == 0 && ctx.Err() != nil { if i&0xffff == 0 && ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
} }
id := inventory.NodeID(i) id := inventory.NodeID(i)
nd := t.Node(id) nd := t.Node(id)
c.ID, c.Node = id, nd c.ID, c.Node, c.path = id, nd, ""
if c.dirPaths != nil && nd.IsDir() {
c.path = c.lowerPath()
c.dirPaths[id] = c.path
}
if i > 0 && q.Match(c) { if i > 0 && q.Match(c) {
r.Matched[i] = true r.Matched[i] = true
covered[i] = true covered[i] = true
+21 -5
View File
@@ -16,6 +16,9 @@ type Ctx struct {
Node *inventory.Node Node *inventory.Node
Now int64 // unix nanoseconds Now int64 // unix nanoseconds
path string path string
// dirPaths caches lower-cased directory paths during Apply, which visits
// parents before children, so a node's path is one concatenation.
dirPaths map[inventory.NodeID]string
} }
func (c *Ctx) str(f Field) string { func (c *Ctx) str(f Field) string {
@@ -25,7 +28,7 @@ func (c *Ctx) str(f Field) string {
return strings.ToLower(n.Name) return strings.ToLower(n.Name)
case FPath: case FPath:
if c.path == "" { if c.path == "" {
c.path = strings.ToLower(c.Tree.Path(c.ID)) c.path = c.lowerPath()
} }
return c.path return c.path
case FExt: case FExt:
@@ -62,6 +65,22 @@ func (c *Ctx) num(f Field) int64 {
return 0 return 0
} }
func (c *Ctx) lowerPath() string {
if c.dirPaths != nil {
if p, ok := c.dirPaths[c.ID]; ok {
return p
}
if pp, ok := c.dirPaths[c.Node.Parent]; ok {
sep := string(filepath.Separator)
if strings.HasSuffix(pp, sep) {
sep = ""
}
return pp + sep + strings.ToLower(c.Node.Name)
}
}
return strings.ToLower(c.Tree.Path(c.ID))
}
type node interface{ eval(*Ctx) bool } type node interface{ eval(*Ctx) bool }
type andNode struct{ l, r node } type andNode struct{ l, r node }
@@ -154,7 +173,4 @@ func (n numNode) eval(c *Ctx) bool {
} }
// Match evaluates the query against one node. // Match evaluates the query against one node.
func (q *Query) Match(c *Ctx) bool { func (q *Query) Match(c *Ctx) bool { return q.root.eval(c) }
c.path = ""
return q.root.eval(c)
}