Skip to content
Merged

Dev #30

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
168 changes: 102 additions & 66 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
plugins {
id 'java'
id 'application'
id 'com.google.protobuf' version '0.9.5'
id 'io.freefair.lombok' version '8.13.1'
id 'com.gradleup.shadow' version '8.3.6'
id 'me.champeau.jmh' version '0.7.3'
}
plugins {
id 'java'
id 'application'
id 'com.google.protobuf' version '0.9.5'
id 'io.freefair.lombok' version '8.13.1'
id 'com.gradleup.shadow' version '8.3.6'
id 'me.champeau.jmh' version '0.7.3'
}

group = 'io.ringbroker'
version = '0.0.1-BETA'
/* ---------- JVM ---------- */

/* ---------- JVM ---------- */
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
Expand All @@ -24,6 +24,19 @@ tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

/* ---------- Toolchain launcher (ensures JavaExec/Test run on JDK 21, not Gradle daemon JDK) ---------- */
def jdk21Launcher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}

tasks.withType(JavaExec).configureEach {
javaLauncher = jdk21Launcher
}

tasks.withType(Test).configureEach {
javaLauncher = jdk21Launcher
}

/* ---------- Repos & Versions ---------- */
repositories { mavenCentral() }

Expand All @@ -32,11 +45,11 @@ ext {
protobufVersion = '3.25.7'
jacksonVersion = '2.19.0'
picocliVersion = '4.7.7'
nettyVersion = '4.2.1.Final'
nettyVersion = '4.2.1.Final'
jmhVersion = '1.37'
slf4jVersion = '2.0.17'
jupiterVersion = '5.12.2'
junitPlatformVersion = '1.12.2' // Added matching platform version
junitPlatformVersion = '1.12.2'
annotationVersion = '1.3.2'
testcontainersVersion = '1.20.3'
}
Expand All @@ -46,25 +59,25 @@ dependencies {
// gRPC and Protobuf
implementation "io.grpc:grpc-netty-shaded:$grpcVersion"
implementation "io.grpc:grpc-stub:$grpcVersion"
implementation "io.grpc:grpc-protobuf:$grpcVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "io.netty:netty-all:$nettyVersion"
// Lombok (enabled via plugin)
compileOnly "org.projectlombok:lombok"
annotationProcessor "org.projectlombok:lombok"
// Jackson YAML for config parsing
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion"
// Picocli CLI support
implementation "info.picocli:picocli:$picocliVersion"
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion"
// SLF4J API (you can choose a backend like logback)
implementation "org.slf4j:slf4j-api:$slf4jVersion"
runtimeOnly "org.slf4j:slf4j-simple:$slf4jVersion"
implementation "io.grpc:grpc-protobuf:$grpcVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "io.netty:netty-all:$nettyVersion"

// Lombok (enabled via plugin)
compileOnly "org.projectlombok:lombok"
annotationProcessor "org.projectlombok:lombok"

// Jackson YAML for config parsing
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion"

// Picocli CLI support
implementation "info.picocli:picocli:$picocliVersion"
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion"

// SLF4J API + simple backend
implementation "org.slf4j:slf4j-api:$slf4jVersion"
runtimeOnly "org.slf4j:slf4j-simple:$slf4jVersion"

// JUnit 5
testImplementation "org.junit.jupiter:junit-jupiter:$jupiterVersion"
Expand All @@ -75,25 +88,25 @@ dependencies {

compileOnly "javax.annotation:javax.annotation-api:$annotationVersion"

// JMH dependencies
jmh "org.openjdk.jmh:jmh-core:${jmhVersion}"
jmh "org.openjdk.jmh:jmh-generator-annprocess:${jmhVersion}"
}
/* ---------- Protobuf / gRPC codegen ---------- */
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
}
generateProtoTasks {
all().each { task ->
task.plugins { grpc {} }
}
}
}
/* ---------- Application entrypoint ---------- */
// ---- JMH (FIXED): ensure BenchmarkList is generated ----
jmhImplementation "org.openjdk.jmh:jmh-core:${jmhVersion}"
jmhAnnotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:${jmhVersion}"
}

/* ---------- Protobuf / gRPC code-gen ---------- */
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
}
generateProtoTasks {
all().each { task ->
task.plugins { grpc {} }
}
}
}

/* ---------- Application entry-point ---------- */
application {
mainClass = 'io.ringbroker.Application'
}
Expand All @@ -105,8 +118,8 @@ test {
// Fat jar is required for the Testcontainers-based cluster integration test.
dependsOn tasks.named('shadowJar')
}
/* ---------- Jar manifest ---------- */

