Skip to content
Draft
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
100 changes: 100 additions & 0 deletions src/test/jmh/redis/clients/jedis/benchmark/csc/ConnChurnBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package redis.clients.jedis.benchmark.csc;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

/**
* 99% GET / 1% SET, uniform 10K-key working set + a side thread dialing/closing fresh connections
* at {@value #CHURN_DIALS_PER_SEC} per second.
* <p>
* Mirrors the {@code ConnChurn} workload from {@code CSC go-redis.md}. The headline finding in that
* exploration was the 2,255× gap between Phase 1 and the BCAST design under churn pressure.
* Connection lifecycle churn is also where Jedis CSC's per-connection tracking model is most
* exposed: an entry whose owner connection is replaced becomes effectively stale until the next
* push drain.
* <p>
* <b>Limitations:</b> the churn thread dials fresh sockets against Redis but cannot directly
* recycle conns inside the workload client's pool — commons-pool2 doesn't expose a per-borrower
* eviction hook here. The dial/close pressure exercises server-side accept/teardown and the auth
* path; it does not by itself force the pool to rotate its tracked conns. Run with a short idle
* timeout if you want pool rotation.
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 1, time = 30, timeUnit = TimeUnit.SECONDS)
@Threads(16)
public class ConnChurnBenchmark extends Workload {

private static final int CHURN_DIALS_PER_SEC = 50;
private volatile Thread churnThread;
private volatile boolean churnStop;

@Override
protected int readPct() {
return 99;
}

@Override
protected KeyDistribution.IndexSampler sampler() {
return KeyDistribution.UNIFORM;
}

@Setup(Level.Trial)
public void setupChurn() {
churnStop = false;
churnThread = new Thread(this::churnLoop, "csc-churn");
churnThread.setDaemon(true);
churnThread.start();
}

@TearDown(Level.Trial)
public void teardownChurn() throws InterruptedException {
churnStop = true;
if (churnThread != null) {
churnThread.join(2_000);
}
}

private void churnLoop() {
final long perDialNanos = TimeUnit.SECONDS.toNanos(1) / CHURN_DIALS_PER_SEC;
long next = System.nanoTime();
while (!churnStop) {
long now = System.nanoTime();
if (now < next) {
try {
TimeUnit.NANOSECONDS.sleep(next - now);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
next += perDialNanos;
try (Jedis j = new Jedis(endpoint.getHostAndPort())) {
String pwd = endpoint.getPassword();
if (pwd != null && !pwd.isEmpty()) j.auth(pwd);
j.ping();
} catch (Exception ignored) {
// dial-side churn — swallow transient errors; benchmark records throughput regardless
}
}
}

@Benchmark
public Object workload(Rng rng) {
return doOnce(rng);
}
}
29 changes: 29 additions & 0 deletions src/test/jmh/redis/clients/jedis/benchmark/csc/CscMain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package redis.clients.jedis.benchmark.csc;

import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
* Manual IDE entry point for the CSC workload suite.
* <p>
* Mirrors the {@code go-redis} CSC sweep — 7 workloads × {@code (none, csc)} × {@code (cold, warm)}
* — and produces {@code benchmarks.json} alongside {@code benchmark.log} so results can be plotted
* the same way.
* <p>
* Maven invocations from {@code CSC Jedis.md}:
*
* <pre>
* mvn -Pjmh test -Djmh.includes="csc\\..*Benchmark" # full sweep
* mvn -Pjmh test -Djmh.includes="csc\\.HotKeyZipfianBenchmark" # single workload
* </pre>
*/
public class CscMain {

public static void main(String... args) throws RunnerException {
new Runner(new OptionsBuilder().include(".*benchmark\\.csc\\..*ReadHeavy99Benchmark.*")
//.resultFormat(ResultFormatType.JSON).result("benchmarks-csc.json")
/*.output("benchmark-csc.log")*/.build()).run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package redis.clients.jedis.benchmark.csc;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

/**
* 99% GET / 1% SET, Zipfian {@code s=1.1} distribution — the classic "hot-key" workload.
* <p>
* Mirrors the {@code HotKeyZipfian} workload from {@code CSC go-redis.md}, which is where the
* canonical Phase 1 CSC implementation collapsed (25 ops/s in the go-redis run). The same hot-key
* single-flight pile-up is the primary risk for Jedis CSC — every thread races for the same handful
* of keys, which serializes on the cache lock and floods the owner connection with invalidation
* traffic.
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(100)
public class HotKeyZipfianBenchmark extends Workload {

private final KeyDistribution.IndexSampler sampler = KeyDistribution.zipf(1.1);

@Override
protected int readPct() {
return 99;
}

@Override
protected KeyDistribution.IndexSampler sampler() {
return sampler;
}

@Benchmark
public Object workload(Rng rng) {
return doOnce(rng);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package redis.clients.jedis.benchmark.csc;

import java.util.Random;

/**
* Key-index sampling distributions used by the CSC workload benchmarks.
* <p>
* Mirrors the four samplers used by the {@code go-redis} CSC exploration ({@code CSC go-redis.md})
* so the Jedis suite can produce apples-to-apples numbers per workload:
* <ul>
* <li>{@link #UNIFORM} — uniform random over {@code [0, n)}.</li>
* <li>{@link #zipf(double)} — Zipfian with exponent {@code s} (default {@code s=1.1}).</li>
* <li>{@link #normal()} — Normal {@code N(n/2, n/6)} clamped to {@code [0, n)}.</li>
* <li>{@link #hot(int, double)} — fixed hot-set hit fraction for synthetic hot-key tests.</li>
* </ul>
* <p>
* All samplers are stateless across calls except for the {@link Random} instance they receive,
* which must be per-thread (see {@link Workload.Rng}).
*/
final class KeyDistribution {

private KeyDistribution() {
}

interface IndexSampler {
int next(Random r, int n);
}

static final IndexSampler UNIFORM = new IndexSampler() {
@Override
public int next(Random r, int n) {
return r.nextInt(n);
}
};

/**
* Zipfian sampler using the {@code rejection-sampling} variant from Devroye's
* {@code "Non-Uniform Random Variate Generation"} (1986), §10.4 (the same approach the
* {@code rand/v2.Zipf} type in Go's standard library uses).
* <p>
* Returns indices in {@code [0, n)} biased toward {@code 0} — wrap with a permutation if the
* working-set should be spread across the key space.
*/
static IndexSampler zipf(final double s) {
if (s <= 1.0) {
throw new IllegalArgumentException("Zipf exponent must be > 1.0, got " + s);
}
return new IndexSampler() {
@Override
public int next(Random r, int n) {
double sm1 = s - 1.0;
double b = Math.pow(2.0, sm1);
while (true) {
double u = r.nextDouble();
double v = r.nextDouble();
double x = Math.floor(Math.pow(u, -1.0 / sm1));
if (x < 1.0 || x > n) continue;
double t = Math.pow(1.0 + 1.0 / x, sm1);
if (v * x * (t - 1.0) / (b - 1.0) <= t / b) {
return (int) (x - 1.0);
}
}
}
};
}

/**
* Normal {@code N(n/2, n/6)} clamped to {@code [0, n)}. Produces a contiguous middle-band hot
* zone — mirrors the {@code NormalDistribution} workload in the go-redis suite.
*/
static IndexSampler normal() {
return new IndexSampler() {
@Override
public int next(Random r, int n) {
double mean = n / 2.0;
double sigma = n / 6.0;
double g = r.nextGaussian() * sigma + mean;
int idx = (int) g;
if (idx < 0) return 0;
if (idx >= n) return n - 1;
return idx;
}
};
}

/**
* Mixed sampler that picks from the first {@code hotCount} keys with probability {@code hotProb}
* and from the rest uniformly otherwise.
*/
static IndexSampler hot(final int hotCount, final double hotProb) {
return new IndexSampler() {
@Override
public int next(Random r, int n) {
if (r.nextDouble() < hotProb) return r.nextInt(Math.min(hotCount, n));
return r.nextInt(n);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package redis.clients.jedis.benchmark.csc;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

/**
* 99% GET / 1% SET, 1K-key working set, configurable value size {@code 1KB / 10KB / 100KB}.
* <p>
* Mirrors the {@code LargePayload} workload from {@code CSC go-redis.md}. The interesting signal
* here is memory pressure, not raw throughput — the larger the payload, the more allocations and GC
* pressure the deserialization path on every cache hit (see {@code CacheEntry.toObject}) produces.
* Capture RSS externally during the run; JMH won't report it.
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(16)
public class LargePayloadBenchmark extends Workload {

@Param({ "1024", "10240", "102400" })
public int payloadBytes;

@Override
protected int workingSet() {
return 1_000;
}

@Override
protected int valueBytes() {
return payloadBytes;
}

@Override
protected int readPct() {
return 99;
}

@Override
protected KeyDistribution.IndexSampler sampler() {
return KeyDistribution.UNIFORM;
}

@Benchmark
public Object workload(Rng rng) {
return doOnce(rng);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package redis.clients.jedis.benchmark.csc;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

/**
* 99% GET / 1% SET, Normal {@code N(n/2, n/6)} clamped — contiguous middle-band hot zone, the sort
* of access pattern bucketed time-series or hashed shards exhibit.
* <p>
* Mirrors the {@code NormalDistribution} workload from {@code CSC go-redis.md}.
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(50)
public class NormalDistributionBenchmark extends Workload {

private final KeyDistribution.IndexSampler sampler = KeyDistribution.normal();

@Override
protected int readPct() {
return 99;
}

@Override
protected KeyDistribution.IndexSampler sampler() {
return sampler;
}

@Benchmark
public Object workload(Rng rng) {
return doOnce(rng);
}
}
Loading
Loading