Skip to content

Add end-to-end rspec harness + benchmark analysis library - #2

Open
gillisd wants to merge 94 commits into
cli-polishfrom
e2e-rspec
Open

Add end-to-end rspec harness + benchmark analysis library#2
gillisd wants to merge 94 commits into
cli-polishfrom
e2e-rspec

Conversation

@gillisd

@gillisd gillisd commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Gives ready the one test class it structurally lacked: proof that a generated zsh stub, loaded into a live interactive shell, aliases the bare command and dispatches through the persistent by-server — plus the pure-Ruby engine for a cold-vs-hot startup waterfall.

Stacked on cli-polish (#1); diff is the 9 e2e commits only.

Phase A — functional E2E harness (:e2e, opt-in)

  • Ready::PtyShell — drives a real zsh -f -i under a pty; printed-marker sync (DO''NE quote-split) so command echo can't be mistaken for output; raises loudly on desync/timeout.
  • Ready::Sandbox — builds an isolated runtime in a temp prefix (ready up: compile stubs + boot by-server), exposes the env a pty needs to attach, and tears it all down (runs by-server stop while the socket is present — the path the rake task botches — then reaps by argv, then removes the tree). Drops a stale GEM_HOME/GEM_PATH left by a removed version manager so the build resolves from the project bundle.
  • spec/e2e/dispatch_spec.rb — sources the plugin against a live socket (readyinit fast path), asserts whence -v rakerake is an alias for ready_rake, and that rake --version dispatches through the by-server and returns the real version.
  • Opt-in wiring: :e2e excluded from the fast loop unless READY_E2E is set; rake spec:e2e task; a dedicated zsh-enabled e2e CI job (both Rubies). rake spec:e2e → 4 examples, 0 failures; zero orphaned servers.

Phase B — waterfall analysis library (fast suite)

  • Ready::Bench::Marks/Stats/Report — parse a marks log into a per-span millisecond cold-vs-hot waterfall with paired deltas and a pty cross-check. Unit-tested over fixture logs (no runtime deps). rake spec → 33 examples, 0 failures.

Zeitwerk

The harness is autoloaded via a second Zeitwerk::Loader for spec/support (namespace Ready) — no manual require glob. rake zeitwerk:validate still passes.

Follow-up (not in this PR)

Phase C — the live cold-vs-hot benchmark that produces real marks to feed Ready::Bench::Report — needs env-gated mark instrumentation in the gem runtime and is scoped as a separate, brainstorm-first change.

🤖 Generated with Claude Code

gillisd added 17 commits July 11, 2026 22:36
Add a second Zeitwerk loader for spec/support (namespace: Ready) and
exclude :e2e-tagged specs from the fast loop unless READY_E2E is set.
Builds an isolated ready runtime in a temp prefix, exposes the env a pty
needs to attach to the live by-server, and tears it down (by-server stop
while the socket is present, then reap by argv, then rm the tree). Drops a
stale GEM_HOME/GEM_PATH left by a removed version manager so the build
resolves gems from the project bundle.
Grounded in a measured per-layer investigation (irb, CLOCK_REALTIME both
sides). Additive design: instrumented copies on both arms, zero gem-runtime
changes, reusing Ready::Sandbox/Executable/Bench. rake bench + :e2e guard.
Replace the kamal-style SPANS with the ready launch layers (shell, rbenv_shim,
rubygems, dep_activate, tool_run, reap, dispatch_infra, full); add Aggregator
(min for process spans, median for in-process); retarget Report to `full`.
Dev-only fixtures; exclude bench/ and docs/ from the packaged gem.
Refine SPANS to launch/rubygems/dep_activate/tool_run (split Gem.activate_bin_path
from the tool require via a stub copy). Validated on the real irb stub:
launch 71 · dep_activate 34 · tool_run 94 · full 207 ms.
Injects server_entry + pre_tool marks into the real Ready::Executable render
output; builds a faithful ready_<exe> function that dispatches through ready_by.
Builds a warm sandbox (gems: preload), runs cold (instrumented copies) and hot
(ready dispatch) arms interleaved from a bundler-free shell, aggregates
min/median, derives the rbenv-shim addendum. Add gems: to Ready::Sandbox.
Measured: irb hot ~30ms vs cold ~258ms (~8x) on this container.
Structural, non-flaky: asserts cold pays dep_activate + tool_run, hot full <<
cold full, and hot skips the rbenv/boot layer. Reduced cold arm (no rbenv) so
it runs in the e2e CI job.
@gillisd

gillisd commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

Phase C added — live layered cold-vs-hot startup benchmark

rake bench (waterfall printer) + a :e2e structural guard. Attributes a rubygems-CLI startup to its layers (shell / rbenv_shim / launch / rubygems / dep_activate / tool_run / reap), cold (instrumented rbenv-shim + rubygems-stub copies + RUBYOPT prelude) vs hot (a stub from the real Ready::Executable#render, dispatched through a warm by-server). Zero production-runtime editslib//zsh/ untouched.

Measured on this container for irb: cold full ~208ms (+~50 rbenv = ~258ms) vs hot full ~29ms — ≈9× faster, every boot layer collapses. Notable talk beat: the hot residual is dominated by the by client being a full Ruby boot.

Design + methodology: docs/superpowers/specs/2026-07-12-phase-c-layer-benchmark-design.md.

gillisd added 8 commits July 12, 2026 04:12
The hot arm fed Ready::Executable a fully-resolved absolute path, hitting
Executable's on-disk branch, whose bin/ vs exe/ handling diverges (an exe/
path is read directly; a bin/ path collapses to a relative "bin/<name>").
Production `ready gem <exe>` never sees this — it passes the bare name and
resolves via Gem.bin_path, reading the real file regardless of bin/exe.

Mirror production: render the hot stub source by name in a scrubbed,
unbundled subprocess (the bench runs under this project's bundle, where the
target gem, e.g. ronin, is not bundled). Drops the exe/-symlink workaround
and generalizes the benchmark to any gem by name.

Verified: ronin (bin/) and irb (exe/) both render and dispatch; rubocop
clean; fast bench suite 10/0.
…t a no-op

The cold arm reported `rubygems ~0.2ms` — a hollow number. With a normal boot
the interpreter autoloads RubyGems before ruby_up (the RUBYOPT prelude) fires,
so the stub's `require "rubygems"` was a $LOADED_FEATURES no-op, and the real
~45ms of RubyGems init sat unlabeled inside the `launch` span.

Boot both shims with --disable-gems (accepted in RUBYOPT and propagated through
rbenv exec, both verified). Now ruby_up fires before RubyGems loads, the stub's
real `require "rubygems"` lands in the `rubygems` span, and `launch` collapses
to the true interpreter+shim cost. Total is preserved; rbenv_overhead stays
clean because both arms are rubygems-free at launch.

Result (ronin): launch 61->10ms, rubygems 0.3->45ms, full ~unchanged. This
matches the talk's rubygems-stub layer (and the `ruby --disable=gems -e
'require "rubygems"'` ~45ms measurement).
The prior comment said RubyGems is "autoloaded" at startup. That is wrong:
Kernel#autoload is lazy, constant-triggered loading, which is not how RubyGems
loads. MRI eagerly performs an implicit `require "rubygems"` during interpreter
init (gated by the default `gems` feature), before any RUBYOPT -r option.

--disable-gems does not change what runs: it's the same `require "rubygems"`
executing the same rubygems.rb (~45ms). It only moves the trigger from the
interpreter's implicit startup require (before ruby_up) to the stub's own
explicit require (after ruby_up), so the cost lands in a markable span. Reword
the comment to say exactly that.
…sion

Ready::CLI never included CommandKit::Options::Version, so --version fell
through to OptionParser's default ("ready: version unknown") and -V was
unregistered ("invalid option"). Include the mixin and set the version from
Ready::VERSION; both flags now print "ready <version>" and exit 0.
… gem

`ready up|compile|clobber` shell out to rake against the project Rakefile,
whose top requires dev-only tooling (bundler/gem_tasks, rspec, rubocop,
gempilot). bundler/gem_tasks runs Bundler::GemHelper.install_tasks at load,
which aborts with "Unable to determine name from existing gemspec" whenever the
rake cwd lacks exactly one gemspec — guaranteed for an installed gem, since the
gemspec is excluded from spec.files while the Rakefile ships.

None of that tooling is needed at runtime: the ready/compile/clobber/ready:*
tasks live in rakelib/ready.rake, which rake auto-loads independently of this
file. Guard the dev-only body on gemspec presence (a source checkout) so an
installed gem loads only the runtime tasks. Regression test spawns rake against
the Rakefile with no gemspec and asserts the abort no longer occurs.
…e server

readyinit set up the ready_* aliases but never exported BY_SOCKET, so the bare
`by` command (aliased to ready_by) fell back to the by client's default
~/.by_socket and failed with "No such file or directory - connect(2)". Gem
stubs bake BY_SOCKET inline per call, but the interactive `by` relies on the
env. Export BY_SOCKET=$READY_SOCK_PATH — the socket the server listens on and
the stubs use.
…stub

compile_by/compile_gem opened the target with File.open(target, "w"), which
truncates before `ready compile` runs. A failed compile then destroyed the
last-good stub and left a broken/partial file that rake treats as up to date on
later builds, so zsh loads garbage (e.g. a usage string) as the stub body and
errors on it. Write to a temp file and rename into place only on success;
remove the temp file on failure, leaving the previous stub intact.
…c compile

Revert the atomic temp-file-and-rename stub compilation (393b2a1). It added
complexity and a new failure surface (stray .tmp files) to make the build
corruption-proof, when clobber already is the reliable reset: `ready clobber`
removes the build dir so the next build regenerates every stub from scratch,
sidestepping the stale-mtime "up to date" trap entirely.

Instead, point failures at it: when readyup's compilation task fails, tell the
user to run `ready clobber` and rebuild. Loud failure + one reset button beats
whack-a-mole hardening.

@gillisd gillisd left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to stop reading because this lacks clarity around all the components, what their roles and behaviors are etc. And more comments is not the solution to this. You need to stop obsessing over primitive types and use real objects instead, that communicate behavior to the reader via proper naming and prose.

Comment thread bench/prof.zsh Outdated
# bench_envelope <run_id> -- <cmd...>
# Bare-read envelope_start (write deferred until after return so the ~48us log
# write never lands inside the span), run cmd, read envelope_end first.
bench_envelope() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Envelope? Why not use the word harness?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 1815be1: the function is bench_harness and the marks are harness_start/harness_end. No "envelope" remains in code, specs, or generated strings.

Comment thread bench/prof.zsh Outdated
export READY_RUN_ID=$rid
local __start=${EPOCHREALTIME}
"$@"
local __end=${EPOCHREALTIME}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare these two locals first, as empties so that there isn't any question that the harness is leaking time

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: local run_id= start_time= end_time= declared empty up front; between the two clock reads nothing runs but the command itself, and both log writes are deferred until after the run.

Comment thread spec/bench/aggregator_spec.rb Outdated
@@ -0,0 +1,20 @@
RSpec.describe Ready::Bench::Aggregator do
subject(:agg) { described_class.new(span_kind: { "shell" => :min, "tool_run" => :median }) }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using strings as keys? I am seeing that throughout this codebase

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbols end to end now — span labels and mark names are symbols everywhere. Strings exist only as text in the marks log file and are symbolized at the parse boundary (MarksLog#mark_times_for).

Comment thread spec/bench/aggregator_spec.rb Outdated
@@ -0,0 +1,20 @@
RSpec.describe Ready::Bench::Aggregator do
subject(:agg) { described_class.new(span_kind: { "shell" => :min, "tool_run" => :median }) }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unidiomatic - do not use "agg", abbreviated. write it out

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot — Aggregator is deleted entirely (see the #combine thread), so agg went with it. No abbreviated names remain in the bench code.

Comment thread spec/bench/aggregator_spec.rb Outdated
subject(:agg) { described_class.new(span_kind: { "shell" => :min, "tool_run" => :median }) }

it "takes the min for process spans and median for in-process spans" do
runs = [

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you calling these spans and then naming the variable runs? Explain to me what you are considering to be a Run vs a Span

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defined, in code: a Run is one timed invocation of the tool under one arm — cold.3 is round 3's cold run — holding the instant of every mark its layers recorded (run.rb). A Span is a named interval between two of those marks, carrying its own summary statistic (span.rb). The variable you flagged was the tell: combine received per-run span hashes and called them runs because no entity existed to say otherwise. Now MarksLog#run(id) returns a Run, Waterfall.of(run) measures its spans, and Waterfall.summarizing(waterfalls) collapses many runs.

Comment thread spec/support/bench/cold_arm.rb Outdated
def real_stub = Pathname(`rbenv which #{@exe}`.strip)

def instrument!
FileUtils.mkdir_p(@workdir)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include in this class

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went one further than the include: workdir.mkpath — Pathname already covers it, so the FileUtils dependency is deleted from the class entirely (consistent with the Pathname-everywhere direction). Say the word if you want include FileUtils instead and I will switch it.

Comment thread spec/support/bench/cold_arm.rb Outdated

def instrument!
FileUtils.mkdir_p(@workdir)
(@workdir / "stub").write(self.class.instrument_stub(real_stub.read))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not double nest method calls. separate into intermediate var for clarity

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split: write_instrumented_stub builds the stub in a named intermediate before writing, and RubygemsStub.for_executable resolves stub_path, guards an empty resolution with an explicit error (previously a bare Errno::ENOENT from File.read("")), then reads stub_source — no nested work-doing calls remain.

Comment thread spec/support/bench/cold_arm.rb Outdated
end

# Command word for a run: `<exe>` (rbenv arm) or `<exe>_direct`.
def command(direct:) = direct ? "#{@exe}_direct" : @exe

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why aren't you using polymorphism here? this is a textbook use case, and would make this all easier to follow

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is polymorphic now: abstract Shim owns the script skeleton (shim_start mark, RUBYOPT prelude arming, --disable-gems) as a template method, and RbenvShim/DirectShim supply command_word and exec_line — plus a post_initialize hook so the subclass never calls super. command(direct:) is gone; Runner asks a shim object for its command_word.

Comment thread spec/support/bench/cold_arm.rb Outdated

def write_shim(path, rbenv:)
prelude = Ready.root / "bench" / "prelude.rb"
target = if rbenv

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, major smell/antipattern of something that should be using polymorphism. You pass a primitive that could fall into 2 forms into a method, also pass its type, operate on that primitive, and make decisions about it based on its type. Needs a refactor.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_shim(path, rbenv:) is gone with the same refactor: each variant writes itself (shim.write!), so no primitive-plus-type crosses a method boundary anywhere in the arm. The only remaining conditionals select which object to use, never what a value means.

Comment thread spec/support/bench/marks.rb Outdated
runs = Hash.new { |h, k| h[k] = {} }
File.readlines(path).each do |line|
run, name, t = line.split
runs[run][name] = t.to_f

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop with the primitives

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested-hash plumbing is replaced by entities: MarksLog owns the file and symbolizes at the parse boundary, MarksLog#run(id) returns a Run that encapsulates its mark times (recorded?, milliseconds_between), Span#measure(run) asks the run rather than digging, and Waterfall holds the measured spans. Marks and its Hash.new { |h, k| h[k] = {} } are deleted.

gillisd added 3 commits July 12, 2026 18:39
vendor/ is bundler's BUNDLE_PATH install target (~4.4k files), .idea/ is
JetBrains config, and talk.tar.gz is a throwaway archive of already-tracked
files. None belong in git; ignore them so status stays legible.
The harness passed {String => Float} hashes everywhere and called them
different things in different places -- the review's core objection.
Name the domain instead:

- MarksLog/Run/Span/Waterfall/ArmResult replace Marks + Aggregator;
  span labels and mark names are symbols, and each Span declares its
  own summary statistic (an unknown one now raises)
- Shim + RbenvShim/DirectShim replace the boolean type-switching
  (write_shim(path, rbenv:), command(direct:))
- RubygemsStub owns the stub surgery; MarkHelper is the single injected
  ready_bench_mark snippet, replacing __bmark, __bp, and the inline
  prelude one-liner (bench/prelude.rb is now generated per-workdir)
- bench_envelope -> bench_harness with harness_start/harness_end marks;
  locals declared empty before the timed region
- Report owns all rendering including the rbenv overhead line; the pty
  cross-check now uses the real pty wall clock (was self-referential);
  rbenv: false now actually disables the rbenv probe (was ignored)
- specs rewritten in domain language; PtyShell spec uses subject

Also: dead real_shim/bench_mark deleted, the unused dep_activated mark
dropped, and the vendored references/ excluded from rubocop.
Per review direction, no File.expand_path/File.join: Configuration's
sock/readyfile defaults and Executable's require_relative rewrite now
join and expand via Pathname.
gillisd added 27 commits July 13, 2026 03:46
Set TargetRubyVersion to 3.4 (the gemspec's required_ruby_version, which CI
also exercises) so accidental 4.0-only syntax is caught instead of shipped,
and the Gemspec/RequiredRubyVersion warning clears. Because of this the
`require "pathname"` at the entry point is no longer flagged as redundant --
it is genuinely needed on 3.4, where Pathname is not autoloaded, so a bare
production install would NameError without it. Exclude extra/ (standalone
runtime helper scripts) as already done for references/.

Alongside the mechanical autocorrections: remove the dead
`render_environment_string` (no caller; the template inlines env), fix a
latent `raise ArgumentError "..."` missing its comma in fetch_env's untested
branch, and extract `resolve_fallback`/`validate_*!` to satisfy the metrics
cops without changing behavior.
Convert message-expectation mocks to spies (allow + have_received), add
:aggregate_failures where examples hold several checks, and replace trivial
`match(/.../)` assertions with exact `eq`/`include`. rake_command_spec stops
stubbing the object under test (RSpec/SubjectStub): it now drives a real
`rake` shell-out against a temp Rakefile and asserts interpreter, cwd, and
exit-status propagation for real. readyfile_spec drops its @ivar for a
build_dir helper. Every assertion is preserved or strengthened.
The sandbox runs `ready up` inside Bundler.with_unbundled_env (simulating a
real user's non-bundler install), which strips the bundle's bin path. With
bundler-cache's path install, by-server lives only there, so the daemon
spawn hit Errno::ENOENT. Install the pinned by (1.1.0) as a system gem --
the same place rbenv puts it locally and on macos-e2e -- so it survives the
unbundled env.
benchmark_spec runs the Runner with `rbenv: false` (the non-rbenv path the
ubuntu e2e job takes), but the flag was ignored below the Runner:
RubygemsStub.for_executable always shelled out to `rbenv which`, so the cold
arm died with Errno::ENOENT on any host without rbenv.

Thread `rbenv:` through ColdArm into RubygemsStub. Without rbenv, resolve the
same rubygems stub as `<ruby bindir>/<name>` -- which is exactly what
`rbenv which` returns when rbenv is present (verified identical for `ri`).
Also skip writing the rbenv shim when rbenv is off; it only exists to measure
the `rbenv exec` overhead the runner already probes only when `@rbenv`.
The benchmark's render_production_source runs `require "ready"` in a scrubbed,
unbundled subprocess (to resolve the target gem like a real install would),
and that needs zeitwerk -- ready's only non-default runtime require (verified:
the scrubbed process activates only zeitwerk plus default gems). With
bundler-cache it lives only in the bundle, so pin-install it system-wide
alongside by, both at their locked versions.
The waterfall table is exhaustive but not graspable at a glance. Add a
six-row summary under the headline -- shell, rbenv shim, rubygems
(interpreter boot folded in), activate deps, the tool, total -- each
backed by the cold arm's real measured span(s), for dropping onto a slide.
The slide summary mixed statistics -- layer rows used each span's median
while the total used the minimum floor (matching the headline). For a light
dependency graph the two nearly agreed, but a heavy one (ronin: activation
~350 ms) made the median layers overshoot the min-based total, so the parts
summed to more than the whole -- which reads as broken on a slide. Use each
layer's minimum floor, the same statistic the total uses, so the layers
always sum to at most the total.
The harness timed every run between its envelope marks no matter whether
the tool succeeded, and sent tool output to /dev/null -- so a tool that
crashed silently was recorded as a fast "success". Two fixes:

- Every measured command tees stdout+stderr to ./log/bench.log (HarnessLog)
  so a failure leaves evidence instead of vanishing.
- bench_harness records each run's exit status; a run that exits non-zero
  (or records none) aborts the whole benchmark, naming the run and pointing
  at the log, instead of producing a phantom measurement.

Verified end-to-end: ri TCPServer completes (16x), rake nosuchtaskxyz aborts
on hot.1 with "exited 1"; by propagates non-zero worker exits so the hot arm
is covered. Guarded against zsh's read-only `status` special (exit code
lands in `exit_code`).
When the persistent by-server has test/unit loaded, its at_exit auto-runner
parses the process ARGV -- so any option a ready'd CLI leaves in ARGV is
rejected ("invalid option: --format=ansi", printed as a Test::Unit usage
banner for by-server). A dispatched CLI is never a test run, so the rendered
stub now sets Test::Unit::AutoRunner.need_auto_run = false before the CLI's
source.

