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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,16 @@ pnpm benchmark:request-local

The command builds `dist` before reporting ten scenarios: sequential request-local hits, sequential process-local hits, enabled bounded fallbacks, request-local coalescing fan-out, process coalescing fan-out, remote-read-deadline coalescing fan-out, tracked Redis hits with shadow omitted, tracked Redis hits deterministically outside a partial shadow ramp, a ramped-down warm-hit confirmation, and a ramped-down clean-miss fill. Both shadow scenarios prove that the caller completes before detached Redis work. The benchmark is a maintainer tool and is not included in the published package. It asserts fallback counts, Redis behavior, coalescing state, timer cleanup, returned values, exactly-once SoT reuse, and conditional confirmation/fill without applying a timing threshold. Override its work sizes with `DIALCACHE_BENCH_ITERATIONS` and `DIALCACHE_BENCH_FANOUT`.

### Redis write benchmark

With a Redis reachable at `REDIS_URL` (default `redis://127.0.0.1:6379`, e.g. `docker run --rm -p 6379:6379 redis:6.2`), measure the local build's write path:

```bash
pnpm benchmark:redis-write
```

The command builds `dist`, then runs sequential tracked and untracked writes at 100 B, 10 KiB, 100 KiB, and 1 MiB payloads, reporting server-side command cost per write from `INFO commandstats` (the `EVALSHA` entry envelopes the stamp script's internal calls) and client-side p50/p95 latency. Like the cache-path benchmark it is a maintainer tool, is not part of the published package, and asserts no timing thresholds — absolute numbers depend on the machine, engine, and load, so compare runs only within one environment. Scale iteration counts with `DIALCACHE_BENCH_WRITE_SCALE`.

### Releasing

Publishing starts by manually running the `Release` workflow from current `main`. After the package checks pass, Semantic Release selects the next version from Conventional Commits since the highest stable `vX.Y.Z` tag. While the package is pre-1.0, breaking changes bump minor — their `BREAKING CHANGE:` footers still drive full release notes without forcing 1.0.0 — `feat` bumps minor, and every other normal PR-title type (`fix`, `perf`, `docs`, `style`, `refactor`, `test`, `build`, `chore`, `ci`, and `revert`) bumps patch. The highest required bump wins. Major bumps return when 1.0.0 is cut; `release.config.mjs` implements this table and must change together with this section.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
],
"scripts": {
"benchmark:request-local": "pnpm build && node scripts/benchmark-request-local.mjs",
"benchmark:redis-write": "pnpm build && node scripts/benchmark-redis-write.mjs",
"build": "tsup src/index.ts src/datadog.ts src/node-redis.ts src/prometheus.ts src/redis-protocol.ts src/valkey-glide.ts --format esm,cjs --dts --clean",
"check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:package",
"typecheck": "tsc --noEmit",
Expand Down
112 changes: 112 additions & 0 deletions scripts/benchmark-redis-write.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Maintainer benchmark for the Redis write path. Measures the local build's
// tracked and untracked writes against a live Redis and reports server-side
// command cost per write (INFO commandstats; the EVALSHA entry envelopes
// script-internal calls) alongside client-side latency percentiles. It
// asserts nothing and applies no timing thresholds: absolute numbers are
// machine-, engine-, and load-dependent, so compare runs only against the
// same environment.
//
// Requires a reachable Redis, e.g.: docker run --rm -p 6379:6379 redis:6.2
// Usage: pnpm benchmark:redis-write (REDIS_URL to override)
// DIALCACHE_BENCH_WRITE_SCALE scales iteration counts (default 1).
import { createClient } from "redis";

import { createNodeRedisDialCacheClient, dialcacheRedisScripts } from "../dist/node-redis.js";

const REDIS_URL = process.env.REDIS_URL ?? "redis://127.0.0.1:6379";
const SCALE = Number(process.env.DIALCACHE_BENCH_WRITE_SCALE ?? "1");
const SIZES = [
{ name: "100 B", bytes: 100, n: 4_000 },
{ name: "10 KiB", bytes: 10 * 1024, n: 2_000 },
{ name: "100 KiB", bytes: 100 * 1024, n: 600 },
{ name: "1 MiB", bytes: 1024 * 1024, n: 150 },
];
const WARMUP = 30;

function percentile(sortedAscending, p) {
const index = Math.min(sortedAscending.length - 1, Math.floor((p / 100) * sortedAscending.length));
return sortedAscending[index];
}

async function commandStats(client) {
const raw = await client.sendCommand(["INFO", "commandstats"]);
const stats = {};
for (const line of String(raw).split("\n")) {
const match = /^cmdstat_([a-z|]+):calls=(\d+),usec=(\d+)/.exec(line.trim());
if (match !== null) {
stats[match[1]] = { calls: Number(match[2]), usec: Number(match[3]) };
}
}
return stats;
}

const client = createClient({
url: REDIS_URL,
scripts: dialcacheRedisScripts,
disableOfflineQueue: true,
socket: { connectTimeout: 2_000 },
});
client.on("error", () => undefined);
try {
await client.connect();
} catch (error) {
console.error(`Could not reach Redis at ${REDIS_URL}; start one first, e.g. docker run --rm -p 6379:6379 redis:6.2`);
throw error;
}
const adapter = createNodeRedisDialCacheClient(client);

const rows = [];
for (const mode of ["tracked", "untracked"]) {
for (const size of SIZES) {
const iterations = Math.max(1, Math.round(size.n * SCALE));
const payload = "x".repeat(size.bytes);
const valueKey = `benchmark:write:${mode}:${size.bytes}:value`;
const watermarkKey = `benchmark:write:${mode}:${size.bytes}:watermark`;
const request = mode === "tracked"
? { valueKey, watermarkKey, cacheTtlMs: 60_000, value: payload }
: { valueKey, cacheTtlMs: 60_000, value: payload };

for (let i = 0; i < WARMUP; i += 1) {
await adapter.write(request);
}
await client.sendCommand(["CONFIG", "RESETSTAT"]);

const latenciesUsec = [];
for (let i = 0; i < iterations; i += 1) {
const start = process.hrtime.bigint();
await adapter.write(request);
latenciesUsec.push(Number(process.hrtime.bigint() - start) / 1_000);
}

// Sum only the commands the client dispatches top-level (SET, EVALSHA,
// and the EVAL recovery). Script-internal calls surface in commandstats
// too, but the EVALSHA entry already envelopes their execution time.
const stats = await commandStats(client);
const serverUsec = (stats.set?.usec ?? 0)
+ (stats.evalsha?.usec ?? 0)
+ (stats.eval?.usec ?? 0);
latenciesUsec.sort((a, b) => a - b);
rows.push({
mode,
size: size.name,
writes: iterations,
serverUsecPerWrite: serverUsec / iterations,
clientP50Usec: percentile(latenciesUsec, 50),
clientP95Usec: percentile(latenciesUsec, 95),
});
}
}
await client.quit();

console.log(`Redis write benchmark — ${REDIS_URL}`);
console.log("mode size writes server µs/write client p50 µs client p95 µs");
for (const row of rows) {
console.log(
row.mode.padEnd(10)
+ row.size.padEnd(10)
+ String(row.writes).padEnd(9)
+ row.serverUsecPerWrite.toFixed(1).padEnd(18)
+ row.clientP50Usec.toFixed(0).padEnd(16)
+ row.clientP95Usec.toFixed(0),
);
}
Loading