diff --git a/.jules/bolt.md b/.jules/bolt.md index aae6d64..8aa68bc 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -132,3 +132,12 @@ Command-line file watchers and daemon tools usually listen for KeyboardInterrupt Action: Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler. + +## 2026-04-29 — Ignore Filter Relpath & Compound Loop Overhead + +Learning: +Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally expensive because it inherently resolves absolute paths. While optimizations existed for exact prefix matching, simple relative paths (e.g., `src/file.py`) against a `.` base path would fall through and trigger a `relpath` call, slowing down high-volume events. Additionally, reconstructing cumulative directory prefixes (`foo`, `foo/bar`) to test against exact/wildcard ignores consumes significant CPU cycles and is entirely unnecessary if the user specified no compound ignore patterns (i.e., no slashes in any pattern). + +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. diff --git a/.jules/warden.md b/.jules/warden.md index 091b3c5..9fff46b 100644 --- a/.jules/warden.md +++ b/.jules/warden.md @@ -168,3 +168,11 @@ Observed the preceding agent optimized process lifecycle management by adding a Alignment / Deferred: Version bumped to `0.1.22` as a patch release. Updated CHANGELOG.md. No heavy pruning or major dependency updates required. + +## 2026-04-30 — Assessment & Lifecycle + +Observation / Pruned: +Observed the preceding agent optimized the ignore file watcher hot paths by explicitly bypassing `os.path.relpath` for the common case, and short-circuiting compound directory evaluations when no slash-based ignore patterns exist. Verified test execution, linting, and dead code pruning without issues. No unused imports or variables were found. No heavy pruning required. + +Alignment / Deferred: +Version bumped to `0.1.23` as a patch release. Updated CHANGELOG.md. diff --git a/CHANGELOG.md b/CHANGELOG.md index a178d81..8fbc4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.1.23] - 2026-04-30 + +### Changed +* **[Performance]:** Optimized ignore file filtering in hot paths by fast-tracking common relative paths and avoiding compound loop iterations when unnecessary, significantly reducing CPU cycles on burst saves. + ## [0.1.22] - 2026-04-29 ### Changed diff --git a/pyproject.toml b/pyproject.toml index 0bb6ff2..0b75ece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "echo-watcher" -version = "0.1.22" +version = "0.1.23" description = "📡 Lightweight file watcher. Trigger commands on changes. <5MB RAM, single binary." authors = [ { name = "shenald-dev", email = "bot@shenald.dev" } diff --git a/src/echo/watcher.py b/src/echo/watcher.py index e77b885..44edb27 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -33,6 +33,7 @@ def __init__(self, command: str, base_path: str = ".", ignore_patterns: list[str 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 + 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) @@ -171,6 +172,8 @@ def _is_ignored_impl(self, path: str) -> bool: path = path[len(self._base_prefix):] elif path == self.base_path or path == self._abs_base_path.rstrip(os.sep): path = "." + elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): + pass else: try: path = os.path.relpath(path, self.base_path) @@ -191,7 +194,7 @@ def _is_ignored_impl(self, path: str) -> bool: return True # Check for exact and wildcard ignore patterns matching cumulative prefix directories - if len(parts) > 1: + if self._has_compound_ignores and len(parts) > 1: prefix = parts[0] # Prefix for parts[0] is already evaluated via earlier exact match `isdisjoint()` # and wildcard matching, so we start accumulating from the second part.