Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/**
91 changes: 91 additions & 0 deletions benchmarks/Live/AsyncBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Cassandra\Benchmarks\Live;

use Cassandra\FutureRows;
use Cassandra\PreparedStatement;
use Cassandra\SimpleStatement;
use Cassandra\Uuid;
use Cassandra\Benchmarks\Support\Env;
use Cassandra\Benchmarks\Support\LiveBenchCase;
use PhpBench\Attributes as Bench;

/**
* Async pipelining vs sequential execution at a fixed request count.
*
* Both subjects issue the same number of point reads. benchSequential blocks
* on each execute() before starting the next, so its latency is N * round_trip.
* benchPipelined fires all executeAsync() first, then drains the futures, so
* in-flight requests overlap and latency approaches round_trip + N * server
* cost. The ratio between them is the real-world win from the async API and
* the driver's request coalescing.
*/
#[Bench\BeforeMethods('setUp')]
#[Bench\AfterMethods('closeSession')]
#[Bench\Groups(['live', 'async'])]
#[Bench\Revs(5)]
#[Bench\Iterations(3)]
#[Bench\Warmup(1)]
#[Bench\ParamProviders('provideConcurrency')]
#[Bench\OutputTimeUnit('milliseconds', precision: 3)]
final class AsyncBench extends LiveBenchCase
{
private const TABLE = 'bench_async';

private PreparedStatement $select;
private Uuid $seedId;

public function setUp(): void
{
$keyspace = Env::keyspace();

$this->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<FutureRows> $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<string, array{requests: int}> */
public function provideConcurrency(): iterable
{
yield '16' => ['requests' => 16];
yield '64' => ['requests' => 64];
yield '256' => ['requests' => 256];
}
}
44 changes: 44 additions & 0 deletions benchmarks/Live/ConnectBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Cassandra\Benchmarks\Live;

use Cassandra\Benchmarks\Support\LiveBenchCase;
use PhpBench\Attributes as Bench;

/**
* Session establishment cost, persistent sessions ON vs OFF.
*
* Each revolution does a full build() + connect() + close(). With persistent
* sessions OFF every revolution spins up a fresh CassSession (control
* connection, host discovery). With them ON, the first revolution in the
* iteration's subprocess populates EG(persistent_list) and the rest reuse the
* cached CassCluster/CassSession keyed by the 1.4.0 uint64 fingerprint — so
* the ON subject should be dramatically cheaper from the second rev onward,
* and its mem_peak should stay flat.
*
* Revolution counts are deliberately low: connect() is orders of magnitude
* more expensive than an offline value construction.
*/
#[Bench\Groups(['live', 'connect'])]
#[Bench\Iterations(3)]
#[Bench\OutputTimeUnit('milliseconds', precision: 3)]
final class ConnectBench extends LiveBenchCase
{
#[Bench\Revs(10)]
#[Bench\Warmup(1)]
public function benchConnectTransient(): void
{
$this->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();
}
}
90 changes: 90 additions & 0 deletions benchmarks/Live/ExecuteBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Cassandra\Benchmarks\Live;

use Cassandra\Bigint;
use Cassandra\PreparedStatement;
use Cassandra\SimpleStatement;
use Cassandra\Uuid;
use Cassandra\Benchmarks\Support\Env;
use Cassandra\Benchmarks\Support\LiveBenchCase;
use PhpBench\Attributes as Bench;

/**
* Read-path throughput: simple vs prepared, and the cost of binding.
*
* A SimpleStatement is re-parsed server-side on every execute; a
* PreparedStatement is parsed once and only bound values travel afterwards.
* The gap between benchSimpleSelect and benchPreparedSelect is the headline
* "why prepare" number. benchPreparedSelectBound adds positional binding so
* the marshalling cost is visible on top of the round trip.
*/
#[Bench\BeforeMethods('setUp')]
#[Bench\AfterMethods('closeSession')]
#[Bench\Groups(['live', 'execute'])]
#[Bench\Revs(50)]
#[Bench\Iterations(5)]
#[Bench\Warmup(1)]
#[Bench\OutputTimeUnit('microseconds', precision: 3)]
final class ExecuteBench extends LiveBenchCase
{
private const TABLE = 'bench_events';

private SimpleStatement $simpleSelect;
private PreparedStatement $preparedSelect;
private PreparedStatement $preparedById;
private Uuid $seedId;

public function setUp(): void
{
$keyspace = Env::keyspace();

$this->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
}
}
}
95 changes: 95 additions & 0 deletions benchmarks/Live/PagingBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Cassandra\Benchmarks\Live;

use Cassandra\Rows;
use Cassandra\SimpleStatement;
use Cassandra\Benchmarks\Support\Env;
use Cassandra\Benchmarks\Support\LiveBenchCase;
use PhpBench\Attributes as Bench;

/**
* Result paging: full scan of a fixed row set at different page sizes.
*
* The table is seeded once (idempotently) to ROWS rows. Each subject walks the
* entire result set, pulling the next page synchronously until the last. A
* small page size means more round trips; a large one means fewer, bigger
* decodes. Sweeping page_size shows where the round-trip/decoding trade-off
* sits for this schema and cluster.
*/
#[Bench\BeforeMethods('setUp')]
#[Bench\AfterMethods('closeSession')]
#[Bench\Groups(['live', 'paging'])]
#[Bench\Revs(10)]
#[Bench\Iterations(3)]
#[Bench\Warmup(1)]
#[Bench\OutputTimeUnit('milliseconds', precision: 3)]
final class PagingBench extends LiveBenchCase
{
private const TABLE = 'bench_paging';
private const ROWS = 1000;

private SimpleStatement $scan;

public function setUp(): void
{
$keyspace = Env::keyspace();

$this->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<string, array{page_size: int}> */
public function providePageSizes(): iterable
{
yield '50' => ['page_size' => 50];
yield '250' => ['page_size' => 250];
yield '1000' => ['page_size' => 1000];
}
}
Loading