Skip to content

feat(local): Chicory WASM option for Alpine/musl (behind DEVCYCLE_USE_CHICORY)#191

Draft
jonathannorris wants to merge 2 commits intomainfrom
cursor/wasmtime-java-binding-on-alpine-linux-8d92
Draft

feat(local): Chicory WASM option for Alpine/musl (behind DEVCYCLE_USE_CHICORY)#191
jonathannorris wants to merge 2 commits intomainfrom
cursor/wasmtime-java-binding-on-alpine-linux-8d92

Conversation

@jonathannorris
Copy link
Member

@jonathannorris jonathannorris commented Mar 20, 2026

Summary

  • Keep wasmtime-java as the default backend; DEVCYCLE_USE_CHICORY still opts into Chicory.
  • Switch the Chicory backend from the default interpreter to Chicory's runtime compiler.
  • Cache exported functions / memory handles and reuse Jackson mappers in the Chicory hot path.
  • Update docs and JMH notes to reflect the compiled Chicory path.

Implementation

  • Add com.dylibso.chicory:compiler to the runtime dependencies.
  • Configure ChicoryLocalBucketing with MachineFactoryCompiler::compile.
  • Cache ExportFunction lookups and the Memory handle in ChicoryLocalBucketing instead of resolving them on each call.
  • Reuse Jackson mappers for bucketed config and flush payload parsing.

Benchmarks

  • Run: ./gradlew jmh
  • Setup: JDK 17, OpenJDK 64-bit, 1 thread, average time per op, 2x1s warmup + 5x1s measurement, 1 fork.
  • Numbers will vary by CPU, JVM, and load; use the ratios as directional.
Benchmark wasmtime (avg) chicory (avg)
generateBucketedConfig - large fixture (~225 KB) 4,946 +- 169 us 14,968 +- 460 us
generateBucketedConfig - small fixture 301 +- 25 us 559 +- 117 us
variableForUserProtobuf - small fixture 62.9 +- 4.9 us 23.3 +- 1.7 us

Takeaway: moving Chicory to the runtime compiler closes most of the gap from the interpreter-only version. On this run, Chicory is still slower on the config-generation paths, but much closer than before, and it is faster on the protobuf path.

Testing

  • ./gradlew test
  • DEVCYCLE_USE_CHICORY=1 ./gradlew test
  • ./gradlew jmh

Notes

  • This uses Chicory's runtime compiler, not build-time compilation.
  • The published runtime dependency set now includes chicory:compiler and its ASM dependencies in addition to the existing WASM runtime dependencies.

Keep wasmtime-java as default; select Chicory via env for Alpine/musl.
Split backends behind LocalBucketing facade; add JMH wasmtime vs Chicory benchmarks.

Co-authored-by: Jonathan Norris <jonathan.norris@dynatrace.com>
};
long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
StringBuilder result = new StringBuilder();
for (int i = 0; i < stringLength; i += 2) {

Check failure

Code scanning / CodeQL

Comparison of narrow type with wide type in loop condition High

Comparison between
expression
of type int and
expression
of wider type long.

Copilot Autofix

AI about 7 hours ago

In general, to fix comparisons between a narrow type loop index and a wider bound, change the loop index (and related arithmetic) to use a type at least as wide as the bound, or safely narrow the bound when it is known to fit into the narrower type. This avoids overflow of the index while the loop condition remains true.

In this case, stringLength is a long, and the loop index i is an int. The best fix with minimal functional impact is to make i a long and ensure any arithmetic using i is done in long as well. The Memory.read method expects an int address, so we should cast the sum startAddress + i back to int when calling mem.read. This way, the comparison i < stringLength is between two long values, eliminating the narrow-vs-wide comparison, while the effective address used for memory access remains an int, as required by the API.

Concretely, in src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java, in the readWasmString(Memory mem, int startAddress) method around lines 186–197, change the loop header from for (int i = 0; i < stringLength; i += 2) to for (long i = 0; i < stringLength; i += 2) and adjust the mem.read call to cast startAddress + i to int. No additional imports or helper methods are needed.

Suggested changeset 1
src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java b/src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java
--- a/src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java
+++ b/src/main/java/com/devcycle/sdk/server/local/bucketing/ChicoryLocalBucketing.java
@@ -192,8 +192,8 @@
         };
         long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
         StringBuilder result = new StringBuilder();
