Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,11 @@ Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally exp
Action:
In `watchdog` event path normalization, bypass the computationally expensive `os.path.relpath` for the common case where `base_path` is `.` and the path is already relative by adding a fast-path condition: `elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): pass`.
To optimize ignore pattern matching in hot loops, pre-compute a flag during initialization (e.g., `self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)`) and use it to short-circuit the evaluation of compound directory paths if no slash-based ignore patterns exist.

## 2026-05-01 — Wildcard Regex Split Optimization

Learning:
Inside the file watcher's `_is_ignored_impl` hot path, applying a combined wildcard regex that includes both simple patterns (e.g. `*.tmp`) and compound patterns (e.g. `src/*.tmp`) to individual path segments (`parts`) and cumulative directory prefixes (`prefix`) is redundant and computationally wasteful. A simple wildcard pattern incorrectly evaluated against a cumulative prefix path loop wastes time, and a compound wildcard will never match a simple directory segment.

Action:
Split wildcard patterns into `simple_wildcards` (no slashes) and `compound_wildcards` (contains slashes), and compile them into separate regular expressions (`simple_wildcard_regex` and `compound_wildcard_regex`). Only apply the simple regex when iterating over individual parts, and apply the compound regex when accumulating the directory prefix. This optimization prevents unnecessary regex checks in the hot path.
21 changes: 14 additions & 7 deletions src/echo/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@ def __init__(self, command: str, base_path: str = ".", ignore_patterns: list[str
# Pre-compute exact vs wildcard patterns for faster matching
self.exact_ignores = {p for p in self.ignore_patterns if not any(c in p for c in ('*', '?', '['))}
wildcard_ignores = [p for p in self.ignore_patterns if any(c in p for c in ('*', '?', '['))]
self.wildcard_regex = None

simple_wildcards = [p for p in wildcard_ignores if '/' not in p]
compound_wildcards = [p for p in wildcard_ignores if '/' in p]

self.simple_wildcard_regex = None
self.compound_wildcard_regex = None
self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)
if wildcard_ignores:
regex_str = "|".join(f"(?:{fnmatch.translate(p)})" for p in wildcard_ignores)
self.wildcard_regex = re.compile(regex_str)

if simple_wildcards:
self.simple_wildcard_regex = re.compile("|".join(f"(?:{fnmatch.translate(p)})" for p in simple_wildcards))
if compound_wildcards:
self.compound_wildcard_regex = re.compile("|".join(f"(?:{fnmatch.translate(p)})" for p in compound_wildcards))

self.current_process = None
self.process_lock = threading.Lock()
Expand Down Expand Up @@ -188,9 +195,9 @@ def _is_ignored_impl(self, path: str) -> bool:
if not self.exact_ignores.isdisjoint(parts):
return True

if self.wildcard_regex:
if self.simple_wildcard_regex:
for part in parts:
if self.wildcard_regex.match(part):
if self.simple_wildcard_regex.match(part):
return True

# Check for exact and wildcard ignore patterns matching cumulative prefix directories
Expand All @@ -203,7 +210,7 @@ def _is_ignored_impl(self, path: str) -> bool:
prefix = f"{prefix}/{part}"
if prefix in self.exact_ignores:
return True
if self.wildcard_regex and self.wildcard_regex.match(prefix):
if self.compound_wildcard_regex and self.compound_wildcard_regex.match(prefix):
return True

return False
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_character_class_wildcard_match():
handler = CommandRunnerHandler("echo 1", ignore_patterns=["[a-z].tmp"])

# Must correctly categorize as wildcard and compile regex
assert handler.wildcard_regex is not None
assert handler.simple_wildcard_regex is not None
assert "[a-z].tmp" not in handler.exact_ignores

assert handler._is_ignored("a.tmp") is True
Expand Down
Loading