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 \ " } }