From 5de4eb7eb75773a17d1c3ada2c7c95a99e0efba6 Mon Sep 17 00:00:00 2001 From: Dusan Malusev Date: Mon, 13 Jul 2026 20:58:17 +0200 Subject: [PATCH] test(bench): add PhpBench benchmark suite (offline + live) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single hand-rolled persistent_sessions_bench.php with a structured PhpBench suite split into two groups: - offline (no DB, CI-friendly): value marshalling (Uuid/Bigint/Decimal/ Varint/Timestamp/Inet/Blob), collection build+read scaled by element count, blob construct/readback across payload sizes, statement/batch construction, and the cluster-builder config path (persistent ON vs OFF). - live (needs a running ScyllaDB): connect (persistent vs transient), simple vs prepared execute, single vs batched writes, result paging at varying page sizes, and async pipelining vs sequential execution. Every subject reports both wall-clock time and peak memory — for an extension a stray copy or leak surfaces in mem_peak first. Live cases open one session per iteration in a @BeforeMethods hook so only the operation under test is timed. A benchmarks/run.sh wrapper autodetects a local out/ build and passes the right --php-binary/--php-config; composer bench* scripts and a README cover running, live setup, and baseline/diff regression tracking. Adds phpbench/phpbench as a dev dependency and a Cassandra\Benchmarks autoload namespace. Co-Authored-By: Claude Opus 4.8 (cherry picked from commit ef0e6653550c98319da60b34559792a2cff73eaa) --- .gitignore | 8 + benchmarks/Live/AsyncBench.php | 91 +++ benchmarks/Live/ConnectBench.php | 44 ++ benchmarks/Live/ExecuteBench.php | 90 +++ benchmarks/Live/PagingBench.php | 95 +++ benchmarks/Live/WriteBench.php | 84 +++ benchmarks/Offline/BlobBench.php | 48 ++ benchmarks/Offline/ClusterBuilderBench.php | 61 ++ benchmarks/Offline/CollectionBench.php | 94 +++ benchmarks/Offline/StatementBench.php | 55 ++ benchmarks/Offline/ValueBench.php | 117 ++++ benchmarks/README.md | 132 ++++ benchmarks/Support/Env.php | 58 ++ benchmarks/Support/LiveBenchCase.php | 81 +++ benchmarks/Support/Sizes.php | 34 + benchmarks/bootstrap.php | 46 ++ benchmarks/persistent_sessions_bench.php | 236 ------- benchmarks/run.sh | 49 ++ composer.json | 13 +- composer.lock | 729 +++++++++++++++++++-- phpbench.json | 13 + 21 files changed, 1881 insertions(+), 297 deletions(-) create mode 100644 benchmarks/Live/AsyncBench.php create mode 100644 benchmarks/Live/ConnectBench.php create mode 100644 benchmarks/Live/ExecuteBench.php create mode 100644 benchmarks/Live/PagingBench.php create mode 100644 benchmarks/Live/WriteBench.php create mode 100644 benchmarks/Offline/BlobBench.php create mode 100644 benchmarks/Offline/ClusterBuilderBench.php create mode 100644 benchmarks/Offline/CollectionBench.php create mode 100644 benchmarks/Offline/StatementBench.php create mode 100644 benchmarks/Offline/ValueBench.php create mode 100644 benchmarks/README.md create mode 100644 benchmarks/Support/Env.php create mode 100644 benchmarks/Support/LiveBenchCase.php create mode 100644 benchmarks/Support/Sizes.php create mode 100644 benchmarks/bootstrap.php delete mode 100644 benchmarks/persistent_sessions_bench.php create mode 100755 benchmarks/run.sh create mode 100644 phpbench.json diff --git a/.gitignore b/.gitignore index f5d0c702b..de653b2a5 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,11 @@ src/*_descriptor.c tools/gen_stub/PHP-Parser-*/ cmake-build/ cmake-build-*/ + +# PhpBench result storage (baselines / history) +/.phpbench/ + +# Benchmark sources are always tracked, even when a name matches a broad +# build-cruft glob above (e.g. `ex*.php` catches Live/ExecuteBench.php on a +# case-insensitive filesystem). +!/benchmarks/** diff --git a/benchmarks/Live/AsyncBench.php b/benchmarks/Live/AsyncBench.php new file mode 100644 index 000000000..fade475c6 --- /dev/null +++ b/benchmarks/Live/AsyncBench.php @@ -0,0 +1,91 @@ +ensureSchema([ + sprintf( + 'CREATE TABLE IF NOT EXISTS %s.%s (id uuid PRIMARY KEY, payload text)', + $keyspace, + self::TABLE, + ), + ]); + + $this->seedId = new Uuid('11111111-1111-1111-1111-111111111111'); + $this->session->execute( + new SimpleStatement('INSERT INTO ' . self::TABLE . ' (id, payload) VALUES (' . $this->seedId . ', ?)'), + ['arguments' => ['hello']], + ); + + $this->select = $this->session->prepare('SELECT id, payload FROM ' . self::TABLE . ' WHERE id = ?'); + } + + public function benchSequential(array $params): void + { + for ($i = 0, $n = $params['requests']; $i < $n; $i++) { + $rows = $this->session->execute($this->select, ['arguments' => [$this->seedId]]); + foreach ($rows as $_) { + } + } + } + + public function benchPipelined(array $params): void + { + /** @var list $futures */ + $futures = []; + for ($i = 0, $n = $params['requests']; $i < $n; $i++) { + $futures[] = $this->session->executeAsync($this->select, ['arguments' => [$this->seedId]]); + } + + foreach ($futures as $future) { + foreach ($future->get() as $_) { + } + } + } + + /** @return iterable */ + public function provideConcurrency(): iterable + { + yield '16' => ['requests' => 16]; + yield '64' => ['requests' => 64]; + yield '256' => ['requests' => 256]; + } +} diff --git a/benchmarks/Live/ConnectBench.php b/benchmarks/Live/ConnectBench.php new file mode 100644 index 000000000..3d052a9d8 --- /dev/null +++ b/benchmarks/Live/ConnectBench.php @@ -0,0 +1,44 @@ +connect(persistent: false)->close(); + } + + #[Bench\Revs(20)] + #[Bench\Warmup(1)] + public function benchConnectPersistent(): void + { + // close() returns the session to the persistent pool; the next build() + // with the same config fingerprint reuses it. + $this->connect(persistent: true)->close(); + } +} diff --git a/benchmarks/Live/ExecuteBench.php b/benchmarks/Live/ExecuteBench.php new file mode 100644 index 000000000..fcb8499dd --- /dev/null +++ b/benchmarks/Live/ExecuteBench.php @@ -0,0 +1,90 @@ +ensureSchema([ + sprintf( + 'CREATE TABLE IF NOT EXISTS %s.%s ' + . '(id uuid PRIMARY KEY, kind text, payload text, created bigint)', + $keyspace, + self::TABLE, + ), + ]); + + // Seed a single deterministic row so every SELECT returns exactly one. + $this->seedId = new Uuid('62c36092-82a1-3a00-93d1-46196ee77204'); + $this->session->execute( + new SimpleStatement(sprintf( + 'INSERT INTO %s (id, kind, payload, created) VALUES (%s, ?, ?, ?)', + self::TABLE, + (string) $this->seedId, + )), + ['arguments' => ['seed', 'hello', new Bigint(1)]], + ); + + $this->simpleSelect = new SimpleStatement('SELECT id, kind, payload FROM ' . self::TABLE . ' LIMIT 1'); + $this->preparedSelect = $this->session->prepare('SELECT id, kind, payload FROM ' . self::TABLE . ' LIMIT 1'); + $this->preparedById = $this->session->prepare('SELECT id, kind, payload FROM ' . self::TABLE . ' WHERE id = ?'); + } + + public function benchSimpleSelect(): void + { + $this->drain($this->session->execute($this->simpleSelect)); + } + + public function benchPreparedSelect(): void + { + $this->drain($this->session->execute($this->preparedSelect)); + } + + public function benchPreparedSelectBound(): void + { + $this->drain($this->session->execute($this->preparedById, ['arguments' => [$this->seedId]])); + } + + private function drain(iterable $rows): void + { + foreach ($rows as $_) { + // materialise every row so decoding is included in the measurement + } + } +} diff --git a/benchmarks/Live/PagingBench.php b/benchmarks/Live/PagingBench.php new file mode 100644 index 000000000..356751a17 --- /dev/null +++ b/benchmarks/Live/PagingBench.php @@ -0,0 +1,95 @@ +ensureSchema([ + sprintf( + 'CREATE TABLE IF NOT EXISTS %s.%s (bucket int, id int, payload text, PRIMARY KEY (bucket, id))', + $keyspace, + self::TABLE, + ), + ]); + + $this->seedOnce(); + $this->scan = new SimpleStatement('SELECT bucket, id, payload FROM ' . self::TABLE . ' WHERE bucket = 0'); + } + + #[Bench\ParamProviders('providePageSizes')] + public function benchFullScan(array $params): void + { + $rows = $this->session->execute($this->scan, ['page_size' => $params['page_size']]); + $this->walk($rows); + } + + private function walk(Rows $rows): void + { + while (true) { + foreach ($rows as $_) { + // decode every row on the page + } + if ($rows->isLastPage()) { + break; + } + $rows = $rows->nextPage(); + } + } + + private function seedOnce(): void + { + $existing = $this->session + ->execute(new SimpleStatement('SELECT COUNT(*) AS c FROM ' . self::TABLE . ' WHERE bucket = 0')) + ->first(); + + if ((int) ($existing['c'] ?? 0) >= self::ROWS) { + return; + } + + $insert = $this->session->prepare('INSERT INTO ' . self::TABLE . ' (bucket, id, payload) VALUES (0, ?, ?)'); + for ($i = 0; $i < self::ROWS; $i++) { + $this->session->execute($insert, ['arguments' => [$i, 'payload-' . $i]]); + } + } + + /** @return iterable */ + public function providePageSizes(): iterable + { + yield '50' => ['page_size' => 50]; + yield '250' => ['page_size' => 250]; + yield '1000' => ['page_size' => 1000]; + } +} diff --git a/benchmarks/Live/WriteBench.php b/benchmarks/Live/WriteBench.php new file mode 100644 index 000000000..5940004c0 --- /dev/null +++ b/benchmarks/Live/WriteBench.php @@ -0,0 +1,84 @@ +ensureSchema([ + sprintf( + 'CREATE TABLE IF NOT EXISTS %s.%s ' + . '(id uuid PRIMARY KEY, kind text, payload text, created bigint)', + $keyspace, + self::TABLE, + ), + ]); + + $this->insert = $this->session->prepare( + 'INSERT INTO ' . self::TABLE . ' (id, kind, payload, created) VALUES (?, ?, ?, ?)', + ); + } + + #[Bench\Revs(50)] + public function benchSingleInsert(): void + { + $this->session->execute($this->insert, [ + 'arguments' => [new Uuid(), 'evt', 'payload', new Bigint(1)], + ]); + } + + #[Bench\Revs(10)] + #[Bench\ParamProviders('provideBatchSizes')] + public function benchBatchInsert(array $params): void + { + $batch = new BatchStatement(Cassandra::BATCH_UNLOGGED); + + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $batch->add($this->insert, [new Uuid(), 'evt', 'payload', new Bigint($i)]); + } + + $this->session->execute($batch); + } + + /** @return iterable */ + public function provideBatchSizes(): iterable + { + yield '10' => ['count' => 10]; + yield '50' => ['count' => 50]; + yield '100' => ['count' => 100]; + } +} diff --git a/benchmarks/Offline/BlobBench.php b/benchmarks/Offline/BlobBench.php new file mode 100644 index 000000000..a6d7d95f8 --- /dev/null +++ b/benchmarks/Offline/BlobBench.php @@ -0,0 +1,48 @@ +payloads[$params['bytes']] ??= random_bytes($params['bytes']); + } + + #[Bench\Revs(300)] + #[Bench\BeforeMethods('setUp')] + public function benchConstruct(array $params): void + { + new Blob($this->payloads[$params['bytes']]); + } + + #[Bench\Revs(300)] + #[Bench\BeforeMethods('setUp')] + public function benchRoundTrip(array $params): void + { + (new Blob($this->payloads[$params['bytes']]))->bytes(); + } +} diff --git a/benchmarks/Offline/ClusterBuilderBench.php b/benchmarks/Offline/ClusterBuilderBench.php new file mode 100644 index 000000000..2ccc35941 --- /dev/null +++ b/benchmarks/Offline/ClusterBuilderBench.php @@ -0,0 +1,61 @@ +withContactPoints('127.0.0.1') + ->withPort(9042) + ->withTokenAwareRouting(true) + ->withConnectTimeout(5.0) + ->withRequestTimeout(12.0) + ->withDefaultConsistency(Cassandra::CONSISTENCY_LOCAL_ONE); + } + + public function benchBuildPersistentOff(): void + { + $this->configure(false)->build(); + } + + public function benchBuildPersistentOn(): void + { + $this->configure(true)->build(); + } + + private function configure(bool $persistent): \Cassandra\Cluster\Builder + { + return Cassandra::cluster() + ->withContactPoints('127.0.0.1') + ->withPort(9042) + ->withPersistentSessions($persistent) + ->withTokenAwareRouting(true) + ->withConnectTimeout(5.0) + ->withRequestTimeout(12.0) + ->withDefaultConsistency(Cassandra::CONSISTENCY_LOCAL_ONE); + } +} diff --git a/benchmarks/Offline/CollectionBench.php b/benchmarks/Offline/CollectionBench.php new file mode 100644 index 000000000..3f2dfb1e6 --- /dev/null +++ b/benchmarks/Offline/CollectionBench.php @@ -0,0 +1,94 @@ +add($i); + } + } + + #[Bench\Revs(200)] + public function benchSetBuildAndRead(array $params): void + { + $set = new Set(Cassandra::TYPE_INT); + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $set->add($i); + } + $set->values(); + } + + #[Bench\Revs(200)] + public function benchMapBuild(array $params): void + { + $map = new Map(Cassandra::TYPE_TEXT, Cassandra::TYPE_INT); + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $map->set('k' . $i, $i); + } + } + + #[Bench\Revs(200)] + public function benchListBuild(array $params): void + { + $list = new Collection(Cassandra::TYPE_TEXT); + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $list->add('v' . $i); + } + } + + #[Bench\Revs(200)] + public function benchTupleBuild(array $params): void + { + // Tuples are fixed-arity; scale by repeating a 2-field tuple $count times. + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $tuple = new Tuple([Cassandra::TYPE_INT, Cassandra::TYPE_TEXT]); + $tuple->set(0, $i); + $tuple->set(1, 'x'); + } + } + + #[Bench\Revs(200)] + public function benchUdtBuild(array $params): void + { + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $udt = new UserTypeValue([ + 'id' => Cassandra::TYPE_INT, + 'name' => Cassandra::TYPE_TEXT, + ]); + $udt->set('id', $i); + $udt->set('name', 'x'); + } + } +} diff --git a/benchmarks/Offline/StatementBench.php b/benchmarks/Offline/StatementBench.php new file mode 100644 index 000000000..8f3eca5bc --- /dev/null +++ b/benchmarks/Offline/StatementBench.php @@ -0,0 +1,55 @@ +cql); + } + + #[Bench\Revs(20000)] + public function benchEmptyBatch(): void + { + new BatchStatement(Cassandra::BATCH_UNLOGGED); + } + + #[Bench\Revs(500)] + #[Bench\ParamProviders('provideElementCounts')] + public function benchBatchAdd(array $params): void + { + $batch = new BatchStatement(Cassandra::BATCH_UNLOGGED); + $stmt = new SimpleStatement($this->cql); + + for ($i = 0, $n = $params['count']; $i < $n; $i++) { + $batch->add($stmt, [new Uuid(), 'evt', 'payload', $i]); + } + } +} diff --git a/benchmarks/Offline/ValueBench.php b/benchmarks/Offline/ValueBench.php new file mode 100644 index 000000000..44e871c81 --- /dev/null +++ b/benchmarks/Offline/ValueBench.php @@ -0,0 +1,117 @@ + extension object -> canonical string. + * + * These are the per-value costs paid on every bound parameter and every + * decoded column. They touch no network, so they run in the `offline` group + * with high revolution counts for tight confidence intervals. Watch the + * `mem_peak` column as closely as time — value objects that leak or over- + * allocate show up here before they ever reach a query. + */ +#[Bench\BeforeMethods('setUp')] +#[Bench\Groups(['offline', 'value'])] +#[Bench\Revs(10000)] +#[Bench\Iterations(5)] +#[Bench\Warmup(2)] +#[Bench\OutputTimeUnit('microseconds', precision: 3)] +final class ValueBench +{ + private string $uuidString = '756716f7-2e54-4715-9f00-91dcbea6cf50'; + private string $bigNumber = '9223372036854775807'; + private string $decimal = '3.14159265358979323846'; + private string $ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; + + public function setUp(): void + { + // Nothing to warm; kept so every subject shares one before-hook shape. + } + + public function benchUuidGenerate(): void + { + new Uuid(); + } + + public function benchUuidFromString(): void + { + new Uuid($this->uuidString); + } + + public function benchUuidRoundTrip(): void + { + (new Uuid($this->uuidString))->uuid(); + } + + public function benchTimeuuidGenerate(): void + { + new Timeuuid(); + } + + public function benchBigint(): void + { + (new Bigint($this->bigNumber))->value(); + } + + public function benchSmallint(): void + { + (new Smallint(32767))->value(); + } + + public function benchTinyint(): void + { + (new Tinyint(127))->value(); + } + + public function benchVarint(): void + { + (new Varint($this->bigNumber))->value(); + } + + public function benchDecimal(): void + { + (new Decimal($this->decimal))->value(); + } + + public function benchDecimalArithmetic(): void + { + $a = new Decimal($this->decimal); + $a->add($a)->__toString(); + } + + public function benchTimestamp(): void + { + (new Timestamp(1_700_000_000, 500_000))->__toString(); + } + + public function benchDuration(): void + { + (new Duration(1, 2, 3_000_000_000))->__toString(); + } + + public function benchInet(): void + { + (new Inet($this->ipv6))->address(); + } + + public function benchBlobSmall(): void + { + (new Blob('scylladb-php-driver'))->bytes(); + } +} diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..aaaf8e647 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,132 @@ +# Benchmarks + +Performance suite for the `cassandra` extension, driven by +[PhpBench](https://phpbench.readthedocs.io/). It is split into two groups: + +| Group | Needs a DB? | What it measures | +|-----------|-------------|------------------| +| `offline` | no | Pure-CPU cost of the extension: value marshalling, statement/collection construction, and the cluster-builder config path. Deterministic and CI-friendly. | +| `live` | yes | Real round-trip behaviour against a running ScyllaDB/Cassandra: connect, execute (simple vs prepared), writes/batches, result paging, and async pipelining. | + +Every subject reports **wall-clock time** (`mode`, with `rstdev` as the spread) +and **peak memory** (`mem_peak`). For the extension, memory is as important as +time — an extra copy or a leak on a hot path shows up in `mem_peak` first. + +## Layout + +``` +benchmarks/ +├── bootstrap.php # autoload + "is the extension loaded?" guard +├── run.sh # wrapper that points PhpBench at a local build +├── Support/ +│ ├── Env.php # SCYLLADB_* connection config (shared with tests) +│ ├── Sizes.php # shared param providers (element counts, byte sizes) +│ └── LiveBenchCase.php # base class: builder, connect, schema setup +├── Offline/ +│ ├── ValueBench.php # Uuid/Bigint/Decimal/Varint/Timestamp/Inet/Blob … +│ ├── CollectionBench.php # Set/Map/List/Tuple/UDT build + read, scaled by size +│ ├── BlobBench.php # blob construct/readback across payload sizes +│ ├── StatementBench.php # SimpleStatement / BatchStatement construction +│ └── ClusterBuilderBench.php# builder chain + build(); persistent ON vs OFF +└── Live/ + ├── ConnectBench.php # build()+connect()+close(), persistent ON vs OFF + ├── ExecuteBench.php # simple vs prepared SELECT, bound lookup + ├── WriteBench.php # single insert vs UNLOGGED batch (scaled) + ├── PagingBench.php # full scan at different page sizes + └── AsyncBench.php # executeAsync pipelining vs sequential execute +``` + +## Running + +The extension must be loaded in the PHP that PhpBench spawns. `benchmarks/run.sh` +handles that for a local dev build (module under `out/…`) by autodetecting the +`.so`/`.dylib` and passing the right flags; if the extension is installed +system-wide it just relies on `phpbench.json` (`extension=cassandra`). + +```bash +composer install # first time — pulls in phpbench + +benchmarks/run.sh --group=offline # offline micro-benchmarks (no DB) +benchmarks/run.sh --group=live # live benchmarks (needs a node) +benchmarks/run.sh # everything +benchmarks/run.sh benchmarks/Live/AsyncBench.php # a single file +benchmarks/run.sh --filter=benchPrepared # a single subject +``` + +Point it at a specific build or PHP with env vars: + +```bash +EXT=out/RelWithDebInfoPHP8.4NTS/cassandra.dylib \ +PHP=php/8.4-debug-nts/bin/php \ +benchmarks/run.sh --group=offline +``` + +The `composer bench*` scripts do the same via `phpbench` directly and assume the +extension is installed by name (best for CI): + +```bash +composer bench # aggregate report, all groups +composer bench:offline +composer bench:live +``` + +### Live benchmarks — bring up a node + +```bash +./scripts/run-scylladb.sh # docker compose up (localhost:9042) +benchmarks/run.sh --group=live +``` + +Connection settings come from the same environment variables as the test suite, +all optional: + +| Variable | Default | +|----------|---------| +| `SCYLLADB_HOSTS` | `127.0.0.1` | +| `SCYLLADB_PORT` | `9042` | +| `SCYLLADB_USERNAME` | `cassandra` | +| `SCYLLADB_PASSWORD` | `cassandra` | +| `SCYLLADB_BENCH_KEYSPACE` | `bench_scylladb` | + +The live group creates the `bench_scylladb` keyspace (`IF NOT EXISTS`) and reuses +it. It is **not** dropped automatically — dropping it would race across +PhpBench's per-iteration subprocesses. Remove it when you are done: + +```bash +benchmarks/run.sh --group=live # ... then: +php -r '/* your one-off */' # or from cqlsh: +# DROP KEYSPACE IF EXISTS bench_scylladb; +``` + +## Tracking regressions with baselines + +PhpBench can store a run and diff against it — this is how you catch a +performance regression in a PR. + +```bash +# 1. On the base commit, snapshot a baseline: +benchmarks/run.sh --group=offline --store --tag=baseline + +# 2. On your branch, compare against it: +benchmarks/run.sh --group=offline --ref=baseline --report=aggregate +``` + +With `--ref`, the `aggregate` report annotates every `mode`/`mem_peak`/`rstdev` +cell with the percentage delta versus the baseline, so a regression is a +red `+NN%` in the time column. Baselines live under `.phpbench/` (git-ignored). +Keep the offline +group as the regression gate: it is deterministic, whereas live timings depend on +the node, network, and load. + +## How it is measured + +- PhpBench runs each **iteration** in its own subprocess and loops the + configured **revolutions** inside it; the reported time is per revolution. +- Offline subjects use high revolution counts (fast, pure-CPU) for tight + confidence intervals. Parameterised subjects (`set` column) sweep a size axis + so non-linear cost is obvious. +- Live subjects open one session per iteration in a `@BeforeMethods` hook, so the + connection cost is paid once and amortised; only the operation under test is + timed. Revolution counts are low because a round trip dwarfs any local work. +- Watch `rstdev`: under a few percent is trustworthy. A noisy live number usually + means a busy or contended node, not a code change. diff --git a/benchmarks/Support/Env.php b/benchmarks/Support/Env.php new file mode 100644 index 000000000..8e1650ad7 --- /dev/null +++ b/benchmarks/Support/Env.php @@ -0,0 +1,58 @@ + */ + public static function hosts(): array + { + $hosts = self::str('SCYLLADB_HOSTS', '127.0.0.1'); + + return array_values(array_filter(array_map('trim', explode(',', $hosts)))); + } + + public static function port(): int + { + return self::int('SCYLLADB_PORT', 9042); + } + + public static function username(): string + { + return self::str('SCYLLADB_USERNAME', 'cassandra'); + } + + public static function password(): string + { + return self::str('SCYLLADB_PASSWORD', 'cassandra'); + } + + /** Keyspace the live benchmarks create (IF NOT EXISTS) and reuse. */ + public static function keyspace(): string + { + return self::str('SCYLLADB_BENCH_KEYSPACE', 'bench_scylladb'); + } + + public static function str(string $key, string $default): string + { + $value = $_SERVER[$key] ?? $_ENV[$key] ?? getenv($key); + + return $value === false || $value === null || $value === '' ? $default : (string) $value; + } + + public static function int(string $key, int $default): int + { + $value = $_SERVER[$key] ?? $_ENV[$key] ?? getenv($key); + + return $value === false || $value === null || $value === '' ? $default : (int) $value; + } +} diff --git a/benchmarks/Support/LiveBenchCase.php b/benchmarks/Support/LiveBenchCase.php new file mode 100644 index 000000000..3a33ed545 --- /dev/null +++ b/benchmarks/Support/LiveBenchCase.php @@ -0,0 +1,81 @@ +withContactPoints(...Env::hosts()) + ->withPort(Env::port()) + ->withCredentials(Env::username(), Env::password()) + ->withPersistentSessions($persistent) + ->withTokenAwareRouting(true); + } + + protected function connect(?string $keyspace = null, bool $persistent = false): Session + { + $cluster = static::builder($persistent)->build(); + + return $keyspace !== null && $keyspace !== '' + ? $cluster->connect($keyspace) + : $cluster->connect(); + } + + /** + * Create the benchmark keyspace (IF NOT EXISTS) plus any table DDL the + * subclass needs, then leave $this->session connected to that keyspace. + * + * @param list $tableDdl fully-formed `CREATE TABLE IF NOT EXISTS` + * statements, keyspace-qualified by $keyspace. + */ + protected function ensureSchema(array $tableDdl = []): void + { + $keyspace = Env::keyspace(); + + $admin = $this->connect(persistent: false); + $admin->execute(new SimpleStatement(sprintf( + "CREATE KEYSPACE IF NOT EXISTS %s WITH replication = " + . "{'class': 'SimpleStrategy', 'replication_factor': 1}", + $keyspace, + ))); + + foreach ($tableDdl as $ddl) { + $admin->execute(new SimpleStatement($ddl)); + } + + $admin->close(); + + $this->session = $this->connect($keyspace, persistent: false); + } + + public function closeSession(): void + { + $this->session?->close(); + $this->session = null; + } +} diff --git a/benchmarks/Support/Sizes.php b/benchmarks/Support/Sizes.php new file mode 100644 index 000000000..606495666 --- /dev/null +++ b/benchmarks/Support/Sizes.php @@ -0,0 +1,34 @@ + */ + public function provideElementCounts(): iterable + { + yield '1' => ['count' => 1]; + yield '10' => ['count' => 10]; + yield '100' => ['count' => 100]; + yield '1000' => ['count' => 1000]; + } + + /** @return iterable */ + public function provideByteSizes(): iterable + { + yield '64B' => ['bytes' => 64]; + yield '1KiB' => ['bytes' => 1024]; + yield '64KiB' => ['bytes' => 64 * 1024]; + yield '1MiB' => ['bytes' => 1024 * 1024]; + } +} diff --git a/benchmarks/bootstrap.php b/benchmarks/bootstrap.php new file mode 100644 index 000000000..e14b46791 --- /dev/null +++ b/benchmarks/bootstrap.php @@ -0,0 +1,46 @@ += 1.4) is not loaded.\n"); - fwrite(STDERR, "Build it, then run with: php -d extension=cassandra " . basename(__FILE__) . "\n"); - exit(1); -} - -$HOST = getenv('SCYLLA_HOST') ?: '127.0.0.1'; -$PORT = (int) (getenv('SCYLLA_PORT') ?: 9042); -$USER = getenv('SCYLLA_USER') ?: null; -$PASS = getenv('SCYLLA_PASS') ?: null; -$N_SESS = (int) (getenv('BENCH_SESSIONS') ?: 200); -$N_QUERY = (int) (getenv('BENCH_QUERIES') ?: 2000); -$KEYSPACE = 'bench_1_4_0'; - -echo "scylladb-php-driver benchmark\n"; -echo " driver version : " . (defined('Cassandra::VERSION') ? Cassandra::VERSION : 'unknown') . "\n"; -echo " php : " . PHP_VERSION . " (" . (ZEND_THREAD_SAFE ? 'ZTS' : 'NTS') . ")\n"; -echo " target : {$HOST}:{$PORT}\n"; -echo " keyspace : {$KEYSPACE}\n\n"; - -/** - * Build a cluster with a fixed config, toggling only persistent sessions. - * The point of the bench is that the cache-key work inside build() differs; - * everything else about the config is held constant. - */ -function makeCluster(bool $persistent): \Cassandra\Cluster -{ - global $HOST, $PORT, $USER, $PASS; - - $builder = Cassandra::cluster() - ->withContactPoints($HOST) - ->withPort($PORT) - ->withPersistentSessions($persistent) - ->withTokenAwareRouting(true) - ->withConnectTimeout(5.0) - ->withRequestTimeout(12.0) - ->withDefaultConsistency(Cassandra::CONSISTENCY_LOCAL_ONE); - - if ($USER !== null) { - $builder = $builder->withCredentials($USER, $PASS ?? ''); - } - - return $builder->build(); -} - -function fmt(float $seconds): string -{ - if ($seconds < 1e-3) { - return sprintf('%6.1f us', $seconds * 1e6); - } - if ($seconds < 1.0) { - return sprintf('%6.2f ms', $seconds * 1e3); - } - return sprintf('%6.3f s ', $seconds); -} - -function mib(int $bytes): string -{ - return sprintf('%6.2f MiB', $bytes / (1024 * 1024)); -} - -// --- connectivity check + schema setup --------------------------------------- - -try { - $boot = makeCluster(false)->connect(); - $boot->execute(new \Cassandra\SimpleStatement( - "CREATE KEYSPACE IF NOT EXISTS {$KEYSPACE} " . - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}" - )); - $boot->execute(new \Cassandra\SimpleStatement( - "CREATE TABLE IF NOT EXISTS {$KEYSPACE}.events (" . - " id uuid PRIMARY KEY, kind text, payload text, created bigint)" - )); - $boot->close(); -} catch (\Throwable $e) { - fwrite(STDERR, "Could not reach ScyllaDB at {$HOST}:{$PORT}: {$e->getMessage()}\n"); - fwrite(STDERR, "Start a node (see the header comment) and try again.\n"); - exit(1); -} - -// --- bench 1: persistent-session cache --------------------------------------- -// -// Loop build()+connect()+close() N times. With persistent sessions ON, the -// second build() onward hits the persistent_list cache (the uint64 fingerprint -// path). With OFF, every build() constructs a fresh CassCluster. We report -// wall-clock ops/sec and, more tellingly for 1.4.0, peak-memory growth. - -function benchSessions(bool $persistent, int $n): array -{ - // Prime once so the very first connect (TCP + control connection) isn't - // counted against the loop we care about. - makeCluster($persistent)->connect()->close(); - - $memBefore = memory_get_peak_usage(true); - $t0 = hrtime(true); - - for ($i = 0; $i < $n; $i++) { - $session = makeCluster($persistent)->connect(); - // one trivial query so the session actually does work - $session->execute(new \Cassandra\SimpleStatement('SELECT now() FROM system.local')); - $session->close(); - } - - $elapsed = (hrtime(true) - $t0) / 1e9; - $memAfter = memory_get_peak_usage(true); - - return [ - 'elapsed' => $elapsed, - 'per_op' => $elapsed / $n, - 'ops_sec' => $n / $elapsed, - 'mem_growth' => $memAfter - $memBefore, - ]; -} - -echo "── bench 1: build()+connect() x {$N_SESS} ───────────────────────────\n"; - -$off = benchSessions(false, $N_SESS); -printf(" persistent OFF : %s total %s/op %8.0f ops/s peak+%s\n", - fmt($off['elapsed']), fmt($off['per_op']), $off['ops_sec'], mib($off['mem_growth'])); - -$on = benchSessions(true, $N_SESS); -printf(" persistent ON : %s total %s/op %8.0f ops/s peak+%s\n", - fmt($on['elapsed']), fmt($on['per_op']), $on['ops_sec'], mib($on['mem_growth'])); - -$speedup = $off['per_op'] / max($on['per_op'], 1e-12); -printf(" → persistent ON is %.2fx faster per build+connect, peak-mem growth %s vs %s\n\n", - $speedup, mib($on['mem_growth']), mib($off['mem_growth'])); - -// --- bench 2: prepared vs simple statement ----------------------------------- - -echo "── bench 2: SELECT x {$N_QUERY} (prepared vs simple) ────────────────\n"; - -$session = makeCluster(true)->connect($KEYSPACE); - -// seed one row to select -$seedId = new \Cassandra\Uuid(); -$session->execute(new \Cassandra\SimpleStatement( - "INSERT INTO events (id, kind, payload, created) VALUES (" . - $seedId->uuid() . ", 'seed', 'hello', 1)" -)); - -$simpleCql = "SELECT id, kind, payload FROM events LIMIT 1"; - -$t0 = hrtime(true); -for ($i = 0; $i < $N_QUERY; $i++) { - $rows = $session->execute(new \Cassandra\SimpleStatement($simpleCql)); - foreach ($rows as $_) { /* drain */ } -} -$simpleElapsed = (hrtime(true) - $t0) / 1e9; - -$prepared = $session->prepare($simpleCql); -$t0 = hrtime(true); -for ($i = 0; $i < $N_QUERY; $i++) { - $rows = $session->execute($prepared); - foreach ($rows as $_) { /* drain */ } -} -$preparedElapsed = (hrtime(true) - $t0) / 1e9; - -printf(" simple : %s total %8.0f q/s\n", fmt($simpleElapsed), $N_QUERY / $simpleElapsed); -printf(" prepared : %s total %8.0f q/s\n", fmt($preparedElapsed), $N_QUERY / $preparedElapsed); -printf(" → prepared is %.2fx the throughput of re-parsed simple statements\n\n", - $simpleElapsed / max($preparedElapsed, 1e-12)); - -// --- bench 3: batch insert throughput ---------------------------------------- - -echo "── bench 3: batched INSERT (100 rows/batch x 20) ────────────────────\n"; - -$insert = $session->prepare("INSERT INTO events (id, kind, payload, created) VALUES (?, ?, ?, ?)"); -$batches = 20; -$perBatch = 100; - -$t0 = hrtime(true); -for ($b = 0; $b < $batches; $b++) { - $batch = new \Cassandra\BatchStatement(Cassandra::BATCH_UNLOGGED); - for ($r = 0; $r < $perBatch; $r++) { - $batch->add($insert, [new \Cassandra\Uuid(), 'evt', str_repeat('x', 64), (int) (hrtime(true) / 1000)]); - } - $session->execute($batch); -} -$batchElapsed = (hrtime(true) - $t0) / 1e9; -$totalRows = $batches * $perBatch; - -printf(" %d rows in %s → %8.0f rows/s\n\n", $totalRows, fmt($batchElapsed), $totalRows / $batchElapsed); - -// --- teardown ---------------------------------------------------------------- - -$session->execute(new \Cassandra\SimpleStatement("DROP KEYSPACE IF EXISTS {$KEYSPACE}")); -$session->close(); - -echo "done. peak memory this process: " . mib(memory_get_peak_usage(true)) . "\n"; diff --git a/benchmarks/run.sh b/benchmarks/run.sh new file mode 100755 index 000000000..85ce4cdeb --- /dev/null +++ b/benchmarks/run.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# Convenience wrapper around `vendor/bin/phpbench run`. +# +# For a locally-built extension (the .so/.dylib lives under out/ rather than +# being installed in the PHP extension_dir) PhpBench needs to be told both which +# PHP to spawn and where the module is. This script autodetects a dev build and +# supplies the right --php-binary / --php-config flags; when the extension is +# installed system-wide it adds nothing and lets phpbench.json drive. +# +# Usage: +# benchmarks/run.sh # everything, aggregate report +# benchmarks/run.sh --group=offline # offline micro-benchmarks only +# benchmarks/run.sh --group=live # live (needs a running ScyllaDB) +# benchmarks/run.sh --store --tag=before # snapshot a baseline +# benchmarks/run.sh --ref=before --report=aggregate --report=diff +# +# Overrides: +# PHP=/path/to/php PHP binary to run benchmarks with +# EXT=/path/to/cassandra.so extension module to load +# SCYLLADB_HOSTS, SCYLLADB_PORT, SCYLLADB_USERNAME, SCYLLADB_PASSWORD, +# SCYLLADB_BENCH_KEYSPACE live-benchmark connection settings + +set -euo pipefail + +cd "$(dirname "$0")/.." + +PHP_BIN="${PHP:-php}" + +# Locate a locally-built module unless one was passed explicitly. +if [[ -z "${EXT:-}" ]]; then + EXT="$(find out -maxdepth 2 \( -name 'cassandra.so' -o -name 'cassandra.dylib' \) 2>/dev/null \ + | xargs -r ls -t 2>/dev/null | head -n1 || true)" +fi + +ARGS=(--php-binary="$PHP_BIN") +if [[ -n "${EXT:-}" && -f "$EXT" ]]; then + ARGS+=(--php-config="{\"extension\":\"$(cd "$(dirname "$EXT")" && pwd)/$(basename "$EXT")\"}") + echo "→ using local extension: $EXT" >&2 +else + echo "→ no local build found under out/; relying on installed 'cassandra' extension" >&2 +fi + +# Default to the aggregate report when the caller passes no report/action flags. +if [[ $# -eq 0 ]]; then + set -- --report=aggregate +fi + +exec vendor/bin/phpbench run "${ARGS[@]}" "$@" diff --git a/composer.json b/composer.json index ea492826f..3a8c9cc24 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "require-dev": { "pestphp/pest": "^3.8", "nesbot/carbon": "^3.5", - "symfony/process": "^7.0" + "symfony/process": "^7.0", + "phpbench/phpbench": "^1.4" }, "config": { "platform": { @@ -29,12 +30,20 @@ }, "autoload-dev": { "psr-4": { - "Cassandra\\Tests\\": "./tests" + "Cassandra\\Tests\\": "./tests", + "Cassandra\\Benchmarks\\": "./benchmarks" }, "files": [ "tests/Support/helpers.php" ] }, + "scripts": { + "bench": "phpbench run --report=aggregate", + "bench:offline": "phpbench run --group=offline --report=aggregate", + "bench:live": "phpbench run --group=live --report=aggregate", + "bench:baseline": "phpbench run --tag=baseline --progress=none --store", + "bench:diff": "phpbench run --ref=baseline --report=aggregate" + }, "php-ext": { "extension-name": "cassandra", "configure-options": [ diff --git a/composer.lock b/composer.lock index ee1992041..b73fde21f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9e85c5af27ffeece28b074e89d2575b7", + "content-hash": "8b2227c1a98d219a610490e93df484e7", "packages": [], "packages-dev": [ { @@ -169,6 +169,83 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "abandoned": true, + "time": "2024-09-05T10:17:24+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -217,6 +294,83 @@ }, "time": "2026-02-07T07:09:04+00:00" }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, { "name": "fidry/cpu-core-counter", "version": "1.3.0", @@ -1257,6 +1411,156 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpbench/container", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", + "shasum": "" + }, + "require": { + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.89", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.2.3" + }, + "time": "2025-11-06T09:05:13+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe", + "reference": "3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^8.2", + "phpbench/container": "^2.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "seld/jsonlint": "^1.1", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", + "webmozart/glob": "^4.6" + }, + "require-dev": { + "dantleech/invoke": "^2.0", + "ergebnis/composer-normalize": "^2.39", + "jangregor/phpstan-prophecy": "^2.0", + "php-cs-fixer/shim": "^3.9", + "phpspec/prophecy": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.2", + "sebastian/exporter": "^6.3.2", + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "files": [ + "lib/Report/Func/functions.php" + ], + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "keywords": [ + "benchmarking", + "optimization", + "performance", + "profiling", + "testing" + ], + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.7.0" + }, + "funding": [ + { + "url": "https://github.com/dantleech", + "type": "github" + } + ], + "time": "2026-06-08T19:09:20+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1936,6 +2240,55 @@ ], "time": "2026-01-27T05:59:18+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -3124,6 +3477,70 @@ ], "time": "2024-10-09T05:16:32+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2026-06-12T11:32:29+00:00" + }, { "name": "staabm/side-effects-detector", "version": "1.0.5", @@ -3256,16 +3673,16 @@ }, { "name": "symfony/console", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", "shasum": "" }, "require": { @@ -3330,7 +3747,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.8" + "source": "https://github.com/symfony/console/tree/v7.4.14" }, "funding": [ { @@ -3350,20 +3767,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -3376,7 +3793,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3401,7 +3818,77 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -3412,25 +3899,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "13b38720174286f55d1761152b575a8d1436fc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", + "reference": "13b38720174286f55d1761152b575a8d1436fc25", "shasum": "" }, "require": { @@ -3465,7 +3956,78 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T08:31:18+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" }, "funding": [ { @@ -3489,7 +4051,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -3548,7 +4110,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -3572,16 +4134,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.36.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -3630,7 +4192,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -3650,20 +4212,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.36.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -3715,7 +4277,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -3735,20 +4297,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.36.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -3800,7 +4362,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -3820,7 +4382,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php83", @@ -3904,16 +4466,16 @@ }, { "name": "symfony/process", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -3945,7 +4507,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.8" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -3965,20 +4527,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -3996,7 +4558,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4032,7 +4594,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -4052,20 +4614,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/string", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "114ac57257d75df748eda23dd003878080b8e688" + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", - "reference": "114ac57257d75df748eda23dd003878080b8e688", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", "shasum": "" }, "require": { @@ -4123,7 +4685,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.8" + "source": "https://github.com/symfony/string/tree/v7.4.13" }, "funding": [ { @@ -4143,7 +4705,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-23T15:23:29+00:00" }, { "name": "symfony/translation", @@ -4497,6 +5059,55 @@ "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, "time": "2026-04-11T10:33:05+00:00" + }, + { + "name": "webmozart/glob", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/glob.git", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/8a2842112d6916e61e0e15e316465b611f3abc17", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/filesystem": "^5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Glob\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozarts/glob/issues", + "source": "https://github.com/webmozarts/glob/tree/4.7.0" + }, + "time": "2024-03-07T20:33:40+00:00" } ], "aliases": [], @@ -4505,7 +5116,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.2|^8.3|^8.4|^8.5", + "php": "^8.3|^8.4|^8.5", "ext-date": "*" }, "platform-dev": {}, diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..2ca0fe316 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,13 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "benchmarks/bootstrap.php", + "runner.path": "benchmarks", + "runner.file_pattern": "*Bench.php", + "runner.php_config": { + "extension": "cassandra" + }, + "runner.iterations": 5, + "runner.warmup": 1, + "runner.retry_threshold": 5.0, + "storage.xml_storage_path": ".phpbench/storage" +}