Skip to content

Commit a38e1e1

Browse files
author
Grok Compression
committed
LRU: add test for eviction and re-decompression
1 parent 2b30d16 commit a38e1e1

4 files changed

Lines changed: 222 additions & 14 deletions

File tree

src/lib/core/codestream/decompress/CodeStreamDecompress.cpp

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ void CodeStreamDecompress::init(grk_decompress_parameters* parameters)
136136
cp_.init(parameters, tileCache_);
137137
auto core = &parameters->core;
138138
tileCache_->setStrategy(core->tile_cache_strategy);
139+
tileCache_->setMaxActiveTiles(core->max_active_tiles);
139140
ioBufferCallback_ = core->io_buffer_callback;
140141
ioUserData_ = core->io_user_data;
141142
grkRegisterReclaimCallback_ = core->io_register_client_callback;
@@ -257,25 +258,34 @@ bool CodeStreamDecompress::decompressImpl(std::set<uint16_t> pendingTiles)
257258
if(pendingTiles.empty())
258259
return true;
259260

260-
// Extract LRU-evicted tiles that can be re-decompressed from cache
261-
// (opt-in: only when compressed chunk cache is active and TLM is present)
262-
std::set<uint16_t> reDecompressTiles;
263-
if(compressedChunkCache_ && cp_.hasTLM())
261+
// Extract LRU-evicted tiles that can be re-decompressed
262+
std::set<uint16_t> reDecompressTLM; // from compressed chunk cache
263+
std::set<uint16_t> reDecompressSeek; // from cached SOT offsets
264+
for(auto it = pendingTiles.begin(); it != pendingTiles.end();)
264265
{
265-
for(auto it = pendingTiles.begin(); it != pendingTiles.end();)
266+
auto cacheEntry = tileCache_->get(*it);
267+
if(cacheEntry && cacheEntry->processor->getImage() && !cacheEntry->processor->getTile() &&
268+
cacheEntry->dirty_)
266269
{
267-
auto cacheEntry = tileCache_->get(*it);
268-
if(cacheEntry && cacheEntry->processor->getImage() && !cacheEntry->processor->getTile() &&
269-
cacheEntry->dirty_ && compressedChunkCache_->contains(*it))
270+
if(compressedChunkCache_ && compressedChunkCache_->contains(*it))
270271
{
271-
reDecompressTiles.insert(*it);
272+
reDecompressTLM.insert(*it);
273+
it = pendingTiles.erase(it);
274+
}
275+
else if(cacheEntry->processor->allSOTMarkersParsed())
276+
{
277+
reDecompressSeek.insert(*it);
272278
it = pendingTiles.erase(it);
273279
}
274280
else
275281
{
276282
++it;
277283
}
278284
}
285+
else
286+
{
287+
++it;
288+
}
279289
}
280290

281291
// Require tile_ to exist for differential updates — an LRU-evicted tile
@@ -299,7 +309,7 @@ bool CodeStreamDecompress::decompressImpl(std::set<uint16_t> pendingTiles)
299309
numTilesDecompressed_ = 0;
300310

301311
// Re-decompress LRU-evicted tiles from compressed chunk cache
302-
for(auto tileIndex : reDecompressTiles)
312+
for(auto tileIndex : reDecompressTLM)
303313
{
304314
auto cacheEntry = tileCache_->get(tileIndex);
305315
auto proc = cacheEntry->processor;
@@ -313,6 +323,20 @@ bool CodeStreamDecompress::decompressImpl(std::set<uint16_t> pendingTiles)
313323
cacheEntry->dirty_ = false;
314324
}
315325

326+
// Re-decompress LRU-evicted tiles by seeking to cached SOT offsets
327+
for(auto tileIndex : reDecompressSeek)
328+
{
329+
auto cacheEntry = tileCache_->get(tileIndex);
330+
auto proc = cacheEntry->processor;
331+
if(!proc->reinitForReDecompress())
332+
continue;
333+
if(!proc->decompressFromCachedTileParts())
334+
continue;
335+
if(!schedule(proc, true))
336+
break;
337+
cacheEntry->dirty_ = false;
338+
}
339+
316340
if(pendingTiles.empty())
317341
return true;
318342

@@ -1233,7 +1257,20 @@ void CodeStreamDecompress::wait(grk_wait_swath* swath)
12331257
return;
12341258
}
12351259

1236-
// 2. wait for sequential parse
1260+
// 2a. Flush batch queues when using non-swath full wait.
1261+
// When tileCompletion_ is active but wait(nullptr) is called (not swath-based),
1262+
// the back-pressure logic in scheduleTileBatch() prevents scheduling queued tiles
1263+
// because lastCleared never advances (no swath consumer). Force-schedule all
1264+
// remaining tiles so the worker thread can exit.
1265+
if(tileCompletion_ && doTileBatching())
1266+
{
1267+
// Remove the scheduling limit so scheduleTileBatch can drain
1268+
batchTileScheduledRows_ = 0;
1269+
tileCompletion_->setLastClearedTileY(INT16_MAX);
1270+
scheduleTileBatch();
1271+
}
1272+
1273+
// 2b. wait for sequential parse
12371274
if(decompressWorker_.joinable())
12381275
decompressWorker_.join();
12391276

src/lib/core/grok.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ typedef struct _grk_decompress_core_params
674674
*/
675675
uint16_t layers_to_decompress;
676676
uint32_t tile_cache_strategy; /* tile cache strategy */
677+
uint16_t
678+
max_active_tiles; /* max tiles with decompressed data (LRU eviction limit, 0 = unlimited) */
677679
uint32_t disable_random_access_flags; /* disable random access flags */
678680
bool skip_allocate_composite; /* skip allocate composite image data for multi-tile */
679681
grk_io_pixels_callback io_buffer_callback; /* IO buffer callback */

src/lib/core/tile_processor/TileCompletion.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,12 @@ class TileCompletion
325325
return lastClearedTileY_;
326326
}
327327

328+
void setLastClearedTileY(int32_t val)
329+
{
330+
std::lock_guard<std::mutex> lock(mutex_);
331+
lastClearedTileY_ = val;
332+
}
333+
328334
int32_t getNeededTileY1() const
329335
{
330336
return neededTileY1_.load(std::memory_order_acquire);

tests/GrkLRUCacheTest.cpp

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,17 @@ struct CodecDeleter
5353
};
5454
using CodecPtr = std::unique_ptr<grk_object, CodecDeleter>;
5555

56-
// Create a synthetic test image and compress it to a J2K file with TLM markers
56+
// Create a synthetic test image and compress it to a J2K file
5757
static bool createTestImage(const std::string& path, uint32_t width, uint32_t height,
58-
uint32_t tileWidth, uint32_t tileHeight)
58+
uint32_t tileWidth, uint32_t tileHeight, bool writeTlm = true)
5959
{
6060
grk_cparameters cparams{};
6161
grk_compress_set_default_params(&cparams);
6262
cparams.cod_format = GRK_FMT_J2K;
6363
cparams.t_width = tileWidth;
6464
cparams.t_height = tileHeight;
65-
cparams.write_tlm = true;
65+
cparams.tile_size_on = (tileWidth < width || tileHeight < height);
66+
cparams.write_tlm = writeTlm;
6667
cparams.numresolution = 3;
6768
// Lossless
6869
cparams.irreversible = false;
@@ -362,6 +363,150 @@ static bool testLRUCacheEviction()
362363
return true;
363364
}
364365

366+
///////////////////////////////////////////////////////////////////
367+
// Test 4: LRU eviction + re-decompress from cached SOT offsets
368+
//
369+
// Creates a non-TLM image with 16 tiles, sets max_active_tiles=4,
370+
// decompresses all tiles (causing eviction), then re-decompresses
371+
// evicted tiles and verifies pixel data matches a fresh reference.
372+
///////////////////////////////////////////////////////////////////
373+
static std::vector<TileData> decompressTileByTile(const std::string& path,
374+
uint32_t tileCacheStrategy,
375+
uint16_t maxActiveTiles)
376+
{
377+
std::vector<TileData> result;
378+
379+
grk_decompress_parameters params{};
380+
params.core.tile_cache_strategy = tileCacheStrategy;
381+
params.core.max_active_tiles = maxActiveTiles;
382+
383+
grk_stream_params streamParams{};
384+
safe_strcpy(streamParams.file, path.data());
385+
386+
CodecPtr codec(grk_decompress_init(&streamParams, &params));
387+
if(!codec)
388+
{
389+
spdlog::error("Failed to init decompressor");
390+
return result;
391+
}
392+
393+
grk_header_info headerInfo{};
394+
if(!grk_decompress_read_header(codec.get(), &headerInfo))
395+
{
396+
spdlog::error("Failed to read header");
397+
return result;
398+
}
399+
400+
if(!grk_decompress_update(&params, codec.get()))
401+
{
402+
spdlog::error("grk_decompress_update failed");
403+
return result;
404+
}
405+
406+
uint16_t numTiles = headerInfo.t_grid_width * headerInfo.t_grid_height;
407+
spdlog::info("decompressTileByTile: grid {}x{} = {} tiles", headerInfo.t_grid_width,
408+
headerInfo.t_grid_height, numTiles);
409+
for(uint16_t t = 0; t < numTiles; ++t)
410+
{
411+
if(!grk_decompress_tile(codec.get(), t))
412+
{
413+
spdlog::error("grk_decompress_tile({}) failed", t);
414+
return {};
415+
}
416+
}
417+
418+
// Now re-decompress all tiles (evicted tiles trigger re-decompress path)
419+
for(uint16_t t = 0; t < numTiles; ++t)
420+
{
421+
if(!grk_decompress_tile(codec.get(), t))
422+
{
423+
spdlog::error("Re-decompress of tile {} failed", t);
424+
return {};
425+
}
426+
427+
auto* tileImg = grk_decompress_get_tile_image(codec.get(), t, true);
428+
if(!tileImg)
429+
{
430+
spdlog::error("get_tile_image({}) returned null after re-decompress", t);
431+
return {};
432+
}
433+
434+
TileData td;
435+
td.tileIndex = t;
436+
for(uint16_t c = 0; c < tileImg->numcomps; ++c)
437+
{
438+
auto& comp = tileImg->comps[c];
439+
if(!comp.data)
440+
continue;
441+
uint32_t w = comp.w;
442+
uint32_t h = comp.h;
443+
auto* pixelData = static_cast<int32_t*>(comp.data);
444+
std::vector<int32_t> pixels(pixelData, pixelData + w * h);
445+
td.componentData.push_back(std::move(pixels));
446+
}
447+
result.push_back(std::move(td));
448+
}
449+
450+
return result;
451+
}
452+
453+
static bool testLRUEvictionReDecompress(const std::string& testFile)
454+
{
455+
spdlog::info("=== Test: LRU eviction + re-decompress from cached SOT offsets ===");
456+
457+
// Reference: decompress each tile individually, no LRU
458+
auto refData = decompressTileByTile(testFile, GRK_TILE_CACHE_IMAGE, 0);
459+
if(refData.empty())
460+
{
461+
spdlog::error("Reference tile-by-tile decompression produced no data");
462+
return false;
463+
}
464+
spdlog::info("Reference: {} tiles captured", refData.size());
465+
466+
// Test: decompress with LRU, max_active_tiles=4 (forces eviction on 16-tile image)
467+
auto lruData = decompressTileByTile(testFile, GRK_TILE_CACHE_IMAGE | GRK_TILE_CACHE_LRU, 4);
468+
if(lruData.empty())
469+
{
470+
spdlog::error("LRU re-decompress produced no data");
471+
return false;
472+
}
473+
spdlog::info("LRU re-decompress: {} tiles captured", lruData.size());
474+
475+
if(refData.size() != lruData.size())
476+
{
477+
spdlog::error("Tile count mismatch: ref={}, lru={}", refData.size(), lruData.size());
478+
return false;
479+
}
480+
481+
for(size_t i = 0; i < refData.size(); ++i)
482+
{
483+
auto& ref = refData[i];
484+
auto& lru = lruData[i];
485+
if(ref.tileIndex != lru.tileIndex)
486+
{
487+
spdlog::error("Tile index mismatch at position {}", i);
488+
return false;
489+
}
490+
if(ref.componentData.size() != lru.componentData.size())
491+
{
492+
spdlog::error("Component count mismatch for tile {}", ref.tileIndex);
493+
return false;
494+
}
495+
for(size_t c = 0; c < ref.componentData.size(); ++c)
496+
{
497+
if(ref.componentData[c] != lru.componentData[c])
498+
{
499+
spdlog::error("Pixel data mismatch for tile {} component {}", ref.tileIndex, c);
500+
return false;
501+
}
502+
}
503+
}
504+
505+
spdlog::info("PASS: LRU eviction + re-decompress matches reference for all {} tiles",
506+
refData.size());
507+
return true;
508+
}
509+
365510
///////////////////////////////////////////////////////////////////
366511
// Main
367512
///////////////////////////////////////////////////////////////////
@@ -395,6 +540,24 @@ int GrkLRUCacheTest::main(int argc, char** argv)
395540
std::filesystem::remove(testFile, ec);
396541
}
397542

543+
// Integration test: non-TLM image with LRU eviction + re-decompress
544+
std::string noTlmFile =
545+
(std::filesystem::temp_directory_path() / "grk_lru_no_tlm_test.j2k").string();
546+
bool noTlmCreated = createTestImage(noTlmFile, 256, 256, 64, 64, false); // 4x4 = 16 tiles, no TLM
547+
if(!noTlmCreated)
548+
{
549+
spdlog::error("Failed to create non-TLM test image, skipping eviction test");
550+
failures++;
551+
}
552+
else
553+
{
554+
if(!testLRUEvictionReDecompress(noTlmFile))
555+
failures++;
556+
557+
std::error_code ec;
558+
std::filesystem::remove(noTlmFile, ec);
559+
}
560+
398561
if(failures > 0)
399562
{
400563
spdlog::error("{} test(s) FAILED", failures);

0 commit comments

Comments
 (0)