Skip to content

DM-56097: Improve mremove/mexists performance - #157

Open
timj wants to merge 15 commits into
mainfrom
tickets/DM-56097
Open

timj wants to merge 15 commits into
mainfrom
tickets/DM-56097

Conversation

@timj

@timj timj commented Sep 14, 2026

Copy link
Copy Markdown
Member

On my Mac (so best case scenario with local disk) a simulated delete of a million files using 50k chunks (emulating butler usage) drops from 14 minutes per million to 2 minutes per million. Threads is still faster.

This is achieved mostly by using a cached process pool in subprocess mode and secondarily by passing multiple files to each worker in one call. Currently the cached worker pool hangs around forever -- we may want an API that butler can call to clean up once removeRuns or pruneDatasets has completed.

Checklist

  • ran Jenkins
  • added a release note for user-visible changes to doc/changes

timj and others added 7 commits September 14, 2026 14:55
Split the memoized lookups so that a caller can supply an upper bound, and
add a flag that a pool worker sets to report a single worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The worker count for a subprocess is now set by a pool initializer, so it no
longer depends on whether the parent had already calculated its own count and
behaves the same under the fork and spawn start methods.

Also stop dropping the caller's num_workers argument to mexists() when a
process pool is in use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Schemes backed by a connection pool keep the modest default; file URIs hold
no pool and so raise their bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each batch reports every URI independently, so one failure does not prevent
the removal of the URIs that follow it in the same batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Benchmarking local removal showed that extra threads add contention rather
than throughput, because unlink on a local filesystem is not latency bound.
The per-scheme bound remains available for a scheme that measures otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Under the spawn start method each worker pays a full interpreter startup, and
the cost grows with the number of modules the calling process has imported.
A pool is now kept alive and handed to later calls, and is discarded if it
breaks. Thread pools are still created per call, since they are cheap and
holding one open would keep its threads alive for no benefit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.28507% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.32%. Comparing base (8f9d513) to head (0ffadca).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
python/lsst/resources/_resourcePath.py 92.45% 3 Missing and 5 partials ⚠️
tests/test_utils.py 99.00% 1 Missing and 1 partial ⚠️
python/lsst/resources/s3.py 80.00% 0 Missing and 1 partial ⚠️
python/lsst/resources/utils.py 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #157      +/-   ##
==========================================
+ Coverage   83.46%   84.32%   +0.85%     
==========================================
  Files          38       39       +1     
  Lines        8112     8492     +380     
  Branches      975     1000      +25     
==========================================
+ Hits         6771     7161     +390     
+ Misses       1044     1034      -10     
  Partials      297      297              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Existence checking now uses the same batching as removal, and the chunking
helper is renamed to reflect that it serves both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timj timj changed the title DM-56097: Improve mremove performance DM-56097: Improve mremove/mexists performance Sep 14, 2026
Comment thread python/lsst/resources/_resourcePath.py Outdated


atexit.register(_clear_pool_executor_cache)
os.register_at_fork(after_in_child=_forget_pool_executor_cache)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep the cache we probably want to consider making _clear_pool_executor_cache public to give us the option of having Butler.pruneDatasets() clear the pool cache so that we don't have the subprocesses hanging around. On the other hand the timing data suggest that there is no reason to use process instead of threads so happy to defer this. The cache won't really be used by butler.

timj and others added 7 commits September 15, 2026 12:49
Replace the cached pool when the executor class or worker count changes.
Shut down the old pool so varying batch sizes do not accumulate workers.
Add regression coverage for replacement and shutdown.

Co-authored-by: Codex <noreply@openai.com>
Evict broken pools when bulk result handlers catch worker failures.
Preserve per-resource failure reports while allowing the next caller to
create a healthy pool. Cover existence, removal, transfer, and S3
removal.

Co-authored-by: Codex <noreply@openai.com>
Register the cache cleanup hook only when os.register_at_fork exists.
Test module loading without the hook and only exercise supported process
start methods in worker tests.

Co-authored-by: Codex <noreply@openai.com>
Three call sites reduced the worker count to the number of chunks a
particular batch produced. With a single cached process pool that makes
the pool's size a function of the batch size, so a small call replaces
the pool built for a large one and the next large call has to build it
again. Under the spawn start method each rebuild costs a full
interpreter startup per worker.

Asking for more workers than there is work for them costs nothing:
ProcessPoolExecutor starts workers on demand for every start method
except fork, and under fork a worker is cheap.

Generated with AI

Co-Authored-By: SLAC AI
Splitting a batch into one URI per chunk meant that checking three files
for existence occupied three workers, and under a process executor could
build a pool of subprocesses to do it. Handing the work over costs more
than the work itself at that size.

Chunks now have a per-scheme floor on their size, and a batch that
produces a single chunk is handled directly by the caller with no
executor involved. The floor is a scheme's property rather than a global
one because it expresses the cost of one operation: a missing-file check
on a local filesystem takes around 100 microseconds, so a batch has to
reach about a hundred URIs before spreading it wins, whereas a scheme
whose every operation is a network round trip is worth overlapping for
two. This generalizes the single-chunk shortcut that S3 bulk removal
already had.

Measured on wekafs with missing files, best of five, milliseconds:

               threads          process
     N    before  after    before  after
     1      0.37   0.08     20.38   0.07
     3      0.54   0.22     20.60   0.25
    30      3.14   2.12     23.75   2.28
   300      9.56  10.03     31.48  36.31
  1000     22.79  21.34     48.10  55.65
 10000    270.81 213.09    266.96 259.08

Generated with AI

Co-Authored-By: SLAC AI
A site that sets AWS_REQUEST_CHECKSUM_CALCULATION to WHEN_REQUIRED stops
boto3 computing a checksum on upload, so the object metadata carries no
CRC32 and test_get_info fails on a developer machine while passing in CI.
The fixture already removes the site's credentials and endpoint so that
tests cannot reach real infrastructure; these two variables belong with
them, because they change what the client does rather than where it
points.

Generated with AI

Co-Authored-By: SLAC AI
@timj
timj marked this pull request as ready for review September 15, 2026 21:50
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