react_on_rails_pro/scripts/load/README.md
A reproducible Ruby-based load and memory harness for the React on Rails Pro Rails → Node renderer transport. Designed to compare HTTPX vs async-http transports on correctness, latency, throughput, and memory growth.
This is the foundation PR. It ships:
standard_render, streaming_renderDeferred to follow-up issues:
ReactOnRails.addAsyncPropsCapabilityToComponentProps. That method is only available in the RSC bundle; the plain server bundle lacks it. The integration tests use special fixture bundles (react-on-rails-pro-node-renderer/tests/fixtures/bundle-incremental.js) with a ReactOnRails.getStreamValues() helper not present in production bundles. No runnable Ruby scenario ships until an RSC-capable test component or fixture is wired up.cd react_on_rails_pro/spec/dummy && bundle install).react_on_rails_pro/CLAUDE.md).react_on_rails_pro/spec/dummy/: pnpm run node-renderer (default port 3800).bin/rails runner, which boots Rails in-process to use the existing initializer config.All commands run from react_on_rails_pro/spec/dummy/:
bin/renderer-harness --smoke
Runs 10 standard-render requests, concurrency 1, no warmup. Exits 0 on success.
# Non-streaming render, 1000 requests across 4 threads
bin/renderer-harness --scenario standard_render --requests 1000 --concurrency 4 --warmup 5
# Streaming render, 60s with 4 threads
bin/renderer-harness --scenario streaming_render --duration 60 --concurrency 4
--warmup is per worker thread. For example, --warmup 5 --concurrency 4 issues 20 warmup
requests before measured requests begin. All workers must finish warmup before measurement starts;
--start-gate-timeout controls how long to wait for them (default: 30 seconds). If bundle upload is
slow on a cold renderer, adjust --upload-timeout (default: 10 seconds).
Issues #3582 and #3583 need benchmark evidence before any production renderer transport change. The benchmark-only transport probe starts a tiny local Node server with equivalent probe endpoints over:
node:http2 h2c over TCP loopback.node:http2 over a Unix domain socket.It does not change the production renderer, the Rails HTTP client, or the default h2c-over-TCP transport.
Run from react_on_rails_pro/ after workspace Node dependencies are installed:
bundle exec ruby scripts/load/transport_probe.rb --requests 3000 --warmup 300
Use Node >= 18, matching the Pro package requirement. The native stream probe
uses Node's Readable#iterator API.
--body-bytes is both the request payload size sent by each probe request and
the server-side body limit. For example, --body-bytes 1048576 sends a 1 MB
request body and rejects requests larger than 1 MB. Normal benchmark requests
send exactly the configured limit, so rejection testing requires a separate
oversized request rather than a standard probe run.
When comparing stream_response deltas, remember that Fastify enforces
bodyLimit before the handler sees the request, while the native HTTP/2 stream
handler reads the request body before writing the response. Treat that difference
as part of the benchmark interpretation rather than production transport proof.
For environments where Unix sockets are unavailable:
bundle exec ruby scripts/load/transport_probe.rb --skip-uds
The probe writes transport_probe_summary.json under
tmp/load-tests/transport-probe/<timestamp>/. Treat this as Phase 0 evidence
only: a production UDS transport still needs Linux/topology validation against
the real renderer harness (bin/renderer-harness) before adding socket-path
configuration or runtime transport plumbing.
Summary JSON records the latency-delta baseline in the top-level baseline
field. fastify_tcp is preferred when selected; if it is omitted, the probe
falls back to native_tcp as the baseline.
To include the node-renderer RSS in memory.csv, pass its PID:
RENDERER_PID=$(pgrep -f "node-renderer")
bin/renderer-harness --scenario standard_render --requests 1000 --renderer-pid $RENDERER_PID
Each run writes to tmp/load-tests/<UTC-timestamp>/ (gitignored):
summary.json — config, env, aggregates, memory slopelatency.csv — per-request samplesmemory.csv — time-series of RSS + GC.statA summary block prints to the terminal at the end.
The harness reads REACT_ON_RAILS_RENDERER_TRANSPORT (default httpx) and records it in summary.json. This PR does not switch transports — the async-http branch will add an async_http value when it merges. To compare:
# baseline
REACT_ON_RAILS_RENDERER_TRANSPORT=httpx bin/renderer-harness --scenario streaming_render --duration 60
# (after async-http branch is checked out)
REACT_ON_RAILS_RENDERER_TRANSPORT=async_http bin/renderer-harness --scenario streaming_render --duration 60
ps -o rss= units are kB on both macOS and Linux but reporting is best-effort; if the process is gone mid-sample, the row is omitted (not zero-filled).config/initializers/react_on_rails_pro.rb).Thread#kill only because renderer-harness is a
short-lived CLI. Replace those paths with cooperative cancellation before
embedding the harness in a long-lived process.cd react_on_rails_pro && bundle exec rspec spec/load/).RUN_RENDERER_LOAD_SMOKE=1; it is not wired into any default workflow in this PR.--upload-timeout, usually indicating an unresponsive node renderer.