Reproduced: by-server with test/unit loaded + an option the CLI leaves in
ARGV -> the Test::Unit 'invalid option' banner. With the prologue line the
same dispatch runs clean.
This reverts c3f5e66. Injecting a test/unit-specific conditional into every
rendered CLI's core source is duct tape in the wrong layer -- the renderer
shouldn't know about test frameworks. The real cause is test/unit being loaded
into the persistent server; the fix is to not preload it (readyfile), not to
defensively disable its auto-runner in each stub.
The slide summary folded the VM boot (launch) into the rubygems row. That
makes 'ready slashes rubygems' untruthful: both arms boot a Ruby VM (ready's
by client boots one too), so lumping the boot into rubygems implies ready
eliminates the boot, which it does not. Give the boot its own 'ruby vm boot'
line; 'rubygems' is now just the framework load -- the cold-only part ready
actually eliminates.
Split the rbenv-shim example into two (the added 'ruby vm boot' label pushed
the layer-order assertion over RSpec/ExampleLength), and exclude the gitignored
log/ scratch dir (run logs + throwaway report-parsing scripts) from rubocop.
results.md: cold-vs-warm startup across 8 verified non-interactive Ruby CLIs,
with the ruby-vm-boot layer split out from rubygems, the rbenv-shim noise kept
separate, and the honest exclusions (rbs/rougify via __FILE__-under-eval,
rspec via the rspec-core resolver gap). readyfile: a worked ronin example
whose gems: entries are require paths (ronin, ronin/support), not gem names.
Redo the CLI benchmark with realistic workloads instead of --version:
ri (ansi doc render), colorls -l, ronin encode, youplot line, kamal
accessory tree, codeball pack. Every run is verified (exit-check plus a
hot-arm output cross-check), and the report splits ruby-vm-boot out from
rubygems. run_multibenchmark_test.sh reproduces it push-button (installs
gems incl. codeball's libmagic, builds shared fixtures, runs all six).
Bare 'ready compile' errored (insufficient arguments). Make the names
argument optional and treat an empty invocation the same as 'all', so
'ready compile' compiles every stub -- the way bare 'make' runs its
default target. Usage/examples/docs updated to show the default.
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