Skip to content

Sharded image read cache and parallel entropy: faster concurrent reads for IPED workers#3

Open
T0k1To wants to merge 3 commits into
sepinf-inc:4.15.0_iped_patchfrom
T0k1To:perf-sharded-cache
Open

Sharded image read cache and parallel entropy: faster concurrent reads for IPED workers#3
T0k1To wants to merge 3 commits into
sepinf-inc:4.15.0_iped_patchfrom
T0k1To:perf-sharded-cache

Conversation

@T0k1To

@T0k1To T0k1To commented Jul 9, 2026

Copy link
Copy Markdown

Summary

This PR ports a set of performance optimizations to the 4.15.0_iped_patch branch, targeting IPED's hottest native path: concurrent file-content reads through libtsk_jni (ReadContentInputStream) from multiple worker threads.

  1. Sharded image read cache: the TSK_IMG_INFO read cache becomes 8 independent banks of 4 entries (same total capacity: 32 x 64KB), each with its own lock. Blocks are 64KB-aligned and mapped to banks by 256KB regions, so lookups scan 4 entries instead of 32, and readers of different regions no longer contend on a single global lock. cache_lock keeps its role as the backend lock (raw/AFF/EWF/logical/pool state stays serialized, no backend changes). Strict bank -> backend lock order and no thread ever takes two banks, so the design is deadlock-free by construction. Includes a per-bank sequential read-ahead (disable with TSK_IMG_READAHEAD=0; the effect on NVMe is neutral, the expected benefit is on slow media such as USB drives, HDDs and network shares).
  2. Parallel entropy calculation: calculateEntropy() counts its ~99 blocks of 64KB with up to 4 threads (per-block histograms merged in block order, so the result is bit-identical to the sequential pass; TSK_ENTROPY_THREADS=0 disables). About 3.4x faster on volumes that trigger encryption detection.
  3. MD5 buffer ops: replace the 1991 RSA reference byte-loops (MD5_memcpy/MD5_memset) with libc memcpy/memset.

Benchmarks (this branch vs unmodified 4.15.0_iped_patch)

tsk_img_read 4KB reads, ext4 corpus (15k files), warm cache, median of 3 interleaved A/B runs, 4-core/8-thread machine:

threads baseline patched gain
1 4180 MB/s 4507 MB/s +8%
2 3122 MB/s 3939 MB/s +26%
4 3060 MB/s 3651 MB/s +19%
8 2575 MB/s 3436 MB/s +33%

End-to-end through the Java bindings (the exact IPED read path: SleuthkitCase + AddImageProcess + 8 threads draining every file via ReadContentInputStream, 16,127 files / 1.5GB): median 1993 -> 2309 MB/s (+16%, steady-state passes +14% to +32%), 0 read errors.

Correctness validation

  • tsk_loaddb case database byte-identical to the unpatched build (154,616 dump statements compared).
  • fls -r listing identical (15,379 lines); icat bit-identical on sampled inodes; entropy output identical.
  • fs_thread_test with 8 threads: identical per-thread logs.
  • ThreadSanitizer clean (fs_thread_test and an 8-thread read microbenchmark).
  • make check PASS; no new compiler warnings (-Wall -Wextra).

Why this matters for IPED, and where the gains show up

All gains are on the image read path; parsing, indexing and Java-side hashing CPU time are unaffected.

  • Processing phase with in-process reading (robustImageReading = false): every worker thread reads file content through SleuthkitInputStream/JNI concurrently. This is where the +16% to +33% aggregate read throughput applies directly (hashing, signature detection, parsing, export, thumbnails).
  • Processing phase with the default robustImageReading = true: each SleuthkitServer helper process is a single-threaded libtsk consumer, so today the benefit there is in the single-thread class (+8% and read-ahead, per server). The full multi-thread win becomes reachable once IPED can raise numImageReaders or prefer in-process reads when a patched TSK is detected. The poor concurrency of the single-lock cache was one of the original reasons for the ceil(cores/6) cap, and this PR removes that constraint. A follow-up IPED-side PR is planned.
  • Evidence decode (AddImageProcess, single-threaded by design): +8% from the cheaper cache lookup, plus read-ahead on sequential metadata sweeps.
  • Encrypted or unrecognized volumes during decode (BitLocker, LUKS, unknown filesystems): the entropy-based detection runs about 3.4x faster per volume.

If accepted, a version bump of the published artifacts (e.g. p2) would let IPED condition numImageReaders/in-process reading on the presence of the sharded cache.

T0k1To added 3 commits July 8, 2026 18:17
…aders

Restructure the TSK_IMG_INFO read cache from a single global cache_lock
into 8 independent banks of 4 entries (total capacity unchanged:
32 x 64KB = 2MB), each bank with its own lock:

- Cache blocks are 64KB-aligned and the address space maps to banks by
  256KB regions, so every block belongs to exactly one bank and lookups
  scan only that bank's 4 entries (the old code scanned all 32 on every
  tsk_img_read call).
- cache_lock keeps its role as the backend lock: image format backends
  (raw, AFF, EWF, logical, pool) hold shared state (seek positions,
  handles) and remain serialized by it. Strict lock order bank->backend
  and no thread ever holds two banks, so the design is deadlock-free by
  construction.
- Sequential-access detection with a bounded read-ahead of the next
  64KB block, tracked per bank; disable at runtime with
  TSK_IMG_READAHEAD=0.
- Lock setup centralized in tsk_img_lock_init()/tsk_img_lock_deinit(),
  used by img_open and the APFS/LVM pool wrappers.
- Requests crossing a 64KB block boundary bypass the cache (direct
  serialized read, as large reads already did).

Benchmarks on this branch (ext4 corpus, 15k files, warm cache, median
of 3 interleaved A/B runs, 4-core/8-thread laptop):

  tsk_img_read 4KB  1 thread:  4180 -> 4507 MB/s  (+8%)
                    2 threads: 3122 -> 3939 MB/s  (+26%)
                    4 threads: 3060 -> 3651 MB/s  (+19%)
                    8 threads: 2575 -> 3436 MB/s  (+33%)

Validation: tsk_loaddb database byte-identical to the unpatched build
(154,616 dump statements); fls -r listing identical (15,379 lines);
icat output bit-identical on sampled inodes; fs_thread_test with 8
threads produces identical per-thread logs; ThreadSanitizer clean on
fs_thread_test and an 8-thread read microbenchmark.

This directly benefits IPED's parallel in-process image reading
(ReadContentInputStream via JNI from multiple worker threads).
calculateEntropy() reads up to 99 x 64KB blocks and counts byte
frequencies sequentially. Count the blocks with a small thread pool
(<= 4 threads, block-stride partitioning), leveraging the sharded
image cache for concurrent reads:

- Per-block histograms are merged in block order, stopping at the
  first failed read, replicating the sequential semantics exactly:
  the result is bit-identical to the sequential pass.
- Sequential path kept for Windows, small block counts, thread
  creation failure, or TSK_ENTROPY_THREADS=0.
- The sequential path buffer stays heap-allocated as upstream
  changed it in 4.15.

Measured ~3.4x faster on ext4 (2.28s -> 0.67s over 200 iterations),
~2.2-2.6x on FAT32/ISO9660. ThreadSanitizer clean. Entropy output
verified identical to the unpatched build.
The RSA reference MD5 implementation (1991) copies and fills memory
with byte-by-byte for loops. Use libc memcpy()/memset() so the
compiler can inline and vectorize. NIST vectors pass, including
irregular chunking that exercises partial buffering.
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