Skip to content

perf(photos): stream album enumeration in fixed-size chunks (bounds peak RSS on large libraries)#472

Open
epheterson wants to merge 2 commits into
mandarons:mainfrom
epheterson:perf/stream-album-chunked
Open

perf(photos): stream album enumeration in fixed-size chunks (bounds peak RSS on large libraries)#472
epheterson wants to merge 2 commits into
mandarons:mainfrom
epheterson:perf/stream-album-chunked

Conversation

@epheterson

Copy link
Copy Markdown
Contributor

Problem

sync_album_photos builds the full download-task list for an album before downloading any of it, so peak resident memory grows with len(album). On large libraries this OOM-kills the container: a ~111K-photo library peaks around 4 GB RSS and gets cgroup-OOM-killed at a 4 GB cap. The only mitigation today is over-provisioning mem_limit.

Change

Stream each album in fixed-size chunks: collect up to chunk_size download tasks → drain them through the existing execute_parallel_downloads → release the buffer → collect the next chunk. Memory is bounded by chunk_size, not len(album).

  • New optional photos.enumeration_chunk_size (default 1000; non-positive / garbage values fall back to the default with an INFO log rather than refusing to sync).
  • Semantically equivalent to the old build-all-then-download path — same total counts, same per-photo side effects (filename-collision handling, hardlink registry, the files set), same subalbum recursion — only the resident set stays flat instead of growing monotonically through enumeration.
  • Self-contained: does its own buffering over for photo in album, so it adds no new icloudpy version requirement. (Complements iter_chunks from icloudpy#140 but does not depend on it.)

Validation

  • 11 new tests in tests/test_streaming_enumeration.py. The headline test_buffer_never_exceeds_chunk_size asserts the bound actually holds — it fires immediately if the refactor ever regresses to monolithic enumeration — plus chunking-equivalence cases and the config-getter edge cases (default, explicit int, zero, negative, garbage string, None config). 100% coverage maintained.
  • Empirically: on a 111K-photo library, chunk_size=1000 sustains < 1 GB resident through a full enumeration (vs ~4 GB OOM before).

Supersedes #462 (rebased clean onto current main). Part of the RFC in #454.

epheterson and others added 2 commits June 3, 2026 12:39
Mandarons currently materializes the entire per-photo download-task
list in memory before passing any of it to execute_parallel_downloads.
For Eric's 111K-photo iCloud library this peaks at ~4 GB RSS
during enumeration (kernel-confirmed cgroup OOM at the 4 GB cap:
total-vm:4270872kB anon-rss:4181524kB), and forces operators to size
the container at 8 GB+ even though steady-state usage is <1 GB.

Fix: replace the build-list-then-download flow with a buffer-and-drain
pipeline. _collect_and_execute_album_in_chunks() accumulates up to
chunk_size DownloadTaskInfo entries, drains them via
execute_parallel_downloads, clears the buffer, then collects the next
chunk. Memory bounded by chunk_size, not len(album).

Concrete changes:
- album_sync_orchestrator.py: new _collect_and_execute_album_in_chunks
  with DEFAULT_ENUMERATION_CHUNK_SIZE=1000. Keeps the legacy
  _collect_album_download_tasks as a thin wrapper for tests that call
  it directly.
- config_parser.py: get_photos_enumeration_chunk_size() exposes the
  knob via photos.enumeration_chunk_size in config.yaml. Defensive
  about non-int / non-positive values (fall back to default rather
  than crash).
- tests/test_streaming_enumeration.py: 11 new tests.
  * 5 chunking equivalence + buffer-bound contract
    (test_buffer_never_exceeds_chunk_size is the headline — proves
    streaming is actually bounded, fires immediately if the refactor
    regresses to monolithic enumeration).
  * 6 config-getter cases (default, explicit int, 0, negative, garbage
    string, None config).

Empirical validation pending after deploy: on Eric's library, the
streaming fix should let mem_limit drop back from 8 GB to ~2 GB. The
icloud-docker-plus repo has the validation step in
docs/plans/2026-05-29-pr12-streaming-photo-enumeration.md.

Co-Authored-By: Claude <noreply@anthropic.com>
Upstream CI runs ruff. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant