11# Cache Mechanism Analysis (2026-05-11)
22
3- How ` cheesy-gallery ` 1.1.1 caches work, how they sit on top of (and
3+ How ` cheesy-gallery ` 1.2.0 caches work, how they sit on top of (and
44sometimes around) Jekyll 4.4.1's own caching, where invalidation
55happens, and what happens once you put a ` git-annex ` worktree
66underneath it all.
77
8+ > ** Note for readers of v1.2+:** this doc was originally written for the
9+ > RMagick-backed v1.1.x. Layers A–C and the invalidation model are
10+ > unchanged under the libvips backend. ** Layer D** has been rewritten
11+ > below to describe libvips' operation cache instead of RMagick's
12+ > decoded pixel cache. Code snippets that mention ` Magick::Image.ping `
13+ > or ` Magick::ImageList.new ` reflect the historical implementation;
14+ > the current call sites are ` Vips::Image.new_from_file ` (replacing
15+ > the ping) and ` Vips::Image.thumbnail ` (replacing the decode+resize
16+ > pair, now fused). The cache-spec spy targets are updated to match —
17+ > see ` spec/cheesy/cache_spec.rb ` .
18+
819## TL;DR
920
1021- The plugin maintains ** two named ` Jekyll::Cache ` instances** that
@@ -209,25 +220,47 @@ re-read at write time — geometry is only used to populate
209220width=… height=…>` attributes. So Layer C does not gate I/O the way
210221Layer B does; it gates the per-image ` Magick::Image.ping ` call.
211222
212- ### 1.4 Layer D — RMagick's internal cache
223+ ### 1.4 Layer D — libvips operation cache
213224
214- When we * do* render (Layer A and B both miss), ` copy_file ` in
215- ` base_image_file.rb:49-62 ` runs:
225+ When we * do* render (Layer A and B both miss), the subclass's
226+ ` process_and_write ` runs ` Vips::Image.thumbnail(source_path, ...) `
227+ directly — libvips fuses decode + shrink-on-load + resize + (for
228+ thumbnails) centre-crop into a single operation. The base class no
229+ longer opens the source file itself; it just hands the source path
230+ to the subclass:
216231
217232``` ruby
218- source = Magick ::ImageList .new (path)
219- begin
220- process_and_write(source, dest_path)
221- ensure
222- source.destroy!
233+ # base_image_file.rb
234+ def copy_file (dest_path )
235+ Jekyll .logger.debug ' Rendering:' , dest_path
236+ process_and_write(path, dest_path)
237+ unless File .symlink?(dest_path)
238+ File .utime(self .class .mtimes[path], self .class .mtimes[path], dest_path)
239+ end
240+ @@render_cache [render_cache_key(dest_path)] = true
223241end
242+
243+ # image_file.rb#process_and_write (full-size)
244+ img = Vips ::Image .thumbnail(source_path, target_w, height: target_h,
245+ size: :down , crop: :none )
246+ img.write_to_file(dest_path, Q: @quality , interlace: true ,
247+ strip: true , optimize_coding: true )
224248```
225249
226- ` Magick::ImageList.new ` decodes the JPEG into a pixel cache. RMagick
227- keeps that decoded image in memory (and, depending on
228- ` MAGICK_TEMPORARY_PATH ` and ` MAGICK_DISK_LIMIT ` , on disk too) until
229- ` destroy! ` is called. The generator's * only* concession to this layer
230- is the ` collection.files.sort! ` on ` generator.rb:117 ` :
250+ What used to be "decode the entire JPEG into a pixel buffer and then
251+ resize" is now one library call that streams only the rows it needs
252+ (JPEG shrink-on-load at 1/2, 1/4, or 1/8 inside the codec, plus
253+ in-memory resize). There is ** no separate decoded-pixel cache** to
254+ size, and no ` destroy! ` lifecycle — the ` Vips::Image ` is freed by GC.
255+
256+ libvips does keep a small ** operation cache** of recently-compiled
257+ operation graphs (default ~ 100 entries; tunable via
258+ ` Vips.cache_set_max ` ). That's a process-local performance optimisation,
259+ not a correctness-affecting cache; it's transparent to our code and
260+ to the four-layer model above.
261+
262+ The generator's ` collection.files.sort! ` on ` generator.rb:117 ` still
263+ helps Layer D, but for a different reason now:
231264
232265``` ruby
233266# sort files by source path, so that we have better cache hits when
@@ -238,12 +271,14 @@ is the `collection.files.sort!` on `generator.rb:117`:
238271collection.files.sort! { |a , b | a.path <=> b.path }
239272```
240273
241- Sorting by source path keeps the full-size variant, the
242- ` *_thumb.jpg ` , and (if applicable) the ` *_index.jpg ` for the same
243- source next to each other in iteration order, so the underlying file
244- data is more likely to be hot in the OS page cache when each variant
245- opens it. There's no shared ` Magick::ImageList ` instance — each
246- subclass opens, decodes, processes, and destroys independently.
274+ Sorting by source path keeps the full-size variant and its thumb(s)
275+ adjacent in iteration order. Under libvips this benefits ** OS page
276+ cache locality** (each ` Vips::Image.thumbnail ` re-opens the file; the
277+ JPEG bytes are warm from the previous variant's open) and gives the
278+ libvips operation cache a better chance of reusing a previously-
279+ compiled graph. The TODO in the comment is now obsolete: there's no
280+ ` ImageList ` instance to share, because there's no separate decode
281+ step.
247282
248283This is the cache layer most affected by the choice of source-image
249284storage (local FS vs. ` git-annex ` -resolved symlink vs. networked
0 commit comments