Skip to content

Indexer: Preflight Oversized Images, Increase Watchdog Frequency, and Identify Files That Cause Crashes - #203

Open
Ocean572 wants to merge 1 commit into
ARahim3:mainfrom
Ocean572:fix/nef-tiff-queue-182
Open

Ocean572 wants to merge 1 commit into
ARahim3:mainfrom
Ocean572:fix/nef-tiff-queue-182

Conversation

@Ocean572

@Ocean572 Ocean572 commented Nov 6, 2026 •

Copy link
Copy Markdown

Closes #182.

Problem

Large RAW images and layered TIFF files could exceed the indexing helper's memory limit during decoding, triggering a Swift allocation trap and terminating the helper.

The shell reported the failure as:

indexer: exited (5)

The existing watchdog checked the helper once per second. This was not frequent enough to reliably identify the file being processed when the crash occurred.

The resulting failure could permanently block the indexing queue:

  1. An oversized image reaches the front of the queue.
  2. The indexing helper crashes while attempting to decode it.
  3. The shell restarts the helper.
  4. The same file is processed again and crashes the helper again.
  5. The file remains pending, preventing subsequent files from being indexed.

The original report described approximately 55,000 files waiting behind a single problematic file.

Changes

This PR introduces three related improvements:

  1. Preflight image dimensions before decoding to identify files likely to exceed the memory budget.
  2. Increase watchdog sampling frequency to detect failures more quickly.
  3. Record crash information and identify the specific file responsible, allowing the application to mark it as failed and continue indexing.

1. Preflight Oversized Images Before Decoding

The indexer now estimates image memory requirements from image metadata before attempting to decode the image.

If the estimated memory requirement exceeds the helper's configured memory limit, the file is marked as couldn't be read without attempting the expensive decode operation.

Different image formats use different estimated memory coefficients:

Image format Estimated memory
RAW 192 bytes/pixel
Layered TIFF 16 bytes/pixel
stb-supported formats 8 bytes/pixel

These estimates allow the indexer to reject potentially oversized images before they trigger an allocation failure.

Formats That Do Not Require Preflight

Plain TIFF, JPEG, and PNG files are not subject to this new preflight rejection.

Their decoding paths already support scaled, tiled processing, which avoids allocating the entire decoded image in memory.

Measurements from this machine demonstrate the difference:

  • A 2,125-megapixel LZW TIFF required approximately 70 MB.
  • A 100-megapixel PNG required approximately 18 MB.

Rejecting these formats based solely on their total pixel count would unnecessarily exclude images that can be processed efficiently through tiled decoding.

2. Increase Watchdog Sampling Frequency

The watchdog now checks the indexing helper 10 times per second, instead of once per second.

This provides more responsive detection of helper failures and improves the ability to track indexing progress and identify problematic files.

3. Record Crashes and Identify the Problematic File

The indexing helper now writes a StoppedFile crash note alongside the index when it is terminated by a fatal signal.

The note is written from a signal handler using system calls only, with the destination path established during installation.

When the helper restarts, the application uses the recorded information to identify and mark the file associated with the crash.

Both the macOS application and the Windows/Linux shell now treat fatal-signal exits similarly to watchdog-triggered stops, allowing the existing restart and restart-cap mechanisms to handle them.

Restart Protection

The existing restart protection limits repeated stops to four within a ten-minute period.

However, this could still prevent an index containing many oversized images from making progress.

The restart counter now resets after the queue successfully moves forward by 100 files between stops.

This allows the indexer to work through multiple problematic files without permanently exhausting its restart allowance.

Important Implementation Details

Pre-push code review identified three significant defects in the initial implementation. All three have been addressed.

1. Identify the File Actually Being Processed, Not the Queue Head

The initial implementation attempted to identify the problematic file using the queue's current head.

This was incorrect because media indexing processes files in batches of eight. The entire batch remains pending until processing completes.

For example, if the third image in a batch crashes the helper, the first image is not necessarily responsible.

Marking the queue head would incorrectly fail an innocent file while leaving the actual problematic file pending.

With seven innocent files ahead of a problematic image, repeated restarts could exhaust the four-stop restart allowance before the problematic file ever reached the queue head.

Corrected Approach

The crash note remains pathless because the helper can crash before Indexer.nowReading is updated.

