Skip to content

Commit dbd0e3e

Browse files
committed
Measure isolated cold benchmark workflows
1 parent 9cd9b5c commit dbd0e3e

9 files changed

Lines changed: 987 additions & 329 deletions

File tree

README.md

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ LiquidSpec.compile do |ctx, source, options|
213213
ctx[:template] = MyLiquid::Template.parse(source, **options)
214214
end
215215

216-
# Render the template compiled immediately above.
216+
# Render the template stored by compile or load_artifact.
217217
LiquidSpec.render do |ctx, assigns, options|
218218
# assigns = variables hash
219-
# options includes: :registers, :strict_errors, :error_mode
219+
# options includes: :registers, :strict_errors, :error_mode, :exception_renderer
220220
ctx[:template].render(assigns, **options)
221221
end
222222
```
@@ -248,28 +248,54 @@ liquid-spec run my_adapter.rb --json --list-passed > results.json
248248

249249
### Optional: compiled-artifact protocol
250250

251-
If your implementation can persist a compiled template as a string (e.g. an
252-
ISeq/bytecode blob stored in memcache or a database) and load it back in a
253-
process that never saw the source, declare both hooks:
251+
Some Liquid implementations compile source once, store executable bytecode or
252+
an equivalent compiled representation in a shared cache, and load those bytes
253+
in application processes that never receive the source. `compile` and `render`
254+
alone cannot measure that important production path: parsing again is not an
255+
artifact-cache hit, while repeatedly rendering a resident template omits the
256+
load and first-use costs.
257+
258+
If your implementation has such a persistent compiled format, declare both
259+
hooks:
254260

255261
```ruby
256-
# Serialize the compiled template (ctx[:template]) into a String
262+
# Called immediately after LiquidSpec.compile, before the template is rendered.
263+
# Return the exact binary String you would put in memcache, a database, etc.
257264
LiquidSpec.dump_artifact do |ctx|
258265
ctx[:template].to_artifact
259266
end
260267

261-
# Load an artifact string back into a renderable template
262-
LiquidSpec.load_artifact do |ctx, blob, options|
263-
ctx[:template] = MyLiquid::Artifact.load(blob)
268+
# Called in a runtime-loaded, template-cold fork that has the bytes but no source.
269+
# Restore all adapter state expected by the regular LiquidSpec.render hook.
270+
LiquidSpec.load_artifact do |ctx, bytes, _options|
271+
ctx[:template] = MyLiquid::Artifact.load(bytes)
264272
end
265273
```
266274

267-
The core `liquid-spec bench ADAPTER` command then adds an artifact stage per spec: it
268-
verifies that the dump → load → render roundtrip reproduces the compiled template's output,
269-
and measures payload bytes, cold artifact load time, first render after a
270-
cold load, and steady-state load time/allocations (the compile-once →
271-
persist → cold load+render production path). Adapters without these hooks
272-
are unaffected.
275+
The contract is deliberately production-oriented:
276+
277+
1. `dump_artifact` receives the state produced by `compile` and must return a
278+
binary-safe `String`. It is invoked before any validation render, because
279+
rendering is allowed to mutate template runtime state.
280+
2. The returned bytes must contain all immutable compile-time information needed
281+
to load the template without its source. Do not capture assigns, observed
282+
values, request objects, or render-time object shapes.
283+
3. `load_artifact` must leave `ctx` in the same renderable state that `compile`
284+
would. The source is unavailable. Its third argument is the runtime options
285+
Hash that the following `render` hook will receive; most loaders ignore it.
286+
4. Assigns, registers, and runtime filesystems are still supplied to the normal
287+
`render` hook for every call; they are not part of the artifact.
288+
289+
With both hooks, `--bench` validates the dump → load → render roundtrip, reports
290+
raw artifact bytes and steady-state load diagnostics, and measures the atomic
291+
artifact-load + first-render workflow in 30 forked children. The parent runtime
292+
is loaded but has never seen the benchmark template, and fork/IPC work is outside
293+
the timer. This models a shared artifact fetched into memory in another process.
294+
295+
`--bench` warns when either hook is missing because artifact size and
296+
load+first-render results will be omitted. Implementations without a persistent
297+
compiled format may leave the hooks absent; the warning documents that the
298+
benchmark is then limited to source compile and resident render paths.
273299

274300
### Optional: local suites
275301

lib/liquid/spec/cli/adapter_dsl.rb

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,30 @@ def render(&block)
163163
@render_block = block
164164
end
165165

166-
# OPTIONAL: define how to serialize the compiled template (ctx[:template])
167-
# into a persistable artifact string — the thing an implementation would
168-
# store in memcache/DB after compiling once.
169-
# Block receives: ctx. Must return a String.
166+
# OPTIONAL: export the just-compiled template as the exact binary String
167+
# an application would persist in a shared cache or database. This hook is
168+
# called immediately after compile, before render can mutate template state.
170169
#
171-
# Declaring BOTH dump_artifact and load_artifact enables the artifact
172-
# benchmark stage (`--bench`): payload size, cold artifact load time,
173-
# steady-state load time and allocations, plus a roundtrip correctness
174-
# check (the loaded artifact must render identical output).
170+
# The artifact must be usable without source code by another process running
171+
# the same implementation. Include all compile-time state needed by load.
172+
# Block receives ctx and must return a binary-safe String.
173+
#
174+
# Declaring BOTH artifact hooks enables payload-size and runtime-loaded,
175+
# template-cold load+first-render benchmarks. --bench warns when either is
176+
# absent because those production-path measurements would otherwise vanish.
175177
def dump_artifact(&block)
176178
@dump_artifact_block = block
177179
end
178180

179-
# OPTIONAL: define how to load an artifact string back into a renderable
180-
# template, storing it in ctx[:template] (the regular render block is then
181-
# used to render it — exactly the production cold path).
182-
# Block receives: ctx, blob, load_options.
181+
# OPTIONAL: import bytes produced by dump_artifact in a process that has
182+
# loaded the implementation but has never seen this template or its source.
183+
# Restore every adapter-context value that the regular render hook needs.
184+
#
185+
# Block receives ctx, artifact bytes, and the runtime options Hash that the
186+
# following render will receive. It must leave ctx ready for the normal
187+
# LiquidSpec.render hook. Runtime assigns,
188+
# registers, and filesystems still arrive through that render hook and must
189+
# not be captured in the artifact.
183190
def load_artifact(&block)
184191
@load_artifact_block = block
185192
end

0 commit comments

Comments
 (0)