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
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
38 changes: 38 additions & 0 deletions documentation/benchmarking.md
Original file line number Diff line number Diff line change
@@ -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).
18 changes: 18 additions & 0 deletions lsp4j-jmh.launch
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.buildship.core.launch.runconfiguration">
<listAttribute key="arguments"/>
<booleanAttribute key="build_scans_enabled" value="false"/>
<stringAttribute key="gradle_distribution" value="GRADLE_DISTRIBUTION(WRAPPER)"/>
<stringAttribute key="gradle_user_home" value=""/>
<stringAttribute key="java_home" value=""/>
<listAttribute key="jvm_arguments"/>
<booleanAttribute key="offline_mode" value="false"/>
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<booleanAttribute key="override_workspace_settings" value="false"/>
<booleanAttribute key="show_console_view" value="true"/>
<booleanAttribute key="show_execution_view" value="true"/>
<listAttribute key="tasks">
<listEntry value="jmh"/>
</listAttribute>
<stringAttribute key="working_dir" value="${workspace_loc:/lsp4j}"/>
</launchConfiguration>
15 changes: 15 additions & 0 deletions org.eclipse.lsp4j.jsonrpc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
2 changes: 1 addition & 1 deletion releng/build.Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pipeline {
-PignoreTestFailures=true \
--refresh-dependencies \
--continue \
clean build testOlderJavas signJar publish \
clean build jmhCompileGeneratedClasses testOlderJavas signJar publish \
"
}
}
Expand Down