Instead, both applications use information they already receive through the existing file events:

  1. The application tracks the most recently reported file event.
  2. If the helper crashes, the application passes that filename through DIGUP_STOPPED_FILE when restarting the helper.
  3. The restarted helper uses that filename to mark the actual problematic file as failed.

The queue-head marking behavior remains as a fallback when no filename is available, such as:

  • A terminal indexing run.
  • A crash occurring before any file event is reported.
  • A helper spawn that does not include DIGUP_STOPPED_FILE.

This avoids incorrectly failing an unrelated file when the actual crash location is known.

2. Detect Fatal Signals Instead of Relying on Exit Codes

The initial implementation classified fatal crashes using ExitStatus::code() alone.

This does not work reliably on Unix systems.

When a child process is terminated by a signal, its exit code is None. Consequently, the shell could interpret the result incorrectly and fail to restart the helper.

Unix Handling

The ended event now includes the terminating signal through:

ExitStatusExt::signal()

This is implemented for Unix targets.

The macOS fatal-signal classification also now includes SIGBUS (signal 10), which was previously missing.

Windows Handling

Windows does not use Unix-style termination signals.

Instead, the shell identifies supported fatal Structured Exception Handling (SEH) exit codes.

The classification covers:

  • Breakpoint.
  • Access violation.
  • Stack overflow.
  • Fast fail.
  • Illegal instruction.
  • In-cell divide.
  • Divide-by-zero.

Both Unix signal classification and Windows exit-code classification are implemented as small, independently testable functions.

3. Prevent the Preflight Parser from Crashing

Review also identified a potential overflow in the layered-TIFF dimension parser.

Layered-TIFF dimensions are obtained from 4-byte LONG tags. A malformed header containing a value such as 0xFFFFFFFF could overflow the parser's own Int arithmetic.

This created a dangerous situation where the memory guard designed to prevent crashes could itself trigger an allocation or arithmetic trap.

The parser has been hardened:

  • Dimension values larger than 1 << 24 are treated as invalid and excluded from memory estimation.
  • Memory calculations use UInt64 with saturating arithmetic on both platforms.
  • Budget comparisons no longer require converting the configured budget to Int.

The last change also prevents an unusually large --memory-limit value from causing an overflow during the helper's own initialization through the << 20 conversion.

Performance and End-to-End Verification

Measured on the development machine:

Hardware and environment: Apple M1, 8 GB RAM, macOS 26.5.1.

RAW Image Preflight

Tested using a 45.44-megapixel Nikon Z9 NEF file.

Configuration Result
Default 4 GB memory limit Rejected in 0.17 seconds
16 GB memory limit Successfully decoded in 5.9 seconds

Under the default 4 GB limit, the file is rejected before decoding with the following message:

needed an estimated 8.1 GB ... a RAW picture of 45 MP

This demonstrates that the preflight can identify oversized RAW files without triggering the expensive decode operation.

Mixed Image Queue

Tested with a queue containing NEF, HEIC, and JPEG files.

The indexer completed with:

  • 2 files successfully indexed.
  • 1 file marked as failed.
  • Helper exited normally with code 0.

Crash Recovery

A helper was deliberately terminated with SIGTRAP during indexing.

The subsequent helper restart successfully identified and marked the problematic file, then continued processing the remaining queue.

Final result:

  • 19 files successfully indexed.
  • 0 files remaining pending.

Stopped-File Identification

Tested end-to-end against the real helper using a sandbox index containing three queued text files:

a.txt, b.txt, and c.txt

The queue head was a.txt.

Two scenarios were tested:

Scenario A: Crash note without a named file

The helper received the stopped-file note without DIGUP_STOPPED_FILE.

Result:

  • a.txt was marked as failed using the queue-head fallback.
  • The file entered state='failed'.
  • The stopped event correctly identified a.txt.

Scenario B: Crash note with DIGUP_STOPPED_FILE=c.txt

The helper received both the crash note and the explicit filename.

Result:

  • c.txt was marked as failed.
  • a.txt remained pending.
  • The queue was able to move past the problematic file.

This verifies that explicit crash identification takes precedence over the queue-head fallback.

Testing

Swift Tests

138 tests passed.

