diff --git a/.jules/bolt.md b/.jules/bolt.md index 63607c3..3c3f76a 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -60,3 +60,6 @@ ## 2026-06-25 - [Optimize Path.exists() when paired with stat()] **Learning:** Checking `Path.exists()` before `Path.stat()` introduces a redundant system call because `exists()` internally uses `stat()`. **Action:** Rely on catching the `OSError` from `Path.stat()` to simultaneously check for existence and retrieve file attributes, saving measurable I/O overhead on large filesystems. +## 2026-07-02 - [Avoid pathlib.Path.resolve() in performance critical paths] +**Learning:** `pathlib.Path.resolve()` is significantly slower than `os.path.realpath()` because it instantiates a new `Path` object and resolves symlinks explicitly. In tight loops or batch processing involving thousands of files, this overhead is noticeable. +**Action:** Use `os.path.realpath(path_str)` directly instead of `pathlib.Path(path_str).resolve()` when performance is a priority and you only need the resolved path string. Convert to a `Path` object only when necessary. diff --git a/media_shrinker.py b/media_shrinker.py index d72a8d5..13caeb7 100644 --- a/media_shrinker.py +++ b/media_shrinker.py @@ -193,7 +193,7 @@ def find_candidates( """ root = Path(root) - excluded = tuple(Path(item).resolve() for item in exclude_paths) + excluded = tuple(Path(os.path.realpath(item)) for item in exclude_paths) excluded_prefixes = tuple(prefix.casefold() for prefix in exclude_dir_prefixes) candidates: list[tuple[Path, int]] = [] @@ -759,7 +759,7 @@ def convert_file( resolved_sources = resolved_protected_sources if resolved_sources is None: - resolved_sources = frozenset(Path(item).resolve() for item in protected_sources) + resolved_sources = frozenset(Path(os.path.realpath(item)) for item in protected_sources) return [ _convert_segment( @@ -1261,7 +1261,7 @@ def _execute_conversions( workers = choose_worker_count(args.workers) ffmpeg_threads = args.ffmpeg_threads if args.ffmpeg_threads >= 0 else None - resolved_candidates = frozenset(Path(item[0]).resolve() for item in candidates) + resolved_candidates = frozenset(Path(os.path.realpath(item[0])) for item in candidates) protected_sources = [c[0] for c in candidates] def process_candidate(candidate_tuple: tuple[Path, int]) -> list[ConversionResult]: @@ -1417,7 +1417,7 @@ def _ensure_not_protected_source_path( protected_sources: frozenset[Path], output: Path ) -> None: """Raise MediaShrinkerError if output would overwrite a protected source.""" - resolved_output = output.resolve() + resolved_output = Path(os.path.realpath(output)) if resolved_output in protected_sources: raise MediaShrinkerError( f"Refusing to use protected source path as generated output: {output}" @@ -1613,7 +1613,7 @@ def _execute_plan( def _ensure_not_source_path(source: Path, output: Path) -> None: """Reject generated paths that would overwrite or delete the source file.""" - if source.resolve() == output.resolve(): + if os.path.realpath(source) == os.path.realpath(output): raise MediaShrinkerError( f"Refusing to use source path as generated output: {output}" )