-        for (int i = 0; i < stringLength; i += 2) {
-            result.append((char) mem.read(startAddress + i));
+        for (long i = 0; i < stringLength; i += 2) {
+            result.append((char) mem.read((int) (startAddress + i)));
         }
         return result.toString();
     }
EOF
@@ -192,8 +192,8 @@
};
long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
StringBuilder result = new StringBuilder();
for (int i = 0; i < stringLength; i += 2) {
result.append((char) mem.read(startAddress + i));
for (long i = 0; i < stringLength; i += 2) {
result.append((char) mem.read((int) (startAddress + i)));
}
return result.toString();
}
Copilot is powered by AI and may make mistakes. Always verify output.
};
long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
String result = "";
for (int i = 0; i < stringLength; i += 2) {

Check failure

Code scanning / CodeQL

Comparison of narrow type with wide type in loop condition High

Comparison between
expression
of type int and
expression
of wider type long.

Copilot Autofix

AI about 8 hours ago

In general, to fix mixed-width comparisons in loop conditions, both sides of the comparison should use the same type, and the narrower side should be widened (or the wider side safely narrowed with validation) so that overflow of the loop variable cannot cause an infinite loop or incorrect termination.

Here, stringLength is a long and the loop index i is an int:

long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
String result = "";
for (int i = 0; i < stringLength; i += 2) {
    result += (char) buf.get(startAddress + i);
}

The best, least-invasive fix is to promote the index variable i to long so that it matches stringLength. That preserves existing behavior for all values that currently fit into an int, while avoiding potential overflow if stringLength is larger. Since ByteBuffer.get(int index) takes an int, we will cast i to int at the point of use; this is safe as long as the underlying memory region and stringLength are consistent, and it does not change the semantics for currently valid inputs. Concretely, in readWasmString (around lines 143–158 in WasmtimeLocalBucketing.java), change the for loop from int i to long i, and cast i to int in the buf.get call.

No new methods or imports are required; the change is confined to that loop.

Suggested changeset 1
src/main/java/com/devcycle/sdk/server/local/bucketing/WasmtimeLocalBucketing.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/com/devcycle/sdk/server/local/bucketing/WasmtimeLocalBucketing.java b/src/main/java/com/devcycle/sdk/server/local/bucketing/WasmtimeLocalBucketing.java
--- a/src/main/java/com/devcycle/sdk/server/local/bucketing/WasmtimeLocalBucketing.java
+++ b/src/main/java/com/devcycle/sdk/server/local/bucketing/WasmtimeLocalBucketing.java
@@ -151,8 +151,8 @@
         };
         long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
         String result = "";
-        for (int i = 0; i < stringLength; i += 2) {
-            result += (char) buf.get(startAddress + i);
+        for (long i = 0; i < stringLength; i += 2) {
+            result += (char) buf.get(startAddress + (int) i);
         }
 
         return result;
EOF
@@ -151,8 +151,8 @@
};
long stringLength = ByteConversionUtils.getUnsignedInt(headerBytes);
String result = "";
for (int i = 0; i < stringLength; i += 2) {
result += (char) buf.get(startAddress + i);
for (long i = 0; i < stringLength; i += 2) {
result += (char) buf.get(startAddress + (int) i);
}

return result;
Copilot is powered by AI and may make mistakes. Always verify output.
Use Chicory's runtime compiler and cache hot-path lookups to reduce the latency gap of the pure-Java WASM path.
Update the docs to clarify that the Chicory backend now runs compiled rather than interpreted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants