Evaluate path predicates incrementally from cached directory paths
This commit is contained in:
+3
-3
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user