diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index d2211f12..f968b3ea 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -65,7 +65,7 @@ jobs:
cache: gradle
- name: Build with Gradle 🏗️
- run: ./gradlew build testOlderJavas
+ run: ./gradlew build jmhCompileGeneratedClasses testOlderJavas
- name: Upload Test Results
if: always()
diff --git a/documentation/benchmarking.md b/documentation/benchmarking.md
new file mode 100644
index 00000000..a817dc3c
--- /dev/null
+++ b/documentation/benchmarking.md
@@ -0,0 +1,38 @@
+# JSON-RPC Benchmarking
+
+The org.eclipse.lsp4j.jsonrpc project comes with some [JMH](https://github.com/openjdk/jmh) based benchmarks.
+This are useful to perform analysis when doing micro-optimization of the JSON-RPC implementation.
+
+## Running Benchmarks
+
+At the command line run `./gradlew jmh` to run the benchmarks.
+A lot of output, including results and caveats from JMH, is provided.
+Additionally, the result summaries are written to `org.eclipse.lsp4j.jsonrpc/build/results/jmh/results.txt`
+
+See the [JMH Gradle Plug-in readme](https://github.com/melix/jmh-gradle-plugin?tab=readme-ov-file#readme) and the [OpenJDK JMH readme](https://github.com/openjdk/jmh) for more details on how best to use JMH.
+
+## Running a single benchmark
+
+To run a single benchmark, add `-PjmhIncludes=NameOfBenchmark` to the `./gradlew` command line.
+
+## Building and running a jmh jar
+
+A typical way of using jmh is to build a self-contained jar.
+Do this by running `./gradlew jmhJar`, the resulting jar is `org.eclipse.lsp4j.jsonrpc/build/libs/org.eclipse.lsp4j.jsonrpc-1.0.0-SNAPSHOT-jmh.jar` (with `1.0.0-SNAPSHOT` replaced with current version of LSP4J).
+This jar can then be used with the normal command line options of jmh, run with `-h` to see what they are:
+
+```sh
+java -jar org.eclipse.lsp4j.jsonrpc/build/libs/org.eclipse.lsp4j.jsonrpc-1.0.0-SNAPSHOT-jmh.jar -h
+```
+
+## Running jmh and gradle is doing nothing?
+
+Gradle's incremental build system may prevent subsequent runs of the `jmh` task from doing anything.
+This happens because gradle does not think anything has changed, so there is nothing to do.
+Delete the results file (`org.eclipse.lsp4j.jsonrpc/build/results/jmh/results.txt`) or do a clean build (`./gradlew clean jmh`).
+
+## Running jmh within Eclipse
+
+At the time of writing there is no plug-in available for Eclipse to simplify running JMH within the IDE.
+LSP4J provides a Buildship launch configuration called `lsp4j-jmh` which can be used to run within the IDE.
+This task can be used once the project is imported in to your Eclipse workspace as explained in the [contribution guide](https://github.com/eclipse-lsp4j/lsp4j/blob/main/CONTRIBUTING.md#eclipse).
diff --git a/lsp4j-jmh.launch b/lsp4j-jmh.launch
new file mode 100644
index 00000000..f5cab536
--- /dev/null
+++ b/lsp4j-jmh.launch
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.eclipse.lsp4j.jsonrpc/build.gradle b/org.eclipse.lsp4j.jsonrpc/build.gradle
index 0084fd13..9b5094b2 100644
--- a/org.eclipse.lsp4j.jsonrpc/build.gradle
+++ b/org.eclipse.lsp4j.jsonrpc/build.gradle
@@ -10,6 +10,10 @@
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
+plugins {
+ id("me.champeau.jmh") version "0.7.3"
+}
+
ext.title = 'LSP4J JSON-RPC'
description = 'Generic JSON-RPC implementation'
@@ -21,3 +25,14 @@ dependencies {
jar.bundle.bnd(
'Import-Package': "com.google.gson.*;version=\"$versions.gson\",*"
)
+
+// Add, for example, -PjmhIncludes=StreamMessageProducerBenchmark, to command line
+// to only run that one benchmark
+def jmhIncludes = project.findProperty("jmhIncludes")
+
+jmh {
+ profilers = ['gc']
+ if (jmhIncludes != null) {
+ includes = [jmhIncludes] // can be simple name or regex
+ }
+}
diff --git a/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageConsumerBenchmark.java b/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageConsumerBenchmark.java
new file mode 100644
index 00000000..6b3bfff1
--- /dev/null
+++ b/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageConsumerBenchmark.java
@@ -0,0 +1,63 @@
+/******************************************************************************
+ * Copyright (c) 2025 Kichwa Coders Canada, Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.jsonrpc.jmh;
+
+import static java.util.Collections.emptyMap;
+
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer;
+import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage;
+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.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@State(Scope.Benchmark)
+public class StreamMessageConsumerBenchmark {
+ private StreamMessageConsumer consumer;
+ private RequestMessage message;
+
+ @SuppressWarnings("resource")
+ @Setup
+ public void setup() {
+ consumer = new StreamMessageConsumer(OutputStream.nullOutputStream(), new MessageJsonHandler(emptyMap()));
+ message = new RequestMessage();
+ message.setId("1");
+ message.setMethod("foo");
+ Map map = new HashMap<>();
+ for (int i = 0; i < 100; i++) {
+ map.put(String.valueOf(i), "X".repeat(i));
+ }
+ message.setParams(map);
+ }
+
+ @Benchmark
+ public void measure() {
+ consumer.consume(message);
+ }
+}
diff --git a/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageProducerBenchmark.java b/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageProducerBenchmark.java
new file mode 100644
index 00000000..5844923c
--- /dev/null
+++ b/org.eclipse.lsp4j.jsonrpc/src/jmh/java/org/eclipse/lsp4j/jsonrpc/jmh/StreamMessageProducerBenchmark.java
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * Copyright (c) 2025 Kichwa Coders Canada, Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.jsonrpc.jmh;
+
+import static java.util.Collections.emptyMap;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer;
+import org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer;
+import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage;
+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.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@State(Scope.Benchmark)
+public class StreamMessageProducerBenchmark {
+
+ private StreamMessageProducer messageProducer;
+ private ByteArrayInputStream bais;
+
+ @Setup
+ public void setup() {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ StreamMessageConsumer consumer = new StreamMessageConsumer(baos, new MessageJsonHandler(emptyMap()));
+ RequestMessage message = new RequestMessage();
+ message.setId("1");
+ message.setMethod("foo");
+ Map map = new HashMap<>();
+ for (int i = 0; i < 100; i++) {
+ map.put(String.valueOf(i), "X".repeat(i));
+ }
+ message.setParams(map);
+ consumer.consume(message);
+ byte[] byteArray = baos.toByteArray();
+ bais = new ByteArrayInputStream(byteArray);
+ messageProducer = new StreamMessageProducer(bais, new MessageJsonHandler(emptyMap()));
+ }
+
+ @Benchmark
+ public void measure(Blackhole bh) {
+ bais.reset();
+ messageProducer.listen(bh::consume);
+ }
+}
diff --git a/releng/build.Jenkinsfile b/releng/build.Jenkinsfile
index d2bda181..1054adad 100644
--- a/releng/build.Jenkinsfile
+++ b/releng/build.Jenkinsfile
@@ -40,7 +40,7 @@ pipeline {
-PignoreTestFailures=true \
--refresh-dependencies \
--continue \
- clean build testOlderJavas signJar publish \
+ clean build jmhCompileGeneratedClasses testOlderJavas signJar publish \
"
}
}