Skip to content

Php/resp bench - #23

Open
prateek-kumar-improving wants to merge 9 commits into
ikolomi:mainfrom
Bit-Quill:php/resp-bench
Open

prateek-kumar-improving wants to merge 9 commits into
ikolomi:mainfrom
Bit-Quill:php/resp-bench

Conversation

@prateek-kumar-improving

Copy link
Copy Markdown

No description provided.

* Fix broken Ruby gem dependency and stale configs, pin build inputs

Signed-off-by: James Xin <james.xin@improving.com>

* Resolve config paths without rewriting them; finish the gem rename

Addresses review findings on the initial commit.

Rewriting driver_config/workload_template to absolute paths made matrix
expansion depend on where the repo sits on disk: those strings double as
identity, since DimensionSpec.matches_driver() fnmatches applies_to globs
against the whole string and write_manifest() records it. A checkout under
any directory whose name contains "glide" made applies_to ["*glide*"] match
every driver, applying GLIDE-only thread env vars to Jedis, Lettuce and
Redisson. Validation and the runtime opens now share one resolve_config_path()
helper, so they agree by construction while the config strings survive
byte-for-byte. The new guard tests chdir off the repo root, without which
they passed against the buggy source.

Also: reject $binding values for driver_config at parse time, fold a
non-string path into the aggregated ValueError, drop a false comment about
detect_engine_for_driver() swallowing errors (generate_driver_config()
raises outside the guarded region, so a bad path crashes loudly), caveat
the requirements.txt pinning claim since transitives float, and finish the
gem rename in ruby/README.md and ValkeyGlideClient#driver_version, which
still looked up a Bundler spec name that repo never published.

Signed-off-by: James Xin <james.xin@improving.com>

* address comment: trim over-verbose dependency comments

Reviewer noted the AI-authored dep comments were too verbose/narrowly scoped.
Trimmed them across the Ruby Gemfile, gemspec, README, the driver_version
comment, and scripts/requirements.txt, keeping only the non-obvious bits
(the require "valkey" name mismatch and the numpy<2.5 CI constraint).
Lockfile follow-up tracked in #20. Comment/doc-only; no logic changes.

Signed-off-by: James Xin <james.xin@improving.com>

---------

Signed-off-by: James Xin <james.xin@improving.com>
Three failure-visibility problems made unattended sweeps unsafe: an
unrelated precondition aborted the whole run, separate runs silently
merged into the same files, and partial failure reported as success.

- Preflight resolves a server CLI (honouring $RESP_BENCH_CLI and
  $SERVER_PROJECT) and PINGs the endpoint with bounded retry before any
  benchmark work, passing auth/TLS flags from the driver config. The
  per-cell FLUSHALL moved inside the guarded region, so a transient
  failure costs one cell rather than the sweep. Recording-only matrices
  skip both the probe and the flush.
- Results are written under <output-dir>/<run-id>/, with --run-id
  defaulting to a UTC timestamp; a populated run directory is refused
  unless --resume or --overwrite is given. The orchestrator maintains an
  <output-dir>/latest symlink.
- _manifest.json records per-cell outcome (status, error summary,
  duration, records written) plus a planned/attempted/succeeded/failed
  summary, and the unconditional success message is gone.

Exit codes are now a contract: 0 = every attempted cell succeeded,
1 = the sweep finished with at least one failed cell, 2 = preflight
failed and nothing ran.

make benchmark-matrix-graphs is updated in the same change so main is
never left pointing the graph generator at the base output dir: it now
reads $(OUTPUT_DIR)/$(RUN_ID) with RUN_ID?=latest.

Signed-off-by: James Xin <james.xin@improving.com>
)

Signed-off-by: James Xin <james.xin@improving.com>
)

* Add Node.js benchmark engine (valkey-glide-node, ioredis, iovalkey)

TypeScript engine under node/, compiled with tsc to node/dist. One event loop,
one client per connection, one worker per connection; `pipeline_depth > 1` gives
each connection that many independent in-flight slots.

