control/crawler: examine sidecar files in parallel and in the background - #21850
control/crawler: examine sidecar files in parallel and in the background#21850hennikul wants to merge 3 commits into
Conversation
The startup crawl issues about six filesystem calls per image -- an existence check, a stat() of the xmp, and four probes for .txt/.wav sidecars -- serially across the whole library, and startup blocks until it has finished. The work is dominated by filesystem latency rather than by cpu time, so on a library held on a network share it dominates startup: for ~85k images on an NFS mount here it takes ~58s from cold and ~12s once caches are warm, spending over 95% of that waiting on i/o. Split dt_control_crawler_run() into three phases: collect the rows from the database, examine the filesystem from a pool of worker threads, then apply the results. The workers touch neither the database nor the gui, and each writes only its own array slot. The result list is assembled single threaded afterwards in collection order, so the output is identical regardless of the number of threads used. As the work is latency bound the pool deliberately oversubscribes; crawler_threads defaults to 16, and 1 restores the original serial path. On top of that, run the crawl from a background job one film roll at a time, ordered by film_rolls.access_timestamp so that the most recently opened rolls are examined first, and move a film roll to the head of the queue when it is opened. Startup no longer waits for the crawl at all. run_crawler_in_background defaults to true; setting it to false restores the blocking startup crawl. Because an xmp file can change while darktable is not running, the queue is deliberately not persisted across sessions -- every session still examines every image, it just no longer does so before the user can start working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The current crawler blocks until the check is done to make sure you don't load an "out of date" image. For example
Blocking prevents this scenario. I think you still need to block until the scan is complete, or at least complete for the film roll that is open. |
Reviewing the previous commit, wpferguson pointed out that the blocking crawl also protects against editing an image whose xmp was updated elsewhere, which crawling purely in the background would lose. That is right, and the consequence is worse than a stale thumbnail. darktable does not re-read an xmp when an image is opened, and dt_image_write_sidecar_file() overwrites whatever is on disk without comparing timestamps. Editing an image before its film roll had been examined would therefore discard the other machine's changes, and because the write also updates write_timestamp the crawler would no longer report the file afterwards -- so the loss would be silent. Wait for the film roll instead, rather than for the whole library. A film roll is examined on demand when it is opened, and its findings are reported straight away instead of with the rest at the end of the crawl, so the user is told before they can edit anything. Examining one film roll costs a few tens of milliseconds here (0.02s on average over 546 rolls, 0.12s for the slowest), against ~58s for the whole library. The collect module does not open film rolls through dt_film_open(), so the collection is followed too, and every film roll it covers is waited for rather than a sample of them, since the user can scroll to any image in it. Worst case -- a collection spanning the whole library, opened immediately -- that costs what the blocking crawl did, and far less in every other case. Only one film roll is examined at a time, by either the background job or a caller waiting for a specific roll, which also keeps the crawler's database transactions from interleaving with each other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@wpferguson you are right, and my answer above was wrong — sorry. I checked what the code Three things, all verifiable in current master:
So in your scenario the edit on computer 2 does not merely win — it overwrites computer 1's Nothing in the background crawl ever applied or refreshed anything — it only collected What I changedPushed as a second commit: wait for the film roll being opened, rather than for the whole A film roll is now examined on demand when it is opened, and its findings are reported The reason this is worth doing rather than just restoring the full block is the cost. From
So the guarantee you are asking for costs a few tens of milliseconds at the point of opening Only one film roll is examined at a time now, by either the background job or a caller TestingWith 2,692 conflicts seeded in the film roll that is open at startup, they are reported Disclosure: as before, written and tested with AI assistance (Claude Code with Claude Opus 5). |
|
The description sounds good, though I haven't had a chance to look at the code. It's similar to what I've had in mind to try for a while now to speed up the crawler, except I wasn't thinking of making it run in the background:
The second phase can be parallelized per directory, the third per image. A blocking crawl can still update the progress bar and estimated time as each directory completes in phase 2. Phase 3 would probably be fast enough not to bother with a progress bar. Keeping the same order in the user dialog is useful for checking correctness of the new version, but not actually necessary. |
… image Suggested by ralfbrown: rather than asking the filesystem about every image individually, list each directory once and answer the questions from that listing. The crawl used to make about six filesystem calls per image -- an existence check, a stat() of the xmp and four probes for .txt/.TXT and .wav/.WAV sidecars. Only one of those actually needs the filesystem once the directory contents are known: the stat() of an xmp that is really there, for its timestamp. The rest become hash lookups. A film roll is exactly one directory -- image filenames never contain a path separator -- so the images of a film roll are collected as one contiguous run and examined against a single listing of it. This is also the unit the background crawl already worked in, so the scheduling is unchanged; only the examination underneath it is different. On a library of ~85k images across 546 film rolls on an NFS mount: filesystem calls 506,449 -> 84,851 whole library, 16 threads, cold ~3.0s background crawl, cold ~11.5s -> ~2.8s waiting for one film roll, median 0.020s -> 0.000s, worst 0.12s -> 0.09s The reported list is unchanged: for the whole library the crawler's output is identical line for line and in the same order as the previous per-image implementation, both with no updated sidecars and with 617 seeded ones, and the resulting images.flags column is identical across all 84,924 rows. Two deliberate differences in what the filesystem is asked: - an image is now considered present when the directory lists it, where before it was a g_file_test() that follows symlinks, so a broken symlink now counts as present rather than missing. - the .txt/.wav probes match the names the directory actually holds, so on case insensitive filesystems a spelling other than the four checked is no longer found by accident. On Linux the behaviour is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@ralfbrown that is a better idea than what I had, and I have implemented it — pushed as a Before writing it I measured the two strategies against each other on my library, serially
Worth noting where the win actually comes from: it is not the directory listings, which are It fits the existing structure better than I expected, because a film roll is exactly one Your three phases and the three the crawl was already split into line up one to one, which Combined with the parallelism and the background scheduling, on ~85k images across 546 film
The first row is the honest caveat: for a single parallel pass over the whole library this The last two rows are the ones that matter for @wpferguson's point: the wait when you open a On ordering — you are right that it is not necessary, but I kept it, because it is what let Two deliberate differences in what gets asked of the filesystem, both called out in the
The one case where this is slower than probing is a directory holding a great many files Disclosure: as before, written and tested with AI assistance (Claude Code with Claude Opus 5). |
Fixes #21849.
The startup crawl asks the filesystem about every image in the library before the user
interface appears — an existence check, a
stat()of the xmp, and four probes for.txt/.TXT/.wav/.WAV— serially, and startup blocks until it has finished. It isdominated by filesystem latency rather than cpu time, so on a library held on network
storage it dominates startup. On mine (84,924 images across 546 film rolls, on an NFS4 mount
of a NAS) that is 506,449 filesystem calls taking ~58 s from cold and ~12 s with warm caches,
of which only ~3 s is cpu. It reports nothing, every time.
This changes both what the crawl asks the filesystem and when it runs. There are three
commits and they are independent of each other.
What the crawl does
A directory at a time, instead of a probe per image. Suggested by @ralfbrown. Each
directory is listed once and the questions are answered from that listing; the only call
that still has to reach the filesystem per image is the
stat()of an xmp that is reallythere, for its timestamp. Everything else becomes a hash lookup. That takes the library from
506,449 filesystem calls to 84,851.
This fits because a film roll is exactly one directory —
images.filenamenever contains apath separator, and on my library it is 546 film rolls to 546 distinct folders — so the
images of a film roll are collected as one contiguous run and examined against a single
listing of it.
In parallel.
dt_control_crawler_run()is split into three phases: collect the rows fromthe database, examine the filesystem from a pool of worker threads, then apply the results.
The workers touch neither the database nor the gui and each writes only its own array slot,
so no locking is needed. The result list is assembled single threaded afterwards, walking the
items in collection order, so the reported list and the resulting database state are
identical regardless of how many threads are used. That is what makes this checkable
against the old behaviour rather than merely plausible.
Because the work is latency bound, the pool deliberately oversubscribes rather than scaling
with the core count.
crawler_threadsdefaults to 16; setting it to 1 restores a serialexamination.
The crawl's existing semantics are kept faithfully, including the slightly surprising one
where an image whose xmp is absent skips the
.txt/.wavcheck entirely (the originalcontinue).When the crawl runs
In the background, newest film roll first. The crawl runs from a
DT_JOB_QUEUE_SYSTEM_BGjob, one film roll at a time, ordered by
film_rolls.access_timestamp. Startup does not waitfor it.
run_crawler_in_backgrounddefaults to true; false restores the blocking startupcrawl.
But it waits for the film roll you open. @wpferguson pointed out that the blocking crawl
also protects against editing an image whose xmp was updated on another machine, and that is
right. The consequence is worse than a stale thumbnail: darktable does not re-read an xmp
when an image is opened, and
dt_image_write_sidecar_file()overwrites whatever is on diskwithout comparing timestamps. Editing an image before its film roll had been examined would
discard the other machine's changes — and because that write also updates
write_timestamp,the crawler would no longer report the file afterwards, so the loss would be silent.
So a film roll is examined on demand when it is opened, and its findings are reported
straight away rather than with the rest at the end of the crawl, so the user is told before
they can edit anything. Waiting for one film roll costs a median of 0.000 s and 0.09 s for
the worst of my 546, against ~58 s for the whole library.
The collect module does not open film rolls through
dt_film_open(), so the collection isfollowed as well, and every film roll a collection covers is waited for rather than a sample
of them, since the user can scroll to any image in it. Worst case — a collection spanning the
whole library, opened immediately at startup — that costs what the blocking crawl costs
today, and far less in every other case.
Only one film roll is examined at a time, by either the background job or a caller waiting on
a specific roll, which also keeps the crawler's database transactions from interleaving.
The conflict dialog is shown non-modally when it comes from the background crawl, since it
can appear long after startup; the synchronous path still shows it modally as before.
The queue is deliberately not persisted between sessions. An xmp can change while
darktable is not running, so every session still examines every image — this takes the work
off the critical path rather than skipping it.
Results
On the library above, cold:
Testing
Verified against the original per-image implementation on the same library:
order, both with no updated sidecars (619 lines) and with 617 seeded ones (1,236 lines)
images.flagscolumn is identical across all 84,924 rows, in serial,parallel and background modes, and for both the per-image and per-directory examination
and no duplicates
on demand, rather than at the end of the crawl
examined before the newest ones
My library contains no genuine conflicts, so the conflict path was exercised by backdating
write_timestampin a copy of the database rather than by touching any file on disk.Quitting during the crawl behaves as it does for any other background job: if it is still
running you get the existing "darktable will be locked until background work has been done"
notice and the existing drain wait in
dt_control_quit(), then it shuts down. The abort flagis checked per image, and
dt_control_crawler_stop(TRUE)indt_cleanup()mirrors theexisting
dt_stop_backthumbs_crawler(TRUE)placement.Deliberate differences in behaviour
g_file_test()followssymlinks — so a broken symlink now counts as present rather than missing.
.txt/.wavlookups match the names the directory actually holds, so on a caseinsensitive filesystem a spelling other than the four checked is no longer found by
accident. On Linux nothing changes.
beside very few images is slower than probing would have been. I did not think that worth
special casing.
I have not tried this on Windows or macOS. The Windows
_wstati64path is carried overunchanged, but it now runs on a worker thread, so it is worth a look from someone who can
test there.
Disclosure: designed, written and tested with AI assistance (Claude Code with Claude Opus 5).
Verified against current master and builds clean with no new warnings.