feat(output): Implement incremental save functionality for packs (fixes #512) - #513
feat(output): Implement incremental save functionality for packs (fixes #512)#513Stoupy51 wants to merge 8 commits into
Conversation
|
Damn, didn't thought copilot review would start automatically on another repo beside mines |
There was a problem hiding this comment.
Pull request overview
This PR adds an incremental output mode to the output contrib plugin to avoid wiping and rewriting entire pack directories on every build, addressing performance issues for large projects (issue #512).
Changes:
- Add
incremental: booltoOutputOptions(defaultFalse). - Implement
incremental_save()to delete removed files, prune empty directories, and skip writing unchanged files. - Update
output()to use incremental saving for non-zipped packs when enabled.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
should meta.output.incremental option be true by default? |
Honestly, yes imo. it's an improvement. |
I was just on a call with airdox, and he says it's not worth it on linux but windows would be good. |
rx-dev
left a comment
There was a problem hiding this comment.
Some things were nitpicks, i realize the original code here used os than I realized though I really do think the pathlib apis are a bit cleaner and has better patterns which would cleanup more of the use-cases here.
There's a lot of functions written out here. Some is good organization, but some feels like over-doing it. Though, each one is described well so perhaps it would be good for future maintenance.
| from beet.core.utils import FileSystemPath, log_time_scope | ||
| from beet.library.base import Pack, PackFile | ||
|
|
||
| ManifestEntry = tuple[int, int, str] |
There was a problem hiding this comment.
| ManifestEntry = tuple[int, int, str] | |
| type ManifestEntry = tuple[int, int, str] |
| result: dict[str, tuple[int, int]] = {} | ||
|
|
||
| def walk(path: str, prefix: str) -> None: | ||
| with os.scandir(path) as entries: |
There was a problem hiding this comment.
We should generally use pathlib to walk the tree.
| for dirpath, _dirnames, _filenames in os.walk(directory, topdown=False): | ||
| if dirpath != str(directory): | ||
| try: | ||
| os.rmdir(dirpath) |
There was a problem hiding this comment.
same here, this code pattern is a bit cleaner with pathlib
|
|
||
|
|
||
| def content_signature(pack_file: PackFile) -> str: | ||
| """ Return a signature that changes whenever the bytes the pack file would write change. |
There was a problem hiding this comment.
the formatting of these docstrings should be consistent with the codebase (no leading spaces).
| Args: | ||
| pack_file (PackFile): The file to sign. | ||
| Returns: | ||
| str: Either `source:<size>:<mtime>` or `sha1:<digest>`. |
There was a problem hiding this comment.
I don't think we use this expanded style for docstrings in the beet repo.
| encoding: str = getattr(pack_file, "encoding", None) or "utf-8" | ||
| return disk_path.read_text(encoding=encoding) != serialized | ||
| return disk_path.read_bytes() != serialized | ||
| except Exception: |
There was a problem hiding this comment.
This captures way to many states here, what specific exceptions are you trying to guard here?
i would also use with contextlib.suppress pattern for this and specify the specific Exception types that u want to guard
| if len(value) == 3 | ||
| } | ||
| except (OSError, ValueError, TypeError, KeyError, IndexError): | ||
| return {} |
There was a problem hiding this comment.
same here, not a big fan of how this is laid out tbh. this is a big hard to read, i would refactor this even if it's a bit more verbose.
| if incremental is None: | ||
| meta_opts = ctx.meta.get("output") | ||
| if isinstance(meta_opts, dict): | ||
| incremental = bool(meta_opts.get("incremental", False)) |
There was a problem hiding this comment.
hmmm, i don't think u should be reading ctx.meta here, the way that @configurable works in the first place should auto-fill the opts with stuff from meta.
| target: Path = Path(path) / pack.name | ||
| incremental_save(pack, target, cache.get_path(f"manifest:{target}")) |
There was a problem hiding this comment.
| target: Path = Path(path) / pack.name | |
| incremental_save(pack, target, cache.get_path(f"manifest:{target}")) | |
| incremental_save(path / pack.name, target, cache.get_path(f"manifest:{target}")) |
path should already be a Path
| incremental = bool(meta_opts.get("incremental", False)) | ||
|
|
||
| paths: list[Path] = [ctx.directory / path for path in opts.directory.entries()] | ||
| packs: list[Pack] = list(filter(None, ctx.packs)) |
There was a problem hiding this comment.
| packs: list[Pack] = list(filter(None, ctx.packs)) | |
| packs: list[Pack] = [pack for pack in ctx.packs if pack is not None] |
matches the symmetry of the above line a bit better imo. (nitpick)

Fixes #512
A new incremental option (default to false) can be given to the output plugin.
either OutputOptions or in beet.yml:
When enabled:
File watchers will now be less in pain each build.
On my tests, I get faster build time, idk if you want to make it True by default so I put at False.
Bonus: Optimized the list_files() function that was building a Path to compute relative time for each file:
8f4d701