Drivers: `valkey-glide-node`, `ioredis`, `iovalkey` (+ `recording` for
server-free tests). The GLIDE id is deliberately not bare `valkey-glide` — that
is already Java's in the global DRIVER_ENGINE_MAP, and reusing it would reroute
Java's glide runs here.

Cross-engine parity verified against the Java reference, not assumed:

- Key sequences byte-identical: 79,000 keys diffed against Java's real
  KeyGenerator across both algorithms, 1-16 workers, prime keys_count, and
  tight prefix padding. javaRandom.ts uses BigInt because the 48-bit LCG
  multiply reaches ~2^83, past what a JS number holds exactly.
- HDR payloads decode in Java (org.HdrHistogram.Histogram) with matching count
  and percentiles. payload_b64 is encodeIntoCompressedBase64() used directly —
  it is already base64.
- summary.min/max use getValueAtPercentile(0/100), not minNonZeroValue/maxValue.
  Java returns bucket-equivalent bounds; the JS properties return the raw
  sample, and they diverge above ~1000us (50000us reads back as 50015 in Java).
- Request budget is shared across workers and claimed per request, matching
  Java's per-phase AtomicLong rather than pre-splitting it.
- PING does not consume a key, matching Java's PingCommand.

Node-specific fairness controls: ioredis/iovalkey auto-pipelining forced off
(it would batch same-tick commands and inflate throughput), reconnects disabled
(the default retries forever, hanging a run on a wrong host), uniform string
decoding across drivers, SET payloads allocated once, and sub-millisecond rate
limits yielding via setImmediate since setTimeout clamps to ~1ms.

Harness: Makefile targets, DRIVER_ENGINE_MAP, both graph scripts, driver
configs (default/high-throughput/example), schema examples, a benchmark-node CI
job, a Node.js block in infra/provision.sh, and config-editor driver list.

Docs: node/README.md and docs/BENCHMARKS_NODE.md, which records the measured
single-core ceiling (throughput plateaus ~42k rps at 200 connections; at
pipeline_depth 16 the process pins 99% of one core at ~72k rps, so past that
the engine and not the driver is the limit). Java solves this with parallel
issuer threads; the worker_threads equivalent is left as a follow-up to be
justified by measurement.

Also fixes the HDR range in docs/ADDING_LANGUAGE.md (3600000000 -> 600000000,
the value every engine actually uses) and extends its validation checklist with
the parity traps found here.

124 tests: unit (no server), full-engine tests via the recording driver, and
live-server tests per driver.

Signed-off-by: James Xin <james.xin@improving.com>

* Wire the Node engine into the sweep automation

Two gaps that made the engine unreachable from, or slow under, the matrix
orchestrator and the AWS runner.

Add configs/matrices/node-driver-comparison.json. Every shipped matrix was
Java-only, so although DRIVER_ENGINE_MAP resolves the Node drivers correctly,
no matrix actually exercised them — a default `bench-aws.sh` run would never
touch the Node engine. Mirrors driver-comparison-defaults.json in shape: three
drivers x five connection counts x five iterations.

Make `node-build` idempotent via a stamp file. The orchestrator calls
`make node-run` once per matrix cell, and `npm ci` deletes and reinstalls
node_modules every time it runs: measured ~40s of pure reinstall per cell, so
~15 minutes wasted on a 75-cell sweep, plus 75 opportunities for a network blip
to fail a cell mid-sweep on the AWS runner. `npm ci` now runs only when
package.json/package-lock.json change; `npm run build` still runs every time
because tsc is incremental and no-ops in under a second. Invocation cost drops
from ~40s to ~1s.

CI is unaffected — the workflow calls `npm ci && npm run build` directly, which
is the right thing on a clean machine.

Signed-off-by: James Xin <james.xin@improving.com>

* address comment: warmup allSettled, rate-limiter scope, exact dep pins

