mirror of
https://github.com/aaru-dps/libaaruformat.git
synced 2026-09-23 23:34:19 +00:00
Bound the block caches by bytes held instead of by entry count
The read caches sized themselves as MAX_CACHE_SIZE / (SectorSize << shift), but ctx->shift is only ever assigned on the DDT v1 path, and the context is zeroed at open. On every DDT v2 image the divisor therefore collapsed to the sector size and the limit came out as 262144 entries, while each block_cache entry holds a whole decompressed data block of SectorSize << dataShift bytes, 8 MiB with the default data_shift of 12. That is a ceiling of roughly 2 TiB rather than the intended 512 MiB, so the count limit was never reached: a sequential sector-by-sector pass cached the entire decompressed image and freed none of it until aaruf_close(). Comparing two 40 GB images this way was killed by the OOM reaper. An entry count cannot express a memory budget when the values vary in size, and here they vary by five orders of magnitude: block_header_cache holds BlockHeader structs while block_cache holds megabyte payloads, yet both were given the same limit. Track the byte size of each value instead and evict least-recently-used entries until the cache is back under budget, and drop the block geometry from cache initialization entirely. The block cache gets the 512 MiB MAX_CACHE_SIZE it always intended; block headers get a separate 8 MiB budget, which covers far more blocks than the payload cache can hold. The entry just inserted is never evicted, so callers may keep using the pointer they handed over for the rest of the call. That also removes the use-after-free that a zero limit used to cause: the old eviction loop started at the hash head and could free the entry just added, after which the caller read from freed memory.
This commit is contained in:
@@ -78,6 +78,10 @@
|
||||
* still enabling efficient sequential and moderate random access patterns. */
|
||||
#define MAX_CACHE_SIZE 536870912ULL
|
||||
|
||||
/** Maximum block header cache size (bytes). Block headers are a few dozen bytes each, so 8 MiB
|
||||
* keeps headers cached for far more blocks than MAX_CACHE_SIZE can hold payloads for. */
|
||||
#define MAX_HEADER_CACHE_SIZE 8388608ULL
|
||||
|
||||
/** Size in bytes of the fixed LZMA properties header (lc/lp/pb + dictionary size). */
|
||||
#define LZMA_PROPERTIES_LENGTH 5
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef LIBAARUFORMAT_LRU_H
|
||||
#define LIBAARUFORMAT_LRU_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <uthash.h>
|
||||
|
||||
@@ -24,29 +25,38 @@ struct CacheEntry
|
||||
{
|
||||
uint64_t key; ///< 64-bit integer key (unique within the cache).
|
||||
void *value; ///< Opaque value pointer associated with key.
|
||||
size_t size; ///< Size in bytes of the memory pointed to by value.
|
||||
UT_hash_handle hh; ///< uthash handle (must remain per uthash docs).
|
||||
};
|
||||
|
||||
/** \struct CacheHeader
|
||||
* \brief Cache top-level descriptor encapsulating the hash table root and capacity limit.
|
||||
*
|
||||
* The cache enforces an upper bound (max_items) on the number of tracked entries. On insert,
|
||||
* the oldest entry is evicted when the limit is reached (LRU via uthash insertion order).
|
||||
* The cache is bounded by the total number of *bytes* held by its values, not by the
|
||||
* number of entries: the caches in this library store values of wildly different sizes
|
||||
* (a block header is a few dozen bytes, a decompressed data block is megabytes), so an
|
||||
* entry-count limit cannot express a memory budget. On insert, the least recently used
|
||||
* entries are evicted until the cache is back under max_bytes.
|
||||
*
|
||||
* Fields:
|
||||
* - max_items: Maximum number of entries allowed; 0 means unlimited.
|
||||
* - max_bytes: Memory budget in bytes; 0 means unlimited.
|
||||
* - cur_bytes: Bytes currently held by the cached values.
|
||||
* - cache: uthash root pointer; NULL when the cache is empty.
|
||||
* - free_func: Optional callback to free cached values on eviction/clear.
|
||||
*
|
||||
* The entry just inserted is never evicted, so a caller may keep using the pointer it
|
||||
* handed over for the remainder of the call even if the value is larger than max_bytes.
|
||||
*/
|
||||
struct CacheHeader
|
||||
{
|
||||
uint64_t max_items; ///< Hard limit for number of entries.
|
||||
uint64_t max_bytes; ///< Memory budget in bytes for the cached values. 0 means unlimited.
|
||||
uint64_t cur_bytes; ///< Bytes currently held by the cached values.
|
||||
struct CacheEntry *cache; ///< Hash root (uthash). NULL when empty.
|
||||
void (*free_func)(void *); ///< Optional callback to free cached values. NULL if not needed.
|
||||
};
|
||||
|
||||
void *find_in_cache_uint64(struct CacheHeader *cache, uint64_t key);
|
||||
void add_to_cache_uint64(struct CacheHeader *cache, uint64_t key, void *value);
|
||||
void add_to_cache_uint64(struct CacheHeader *cache, uint64_t key, void *value, size_t size);
|
||||
void free_cache(struct CacheHeader *cache);
|
||||
|
||||
#endif // LIBAARUFORMAT_LRU_H
|
||||
|
||||
Reference in New Issue
Block a user