feat(local): Chicory WASM option for Alpine/musl (behind DEVCYCLE_USE_CHICORY)#191
feat(local): Chicory WASM option for Alpine/musl (behind DEVCYCLE_USE_CHICORY)#191jonathannorris wants to merge 2 commits intomainfrom
Conversation
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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 9 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.
| @@ -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(); | ||
| } |
| }; | ||
| 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 10 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.
| @@ -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; |
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.
Summary
wasmtime-javaas the default backend;DEVCYCLE_USE_CHICORYstill opts into Chicory.Implementation
com.dylibso.chicory:compilerto the runtime dependencies.ChicoryLocalBucketingwithMachineFactoryCompiler::compile.ExportFunctionlookups and theMemoryhandle inChicoryLocalBucketinginstead of resolving them on each call.Benchmarks
./gradlew jmhgenerateBucketedConfig- large fixture (~225 KB)generateBucketedConfig- small fixturevariableForUserProtobuf- small fixtureTakeaway: 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 testDEVCYCLE_USE_CHICORY=1 ./gradlew test./gradlew jmhNotes
chicory:compilerand its ASM dependencies in addition to the existing WASM runtime dependencies.