Three review items from #27.

Warmup used Promise.all, which rejects on the first failure while the remaining
warmup loops keep running unawaited — so executePhase's finally ran
closeClients() underneath them and they rejected against closed clients with
nobody awaiting. Switched to Promise.allSettled and rethrow the first rejection
once every loop has settled, preserving fail-fast with nothing left in flight.

RateLimiter was constructed before warmup. Its constructor sets
nextAllowedNanos to "now", so the whole warmup duration was banked as credit and
the workload issued (warmup_duration / interval) requests back-to-back before
pacing engaged — defeating the evenly-spaced, no-burst property the limiter
exists for. Now constructed after warmup. Added a regression test sized so the
burst would swallow the entire workload (~500ms warmup at a 20ms interval banks
~25 free requests; the phase issues 25), verified to fail at 676ms with the bug
present and pass at ~980ms without it.

Pinned all six dependencies exactly instead of using caret ranges, matching
Java/C#/Ruby and PR #5's "pin build inputs". Pinned to the versions already
resolved in the committed lockfile, so package-lock.json is unchanged and
npm ci still succeeds. This matters more for a benchmark than for ordinary
code: a caret range lets `npm install` quietly measure a different client build.

Reviewers: jeremyprime, Aryex.
Signed-off-by: James Xin <james.xin@improving.com>

* address comment: graph the Node results, and allow per-engine CI runs

Two gaps in the CI wiring, found while trying to exercise benchmark-node.

generate-graphs listed benchmark-node in `needs` and downloaded its artifacts,
but had no Node graph step — only Java and Ruby. Node results were collected and
then silently dropped. Added a Node step; DRIVER_LANGUAGE_MAP already maps the
three Node driver ids to "node", so nothing else was needed. One step rather
than three: the reference workload runs a single connection, and the Java/Ruby
"10/100 Clients" steps all re-read that same 1-connection glob, so the extra
copies would be duplicates of the same data.

The workflow also took no inputs, so validating one engine meant running all of
them — Java's 9 drivers plus Ruby's 2, each at 1M requests. Added an `engines`
choice input (all/java/ruby/node, default all) gating each engine job and each
graph step. generate-graphs needs `if: !cancelled()` because a skipped `needs`
job would otherwise skip it too, which would produce no graphs at all on an
engine-scoped run.

Default is `all`, so the existing behaviour is unchanged.

Signed-off-by: James Xin <james.xin@improving.com>

* address comment: allow --language node in generate_graphs.py

The Node graph step added in c780f89 failed in CI with:

  generate_graphs.py: error: argument --language: invalid choice: 'node'
  (choose from 'java', 'ruby', 'csharp', 'python')

My omission: the earlier commit added the three Node driver ids to
DRIVER_LANGUAGE_MAP but not to the argparse choices list, so --language node
was rejected before the map was ever consulted.

Added "node" to the choices and a comment noting the two lists must stay in
sync. Verified by re-running the exact failing command against the artifacts
from run 34914789751: 3 result records found, 9 graphs generated.

Signed-off-by: James Xin <james.xin@improving.com>

---------

Signed-off-by: James Xin <james.xin@improving.com>
Signed-off-by: Kumar <kupratec@amazon.com>
Signed-off-by: Kumar <kupratec@amazon.com>
Signed-off-by: Kumar <kupratec@amazon.com>
Resolve conflicts from the merged Node.js engine (#27), which touched the same
shared files as the PHP engine PR. All resolutions are additive — both engines
coexist:

- scripts/{run_benchmark_matrix,generate_graphs}.py: keep php + node driver maps
- Makefile: keep both PHP and Node engine target sections and .PHONY entries
- README.md: keep PHP and Node rows in languages/structure/target tables
- .github/workflows/benchmark.yml: keep test-php + benchmark-php and benchmark-node
  jobs; add 'php' to the engines input; generate-graphs needs all four engines
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.

2 participants