Skip to content

Run lobsters with a fixed clock - #571

Open
thomasklemm wants to merge 1 commit into
ruby:mainfrom
thomasklemm:thomasklemm/lobsters-fixed-clock
Open

thomasklemm wants to merge 1 commit into
ruby:mainfrom
thomasklemm:thomasklemm/lobsters-fixed-clock

Conversation

@thomasklemm

@thomasklemm thomasklemm commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

This fixes Time.now in lobsters at 2026-07-01 12:00 UTC, during boot and in the benchmark block, to keep its work per iteration from depending on the date it runs. #572 is an alternative that fixes the clock at a different instant; merge one of the two.

On main, in lobsters' US Central time zone, when a lookback such as 70.days.ago crosses a DST change, ActiveSupport looks up the zone period again: 1,476 extra lookups per pass on 2026-11-05, none in July. On Dec 25 and Jun 28 the logo changes too. With the clock fixed, the tzinfo lookup counts were the same on every tested date.

Across the four tested dates, allocations per iteration varied by 0.01%, against 1.37% on main (Ruby 4.0.9, system clock set to each date):

2026-11-05 2026-12-25 2027-03-20 2027-06-28 spread
main 657,758 657,025 657,811 648,912 1.37%
this PR 648,889 648,916 648,913 648,872 0.01%

Response bodies are byte-identical between runs on 2026-11-05 and 2027-01-20, apart from CSRF tokens. The harness keeps the real clock. Date.today, DateTime.now, Time.new without arguments and SQLite's 'now' stay live. Cache entries and the session no longer age. TRACK_AR_TIME uses the monotonic clock.

For lobsters' history this is a one-time step of up to −1.35% objects against runs whose lookbacks cross a DST change (roughly November to January and mid-March to mid-June).

How this was measured

Objects: GC.stat(:total_allocated_objects) around each iteration of the default harness, WARMUP_ITRS=1 MIN_BENCH_ITRS=3, mean of the 3 measured iterations per date, spread = (max − min) / min of the per-date means, system clock set with libfaketime (FAKETIME='@<date> 12:00:00' FAKETIME_DONT_FAKE_MONOTONIC=1), cg_harness.rb with CG_DUMP=0. Responses: raw_dump.rb writes status, headers and body of all 114 requests in the pass after one warmup pass; raw_cmp.rb blanks the authenticity_token values, the csrf-token meta tag and the random headers (session cookie, ETag, X-Request-Id, X-Runtime) and compares the rest byte for byte: 0 of 114 differ. Lookups: dst_probe.rb.

Lobsters' work per iteration changed with the calendar date: in its US Central time zone, lookbacks such as 70.days.ago that cross a DST change make ActiveSupport look up the zone period again, and two dates switch the logo. Time.now now returns a fixed instant in July, from which no lookback (up to 90 days) crosses a DST change. Harness code outside the benchmark block, including harnesses that HARNESS=chain loads late, keeps the real clock. TRACK_AR_TIME measures with the monotonic clock.
@thomasklemm
thomasklemm force-pushed the thomasklemm/lobsters-fixed-clock branch from b63e9d8 to 33e9aa4 Compare November 5, 2026 20:32
@thomasklemm
thomasklemm marked this pull request as ready for review November 5, 2026 21:08
thomasklemm added a commit to thomasklemm/ruby-bench that referenced this pull request Nov 6, 2026
The Rails benchmarks here mostly read. None of them submits forms,
fails a validation, or runs after-commit callbacks and broadcasts;
lobsters' only writes are its read-ribbon bookkeeping. Real apps spend
much of their time saving records: validations, callbacks,
transactions, INSERT/UPDATE/DELETE and enqueuing jobs.

The app is Sam Ruby's create-blog from roundhouse-rb/roundhouse (MIT):
rails new --css tailwind on Rails 8.1.4.1, with Article and Comment
scaffolds, validations and Turbo broadcasts. It keeps the production
defaults of rails new: SQLite, Solid Cache, Solid Queue, Solid Cable,
Propshaft and importmap, no external services. The built Tailwind CSS
is checked in, so no CSS build runs. benchmarks/blog/README.md lists
the few changes to the generated tree.

Each iteration runs 200 seeded visits in-process, like railsbench:
reads (index, show, JSON) and writes (create, update and delete
articles, add and delete comments, a create that fails validation
with 422). Writes submit the CSRF token from the page's form, as a
browser does, and every response's status is checked, along with a
body marker or the redirect target. The iteration runs in one
transaction per database that is rolled back at the end, as in Rails
tests, so every iteration sees the same rows and IDs. Time.now is
fixed while the app runs, as proposed for lobsters in ruby#571.

Saves run as savepoints inside that transaction, and their
after-commit callbacks run when the savepoint is released, as in Rails
tests. Solid Cable inserts each broadcast in the request rather than
on its batching writer thread, so it lands in the transaction; the
later broadcasts are enqueued into Solid Queue. Since nothing commits,
a real COMMIT is not measured.
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