The original baseline was 125 tests, with 13 new tests added in the PreflightTests suite:

  • 4 layered-TIFF parser tests, including malformed dimension handling.
  • 3 ImageIO preflight tests.
  • 2 camera-file tests gated by DIGUP_TEST_RAW.
  • 3 stopped-file crash-note tests.
  • 1 error-message wording test.

Rust Tests

17 tests passed.

The original baseline was 13 tests, with four new tests:

  • 2 crash-classification tests covering Unix signals and Windows exception codes.
  • 2 reap tests using real child processes:
    • SIGTRAP produces (None, Some(5)).
    • exit 75 produces (Some(75), None).

Additionally:

  • cargo check --tests completed successfully.

Scope and Limitations

The following platform-specific details and limitations are important for acceptance testing.

Memory Estimation Accuracy

The memory coefficients are estimates rather than universal measurements:

  • RAW (192 bytes/pixel): Calibrated against one camera on one macOS system. The measured requirement was approximately 17 bytes/pixel, with a field-derived floor of at least 176 bytes/pixel under an 8 GB watchdog. The higher 192-byte coefficient is deliberately conservative for modern ImageIO.
  • Layered TIFF (16 bytes/pixel): A design estimate, not a directly measured coefficient.
  • stb-supported formats (8 bytes/pixel): A design estimate, not a directly measured coefficient.

Windows Crash Notes

The Windows CrashNotes implementation covers the SEH exception classes that the C runtime translates into the supported fatal signal categories (SEGV/FPE/ILL).

Other Windows crash types may not produce a crash note.

In those cases, the fallback may not identify or mark the problematic file. The restart cap still limits repeated crashes.

The Windows-specific signal plumbing and SEH exit-code handling compile only for Windows targets and have not been verified on this development machine.

Signal Handler Behavior

Installing plain signal handlers replaces Swift's default crash-printing behavior.

The current implementation re-raises the signal so macOS can still generate its .ips crash report. However, Linux loses Swift's normal backtrace output.

Chaining Swift's existing signal handler through sigaction is identified in the code as a potential follow-up improvement.

GUI Acceptance Testing

The development machine runs the indexing helpers and unit tests but does not exercise the Tauri GUI application.

The following still require acceptance testing:

  • macOS panel restart behavior following a real helper crash.
  • Windows shell behavior following a fatal helper exit.
  • Cross-platform crash recovery through the actual application UI.

Platform-Specific Test Coverage

The two camera-file tests require a real NEF file on the machine running the suite and remain gated behind DIGUP_TEST_RAW.

The three ImageIO-path tests compile out on Linux, where the two stb-side preflight tests run instead.

The new PreflightTests suite was introduced with this branch, so all 13 tests in that suite are new.

…aster, and note the file a crash died on

Large RAWs and layered TIFFs ran the indexing helper's reading past its memory and killed
it — Swift's allocation trap, "indexer: exited (5)" — faster than the watchdog's once-a-second
tick could note the file. The file stayed at the queue's head, and every restart took it first
and died on it again: the ARahim3#182 report has 55,000 files waiting behind one.

A picture whose header says its reading would ask for more than the helper's memory limit is
now marked "couldn't be read" before decoding: RAW at 192 bytes per pixel (measured 17 B/px on
a real 45.44 MP Nikon Z9 NEF on macOS 26.5.1, 782 MB peak, and ≥176 in the field at an 8 GB
watchdog — 192 chosen above the field floor, disclosed conservative for modern ImageIO),
layered TIFFs at 16 B/px and the stb side at 8 B/px (design estimates, disclosed as unmeasured
in the code). Plain TIFFs, JPEGs and PNGs keep no preflight: their scaled decode streams in
tiles, measured at 70 MB for a 2,125 MP LZW TIFF and 18 MB for a 100 MP PNG.

The watchdog now looks ten times a second. A helper killed on the spot writes a note beside the
index from a signal handler (syscalls only, path fixed at install), and the next helper marks
the file the crash was on. The Mac app and the Tauri shell treat a fatal-signal exit like a
watchdog stop (restart, cap), and the 4-stops-in-10-min cap resets once the queue has moved 100
files between stops, so an index full of oversized RAWs works through them instead of freezing.

Which file a crash was on is the hard half, and the pre-push review of this branch pointed out
three places where the first version got it wrong:

* A note with no path used to mean "mark the queue's head", which it is not: media runs in
  batches of eight and the whole batch stays pending till the batch ends, so a trap on the third
  image failed an innocent first one and left the poison queued (7 innocents per poison, with 4
  as the restart cap — the queue froze before the bad file reached the head). The note is
  therefore still pathless (the crash beats `Indexer.nowReading`), and **the app names the file
  for the next helper**: both apps already see every `file` event, so `Engine` / the shell keep
  the last one reported and pass it as `DIGUP_STOPPED_FILE` on the restart; the head-mark stays
  as the fallback for a spawn with no name (a terminal run, a crash before any file event).
* A fatal-signal exit was classified from the exit code alone, which is inert on Linux:
  `ExitStatus::code()` is None for a child killed by a signal, so the shell saw 0 and restarted
  nothing. The `ended` event now carries the signal (`ExitStatusExt::signal()`, Unix-only;
  Windows has no signals and carries the class as the SEH exit code — breakpoint, access
  violation, stack overflow, fast fail, illegal instruction, divide-by-zero, all matched), the
  Mac list gained SIGBUS (10), and the classification is a small function with unit tests in
  both spellings.
* The preflight itself could trap: the layered-TIFF sizes come from 4-byte LONG tags, and a
  header saying 0xFFFFFFFF overflowed the parser's own `Int` sum — a trap inside the guard built
  to stop traps. Sizes above 1<<24 are garbage and get no estimate; the sums are `UInt64` and
  saturating on both platforms, and the budget comparisons no longer force `Int(budget)` (an
  absurd `--memory-limit` trapped in the helper's own setup too, via `<< 20`).

Measured on this box (Apple M1, 8 GB, macOS 26.5.1): the Z9 NEF is marked in 0.17 s under the
4 GB default limit ("needed an estimated 8.1 GB ... a RAW picture of 45 MP") and decodes in
5.9 s at a 16 GB limit; a queue of NEF+HEIC+JPEG ends 2 done 1 failed with exit 0; a helper
killed with SIGTRAP mid-run is followed by a helper that marks the file and finishes the rest
(19 done, pending 0). Named marking, end to end against the real helper on a sandbox index of
three queued text files with the newest (the queue's head) a.txt: the note alone marks a.txt
(the fallback, `state='failed'`, event `stopped` naming it); the note plus
`DIGUP_STOPPED_FILE=c.txt` marks **c.txt** and leaves a.txt pending — the innocent head is not
failed and the queue moves past the poison. Suite: 138 green (base 125, +13, all of them the new
`PreflightTests` suite: 4 layered-TIFF parser tests including the garbage-dimensions case, 3
preflight-through-ImageIO tests, 2 camera-file tests gated on `DIGUP_TEST_RAW`, 3 stopped-file
note tests, 1 wording test). `cargo test` 17 green (base 13, +2 crash-class tests, +2 `reap` tests on real
children: SIGTRAP gives `(None, Some(5))`, `exit 75` gives `(Some(75), None)`), and
`cargo check --tests` is clean.

Disclosed, as written in the code: the layered-TIFF and stb bytes-per-pixel coefficients are
design estimates, not field measurements; the Windows `CrashNotes` net covers only the SEH
classes the C runtime translates there (SEGV/FPE/ILL), so a Windows crash can leave no note —
the fallback then marks nothing and the cap bounds the restarts, and the Windows-side blocks
(the SEH codes, the `fatal_signal` no-op) compile only for Windows targets, so this box's runs
do not exercise them; installing plain signal handlers trades Swift's own crash print for the
note (macOS keeps its `.ips` via the re-raise, Linux loses the print, and chaining Swift's
handler through `sigaction` is the natural next step); the preflight tests share the helper's
global budget (`ImageExtractor.decodeMemoryBudget`) and run serialized, coupled to one another by
design; and the GUI paths — the Mac app restarting a helper after a real crash, the shell app on
Windows — are not exercised on this box, which runs the helpers and the unit tests, not the
Tauri app.
@Ocean572 Ocean572 changed the title Indexer: mark oversized pictures from their header, tick the watchdog faster, and note the file a crash died on Indexer: Preflight Oversized Images, Increase Watchdog Frequency, and Identify Files That Cause Crashes Nov 6, 2026

This branch has not been deployed

No deployments
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.

Indexer exits (code 5) on large NEF/TIFF files and stalls the whole queue

1 participant