/* ---------- Jar manifest ---------- */
jar {
manifest {
attributes(
Expand All @@ -116,21 +129,20 @@ jar {
)
}
}
/* ---------- JMH Configuration ---------- */

/* ---------- JMH Configuration ---------- */
jmh {
includes = ['.*Benchmark.*'] // Include classes with "Benchmark" in their name
resultFormat = 'JSON' // Output format for results
includes = ['.*Benchmark.*']
resultFormat = 'JSON'
resultsFile = project.file("${project.buildDir}/reports/jmh/results.json")
timeOnIteration = '1s' // Time per iteration
warmupIterations = 2 // Number of warmup iterations
iterations = 5 // Number of measurement iterations
fork = 2 // Number of forks
failOnError = true // Fail build on errors during benchmarking
forceGC = true // Force GC between iterations
jvmArgsAppend = ['--enable-preview'] // Add any JVM args needed for your project

// Allow quick overrides from the command line (e.g. -PjmhInclude=Foo -PjmhIterations=1).
timeOnIteration = '1s'
warmupIterations = 2
iterations = 5
fork = 2
failOnError = true
forceGC = true
jvmArgsAppend = ['--enable-preview']

if (project.hasProperty('jmhInclude')) {
includes = [project.property('jmhInclude')]
}
Expand All @@ -147,3 +159,27 @@ jmh {
jvmArgsAppend += ['-Djmh.ignoreLock=true']
}
}

/* ---------- Custom Benchmarker runner ---------- */
tasks.register('benchmarkProfile', JavaExec) {
group = 'benchmark'
description = 'Runs the custom JMH Benchmarker main (Windows: JFR via Benchmarker; non-Windows: async-profiler).'

dependsOn tasks.named('jmhClasses')

// IMPORTANT: include both JMH + MAIN outputs & deps so forks can load project classes
classpath = files(
sourceSets.jmh.runtimeClasspath,
sourceSets.main.runtimeClasspath
)

mainClass = 'io.ringbroker.benchmark.Benchmarker'

jvmArgs '--enable-preview', '-XX:+UnlockDiagnosticVMOptions', '-XX:+DebugNonSafepoints'

if (project.hasProperty('asyncLibPath')) systemProperty 'ringbroker.async.libPath', project.property('asyncLibPath')
if (project.hasProperty('asyncDir')) systemProperty 'ringbroker.async.dir', project.property('asyncDir')
if (project.hasProperty('asyncEvent')) systemProperty 'ringbroker.async.event', project.property('asyncEvent')
if (project.hasProperty('asyncOutput')) systemProperty 'ringbroker.async.output', project.property('asyncOutput')
if (project.hasProperty('profileDir')) systemProperty 'ringbroker.profile.dir', project.property('profileDir')
}
86 changes: 80 additions & 6 deletions src/jmh/java/io/ringbroker/benchmark/Benchmarker.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,93 @@
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* Main benchmark suite runner for RingBroker performance testing.
* This class serves as the entry point for running all benchmarks.
*/
public class Benchmarker {

public static void main(final String[] args) throws RunnerException {
final Options opt = new OptionsBuilder()
.include("io.ringbroker.benchmark.*Benchmark")
.exclude(Benchmarker.class.getSimpleName())
.exclude(RawTcpClient.class.getSimpleName())
.build();
final boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");

// Where all profiler artifacts go (JFR or async-profiler outputs).
final Path outDir = Paths.get(System.getProperty(
"ringbroker.profile.dir",
System.getProperty("ringbroker.async.dir", "build/reports/jmh/profile")
)).toAbsolutePath().normalize();
ensureDirectory(outDir);

final OptionsBuilder builder = new OptionsBuilder();
builder.include("io.ringbroker.benchmark.*Benchmark");
builder.exclude(Benchmarker.class.getSimpleName());
builder.exclude(RawTcpClient.class.getSimpleName());

// Ensure forks also get these (NOT just the JavaExec runner JVM)
builder.jvmArgsAppend(
"--enable-preview",
"-XX:+UnlockDiagnosticVMOptions",
"-XX:+DebugNonSafepoints"
);

// Pick profiler backend:
// - Windows: use JFR (built-in, no native DLL needed)
// - Others: use async-profiler (as you had)
if (isWindows) {
final String jfrSettings = System.getProperty("ringbroker.jfr.settings", "profile");
final int stackDepth = Integer.getInteger("ringbroker.jfr.stackdepth", 256);

// JFR supports %p (pid) and %t (timestamp) filename expansion. :contentReference[oaicite:2]{index=2}
final String jfrFile = outDir.resolve("ringbroker-%p-%t.jfr").toString();

builder.jvmArgsAppend(
"-XX:StartFlightRecording=filename=" + jfrFile + ",settings=" + jfrSettings,
"-XX:FlightRecorderOptions=stackdepth=" + stackDepth
);
} else {
final String asyncLibPath = firstNonBlank(
System.getProperty("ringbroker.async.libPath"),
System.getenv("ASYNC_PROFILER_LIB"),
"/opt/async-profiler/lib/libasyncProfiler.so"
);

// Important: JMH async profiler options are separated by ';' not ','.
final String asyncProfilerOptions = String.join(";",
"libPath=" + asyncLibPath,
"event=" + System.getProperty("ringbroker.async.event", "cpu"),
"output=" + System.getProperty("ringbroker.async.output", "flamegraph"),
"dir=" + outDir
);

builder.addProfiler("async", asyncProfilerOptions);
}

if (Boolean.getBoolean("ringbroker.profile.gc")) {
builder.addProfiler("gc");
}

final Options opt = builder.build();
new Runner(opt).run();
}
}

private static String firstNonBlank(final String... values) {
for (final String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
throw new IllegalStateException("No non-blank value provided");
}

private static void ensureDirectory(final Path path) {
try {
Files.createDirectories(path);
} catch (final IOException e) {
throw new IllegalStateException("Failed to create profiler output directory: " + path, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,4 @@ private static void wipeDir(final Path root) throws IOException {
});
}
}
}
}
Loading
Loading