From dc392dcae3748bbe13e6832afa60b0ab271caf26 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:17:36 +0000 Subject: [PATCH 1/6] Add benchmark for the full JS script evaluation pipeline The existing benchmark only covers value translation in isolation. This adds a second benchmark that covers the paths that scripts actually go through at runtime: context creation, script instantiation, reading a script member value, calling a JS function as an Integrated Dynamics operator, calling an Integrated Dynamics operator from JS, and unwrapping values that were translated to Graal before. The benchmark harness now also warms up before measuring, measures with nanosecond precision, and reports the fastest of several rounds, as the previous millisecond-precision single-shot measurements were too noisy to tell improvements apart from run-to-run variance. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- build.gradle | 6 + .../BenchmarkScriptEvaluation.java | 168 ++++++++++++++++++ .../BenchmarkValueTranslators.java | 65 ++++--- 3 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkScriptEvaluation.java diff --git a/build.gradle b/build.gradle index e09240bee..08f7249ec 100644 --- a/build.gradle +++ b/build.gradle @@ -446,3 +446,9 @@ task benchmark(type: JavaExec) { main = "org.cyclops.integratedscripting.evaluate.translation.BenchmarkValueTranslators" } test.dependsOn benchmark + +task benchmarkScriptEvaluation(type: JavaExec) { + classpath sourceSets.test.runtimeClasspath + main = "org.cyclops.integratedscripting.evaluate.translation.BenchmarkScriptEvaluation" +} +test.dependsOn benchmarkScriptEvaluation diff --git a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkScriptEvaluation.java b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkScriptEvaluation.java new file mode 100644 index 000000000..c6363edb2 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkScriptEvaluation.java @@ -0,0 +1,168 @@ +package org.cyclops.integratedscripting.evaluate.translation; + +import net.minecraft.DetectedVersion; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; +import org.cyclops.integrateddynamics.api.evaluate.variable.IVariable; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.core.evaluate.operator.Operators; +import org.cyclops.integrateddynamics.core.evaluate.variable.*; +import org.cyclops.integratedscripting.evaluate.ScriptHelpers; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; + +/** + * Benchmarks for the full JavaScript script evaluation pipeline, + * complementing the more narrowly scoped {@link BenchmarkValueTranslators}. + * + * @author rubensworks + */ +public class BenchmarkScriptEvaluation { + + static { + // We need the Minecraft registries to be filled + SharedConstants.setVersion(DetectedVersion.BUILT_IN); + Bootstrap.bootStrap(); + } + + private static ValueDeseralizationContext VDC = null; + + private static final String SCRIPT = """ + exports = { + value: 42, + add: (a, b) => a + b, + identity: (a) => a, + useOps: (a) => idContext.ops.numberIncrement(a), + }; + """; + + public static void main(String[] args) throws EvaluationException { + beforeAll(); + + /* +Latest results +Context-createBase: 0.02202ms/op +Context-createBaseJs: 0.3006ms/op +Context-createPopulated: 0.3253ms/op +Script-instantiate: 0.2021ms/op +Script-instantiate-useOps: 0.4355ms/op +Script-memberValue: 0.0001532ms/op +Operator-callJsFromId-int: 0.0004033ms/op +Operator-callJsFromId-item: 0.0009777ms/op +Operator-callIdFromJs-int: 0.0004680ms/op +Operator-roundtrip-int: 0.0006388ms/op +Proxy-unwrapItem: 0.0005577ms/op +Proxy-unwrapNbt: 0.0005382ms/op +Proxy-unwrapOperator: 0.0004444ms/op + */ + + // Context creation, as happens for every (re)instantiated script. + BenchmarkValueTranslators.benchmark("Context-createBase", + () -> ScriptHelpers.createBaseContext(null).close(), 500, 1000); + BenchmarkValueTranslators.benchmark("Context-createBaseJs", () -> { + Context context = ScriptHelpers.createBaseContext(null); + context.getBindings("js"); + context.close(); + }, 500, 1000); + BenchmarkValueTranslators.benchmark("Context-createPopulated", + () -> ScriptHelpers.createPopulatedContext(null, VDC).close(), 500, 1000); + + // Full script instantiation, as happens whenever a script (re)loads. + Source source = Source.newBuilder("js", SCRIPT, "bench.js").buildLiteral(); + BenchmarkValueTranslators.benchmark("Script-instantiate", () -> { + Context context = ScriptHelpers.createPopulatedContext(null, VDC); + try { + context.eval(source); + } finally { + context.close(); + } + }, 300, 1000); + + // The same, but with a script that actually makes use of the global operators. + BenchmarkValueTranslators.benchmark("Script-instantiate-useOps", () -> { + Context context = ScriptHelpers.createPopulatedContext(null, VDC); + try { + context.eval(source); + context.getBindings("js").getMember("exports").getMember("useOps").execute(1); + } finally { + context.close(); + } + }, 300, 1000); + + // Reading a script member value, as happens on every script variable (re)evaluation. + Context context = ScriptHelpers.createPopulatedContext(null, VDC); + context.eval(source); + Value exports = context.getBindings("js").getMember("exports"); + BenchmarkValueTranslators.benchmark("Script-memberValue", () -> { + context.resetLimits(); + ValueTranslators.REGISTRY.translateFromGraal(context, exports.getMember("value"), + ScriptHelpers.getDummyEvaluationExceptionFactory(), VDC); + }, 100000, 200000); + + // Calling a JS function as an Integrated Dynamics operator. + IOperator jsOperatorAdd = getOperator(context, exports, "add"); + IVariable[] intArgs = new IVariable[]{ + new Variable<>(ValueTypeInteger.ValueInteger.of(1)), + new Variable<>(ValueTypeInteger.ValueInteger.of(2)), + }; + BenchmarkValueTranslators.benchmark("Operator-callJsFromId-int", + () -> jsOperatorAdd.evaluate(intArgs), 20000, 50000); + + IOperator jsOperatorIdentity = getOperator(context, exports, "identity"); + IVariable[] itemArgs = new IVariable[]{ + new Variable<>(ValueObjectTypeItemStack.ValueItemStack.of(new ItemStack(Items.ARROW))), + }; + BenchmarkValueTranslators.benchmark("Operator-callJsFromId-item", + () -> jsOperatorIdentity.evaluate(itemArgs), 20000, 50000); + + // Calling an Integrated Dynamics operator from JS. + Value opsCaller = context.eval(Source.newBuilder("js", + "(function() { return idContext.ops.numberIncrement(1); })", "bench-ops.js").buildLiteral()); + BenchmarkValueTranslators.benchmark("Operator-callIdFromJs-int", () -> { + context.resetLimits(); + opsCaller.execute(); + }, 20000, 50000); + + // A JS function that calls into an Integrated Dynamics operator, called from Integrated Dynamics. + IOperator jsOperatorUseOps = getOperator(context, exports, "useOps"); + IVariable[] intArg = new IVariable[]{ new Variable<>(ValueTypeInteger.ValueInteger.of(1)) }; + BenchmarkValueTranslators.benchmark("Operator-roundtrip-int", + () -> jsOperatorUseOps.evaluate(intArg), 20000, 50000); + + // Translating values that were translated in the other direction before (proxy unwrapping), + // as happens for all Integrated Dynamics values that a script passes back unchanged. + runUnwrap(context, "Item", ValueObjectTypeItemStack.ValueItemStack.of(new ItemStack(Items.ARROW))); + runUnwrap(context, "Nbt", ValueTypeNbt.ValueNbt.of(new CompoundTag())); + runUnwrap(context, "Operator", ValueTypeOperator.ValueOperator.of(Operators.ARITHMETIC_ADDITION)); + + context.close(); + } + + private static IOperator getOperator(Context context, Value exports, String member) throws EvaluationException { + return ((ValueTypeOperator.ValueOperator) ValueTranslators.REGISTRY.translateFromGraal(context, + exports.getMember(member), ScriptHelpers.getDummyEvaluationExceptionFactory(), VDC)).getRawValue(); + } + + private static void runUnwrap( + Context context, String label, V value) throws EvaluationException { + Value graalValue = ValueTranslators.REGISTRY.translateToGraal(context, value, + ScriptHelpers.getDummyEvaluationExceptionFactory(), VDC); + BenchmarkValueTranslators.benchmark("Proxy-unwrap" + label, + () -> ValueTranslators.REGISTRY.translateFromGraal(context, graalValue, + ScriptHelpers.getDummyEvaluationExceptionFactory(), VDC), 20000, 50000); + } + + public static void beforeAll() { + ValueTypeListProxyFactories.load(); + Operators.load(); + ValueTranslators.load(); + VDC = ValueDeseralizationContextMocked.get(); + } + +} diff --git a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkValueTranslators.java b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkValueTranslators.java index b47b13b39..a23ea952a 100644 --- a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkValueTranslators.java +++ b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/BenchmarkValueTranslators.java @@ -40,24 +40,24 @@ public static void main(String[] args) { /* Latest results -FromGraal-int: 1.6E-4ms/op -FromGraal-boolean: 1.8E-4ms/op -FromGraal-double: 3.1E-4ms/op -FromGraal-long: 1.1E-4ms/op -FromGraal-string: 1.3E-4ms/op -FromGraal-list: 0.0033ms/op -FromGraal-operator: 0.00185ms/op -FromGraal-nbt: 0.0222ms/op -FromGraal-item: 0.00963ms/op -ToGraal-int: 3.1E-4ms/op -ToGraal-boolean: 2.5E-4ms/op -ToGraal-double: 2.9E-4ms/op -ToGraal-long: 2.8E-4ms/op -ToGraal-string: 2.5E-4ms/op -ToGraal-list: 8.3E-4ms/op -ToGraal-operator: 3.1E-4ms/op -ToGraal-nbt: 4.2E-4ms/op -ToGraal-item: 6.7E-4ms/op +FromGraal-int: 0.000005884ms/op +FromGraal-boolean: 0.00002270ms/op +FromGraal-double: 0.00003193ms/op +FromGraal-long: 0.00002828ms/op +FromGraal-string: 0.00004549ms/op +FromGraal-list: 0.0007997ms/op +FromGraal-operator: 0.0004249ms/op +FromGraal-nbt: 0.008248ms/op +FromGraal-item: 0.004357ms/op +ToGraal-int: 0.00009166ms/op +ToGraal-boolean: 0.000005404ms/op +ToGraal-double: 0.00006237ms/op +ToGraal-long: 0.00006071ms/op +ToGraal-string: 0.00004514ms/op +ToGraal-list: 0.0002058ms/op +ToGraal-operator: 0.00007643ms/op +ToGraal-nbt: 0.00007930ms/op +ToGraal-item: 0.00007836ms/op */ runFromGraal("int", getJsValue("10"), REPLICATION); @@ -115,17 +115,36 @@ private static void runToGraal(String label, IValue idValue, int replication) { } public static void benchmark(String label, ThrowingRunnable runnable, int replication) { - long startTime = System.currentTimeMillis(); + benchmark(label, runnable, replication / 10, replication); + } + + public static int ROUNDS = 5; + + public static void benchmark(String label, ThrowingRunnable runnable, int warmup, int replication) { try { - for (int i = 0; i < replication; i++) { + // Warm up the JIT (and Graal's own profiling) before measuring. + for (int i = 0; i < warmup; i++) { runnable.run(); } + + // Run multiple rounds, and report the fastest one, + // as that is the least affected by GC pauses and other noise. + long best = Long.MAX_VALUE; + for (int round = 0; round < ROUNDS; round++) { + long startTime = System.nanoTime(); + for (int i = 0; i < replication; i++) { + runnable.run(); + } + best = Math.min(best, System.nanoTime() - startTime); + } + System.out.println(label + ": " + format(((double) best) / replication / 1_000_000D) + "ms/op"); } catch (EvaluationException e) { e.printStackTrace(); } - long stopTime = System.currentTimeMillis(); - long elapsedTime = stopTime - startTime; - System.out.println(label + ": " + ((double) elapsedTime) / replication + "ms/op"); + } + + private static String format(double value) { + return new java.math.BigDecimal(value).round(new java.math.MathContext(4)).toPlainString(); } @FunctionalInterface From 0c6cee88e5560542d7d8ea51609dcf7a932ec4f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:38:22 +0000 Subject: [PATCH 2/6] Lazily populate idContext.ops in script contexts Every created script context eagerly translated all 276 global interact operators into idContext.ops, which took longer than the rest of the script instantiation combined, while many scripts use only a handful of them, or none at all. The ops object is now defined as a self-replacing lazy getter, so that it is only built once a script actually accesses it. After the first access it is a plain data property again, so repeated idContext.ops accesses stay as fast as before. Exposing ops as a host proxy object instead would have made every access cross the host boundary, which measured ~9% slower per operator call. Script-instantiate 388us -> 197us (-49%) Context-createPopulated 425us -> 259us (-39%) Instantiating a script that does use idContext.ops becomes ~8% slower, as it now pays for the lazy getter on top of building the ops object. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- .../evaluate/ScriptHelpers.java | 51 ++++++++++++++++--- .../ValueTranslatorsJavaScriptTests.java | 12 +++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java b/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java index f59c5067d..997f90eff 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java @@ -11,6 +11,7 @@ import org.cyclops.integratedscripting.core.packageddependencies.UnsafeHelper; import org.cyclops.integratedscripting.evaluate.translation.ValueTranslators; import org.graalvm.polyglot.*; +import org.graalvm.polyglot.proxy.ProxyExecutable; import javax.annotation.Nullable; import java.nio.file.Path; @@ -35,6 +36,32 @@ public class ScriptHelpers { } } + /** + * A factory for the {@code idContext} object, with {@code ops} defined as a self-replacing lazy getter. + * This way, the global operators are only translated once a script actually accesses them, + * while accesses after the first one are plain property reads. + */ + private static final Source SOURCE_ID_CONTEXT = Source.newBuilder("js", """ + (function(resolveOps) { + var idContext = {}; + Object.defineProperty(idContext, 'ops', { + configurable: true, + enumerable: true, + get: function() { + var ops = resolveOps(); + Object.defineProperty(idContext, 'ops', { + value: ops, + configurable: true, + enumerable: true, + writable: true, + }); + return ops; + }, + }); + return idContext; + }) + """, "integratedscripting_idcontext.js").buildLiteral(); + public static void load() { // Do nothing } @@ -78,15 +105,23 @@ public static Context createBaseContext(@Nullable Function contextBuilderModifier, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { Context context = createBaseContext(contextBuilderModifier); - // Create idContext field with ops + // Create idContext field with ops. + // The ops object is populated lazily, because translating all global operators is expensive, + // while many scripts never touch them. Value jsBindings = context.getBindings("js"); - Value jsObjectClass = jsBindings.getMember("Object"); - Value idContext = jsObjectClass.newInstance(); - Value ops = jsObjectClass.newInstance(); - for (Map.Entry entry : Operators.REGISTRY.getGlobalInteractOperators().entrySet()) { - ops.putMember(entry.getKey(), ValueTranslators.REGISTRY.translateToGraal(context, ValueTypeOperator.ValueOperator.of(entry.getValue()), getDummyEvaluationExceptionFactory(), valueDeseralizationContext)); - } - idContext.putMember("ops", ops); + Value idContext = context.eval(SOURCE_ID_CONTEXT).execute((ProxyExecutable) args -> { + Value ops = jsBindings.getMember("Object").newInstance(); + try { + for (Map.Entry entry : Operators.REGISTRY.getGlobalInteractOperators().entrySet()) { + ops.putMember(entry.getKey(), ValueTranslators.REGISTRY.translateToGraal(context, + ValueTypeOperator.ValueOperator.of(entry.getValue()), + getDummyEvaluationExceptionFactory(), valueDeseralizationContext)); + } + } catch (EvaluationException e) { + throw new RuntimeException(e); + } + return ops; + }); jsBindings.putMember("idContext", idContext); return context; diff --git a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java index cfeb24334..cbad73546 100644 --- a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java +++ b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java @@ -503,6 +503,18 @@ public void testObjectFluidMethods() throws EvaluationException { // Entity, ingredients, and recipe are not easily testable + @Test + public void testGlobalFunctionsLazyResolution() { + Value idContext = CTX.getBindings("js").getMember("idContext"); + + // The ops object is resolved lazily, but must be stable once resolved. + Value ops = idContext.getMember("ops"); + assertThat(ops.hasMembers(), is(true)); + assertThat(ops.getMemberKeys().isEmpty(), is(false)); + assertThat(CTX.eval("js", "idContext.ops === idContext.ops").asBoolean(), is(true)); + assertThat(CTX.eval("js", "Object.keys(idContext.ops).length > 0").asBoolean(), is(true)); + } + @Test public void testGlobalFunctions() throws EvaluationException { Value ops = CTX.getBindings("js").getMember("idContext").getMember("ops"); From c3a7de432e4704e2d0b5c1798e706a4764e9bf09 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:40:19 +0000 Subject: [PATCH 3/6] Don't overwrite the exports binding when translating an NBT end tag Translating an NBT end tag to Graal evaluated "exports = { 'nbt_end': true }", which assigns to the global exports binding, and thereby silently discarded whatever the script itself had exported. It now uses a proxy object with the same single member, which as a side effect also removes a JS parse and evaluation from that path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- .../translation/translator/ValueTranslatorNbt.java | 9 +++++++-- .../translation/ValueTranslatorsJavaScriptTests.java | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java index 0bfe48d4d..1bf558a64 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java @@ -1,5 +1,6 @@ package org.cyclops.integratedscripting.evaluate.translation.translator; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import net.minecraft.nbt.*; import net.minecraft.network.chat.Component; @@ -15,6 +16,7 @@ import org.cyclops.integratedscripting.evaluate.translation.ValueTranslators; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyObject; import javax.annotation.Nullable; import java.util.ArrayList; @@ -26,6 +28,9 @@ */ public class ValueTranslatorNbt implements IValueTranslator { + private static final String KEY_END_TAG = "nbt_end"; + private static final ProxyObject PROXY_END_TAG = ProxyObject.fromMap(ImmutableMap.of(KEY_END_TAG, true)); + @Override public IValueType getValueType() { return ValueTypes.NBT; @@ -53,7 +58,7 @@ public Value translateToGraal(Context context, ValueTypeNbt.ValueNbt value, IEva public Value translateTag(Context context, Tag tag, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { switch (tag.getId()) { case Tag.TAG_END -> { - return context.eval("js", "exports = { 'nbt_end': true }"); + return context.asValue(PROXY_END_TAG); } case Tag.TAG_BYTE -> { return context.asValue(((ByteTag) tag).getAsByte()); @@ -116,7 +121,7 @@ public ValueTypeNbt.ValueNbt translateFromGraal(Context context, Value value, IE } } - if (value.getMemberKeys().equals(Sets.newHashSet("nbt_end"))) { + if (value.getMemberKeys().equals(Sets.newHashSet(KEY_END_TAG))) { return ValueTypeNbt.ValueNbt.of(EndTag.INSTANCE); } diff --git a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java index cbad73546..884c8c9fb 100644 --- a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java +++ b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorsJavaScriptTests.java @@ -503,6 +503,13 @@ public void testObjectFluidMethods() throws EvaluationException { // Entity, ingredients, and recipe are not easily testable + @Test + public void testNbtEndTagDoesNotOverrideExports() throws EvaluationException { + CTX.eval("js", "exports = { marker: 1 };"); + ValueTranslators.REGISTRY.translateToGraal(CTX, ValueTypeNbt.ValueNbt.of(EndTag.INSTANCE), EF, VDC); + assertThat(CTX.getBindings("js").getMember("exports").getMemberKeys(), equalTo(Sets.newHashSet("marker"))); + } + @Test public void testGlobalFunctionsLazyResolution() { Value idContext = CTX.getBindings("js").getMember("idContext"); From 42249e5999de032c083623a382844ff8a43c1812 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:41:56 +0000 Subject: [PATCH 4/6] Unwrap Graal proxies with instanceof instead of ClassCastException The translators that unwrap values which were translated to Graal before cast the proxy and caught the resulting ClassCastException to detect a mismatch. Since the translators are tried in order, translating any proxy threw and caught up to seven exceptions before reaching its own translator, each with the stack trace fill-in that entails. Proxy-unwrapNbt 2.67us -> 1.10us (-59%) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- .../translator/ValueTranslatorNbt.java | 9 ++------- .../ValueTranslatorObjectAdapter.java | 19 ++++++------------- .../translator/ValueTranslatorOperator.java | 9 ++------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java index 1bf558a64..aec449a83 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorNbt.java @@ -112,13 +112,8 @@ public Value translateCompoundTag(Context context, CompoundTag tag, IEvaluationE @Override public ValueTypeNbt.ValueNbt translateFromGraal(Context context, Value value, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { // Unwrap the value if it was translated in the opposite direction before. - if (value.isProxyObject()) { - try { - NbtCompoundTagProxyObject proxy = value.asProxyObject(); - return ValueTypeNbt.ValueNbt.of(proxy.getTag()); - } catch (ClassCastException classCastException) { - // Fallback to case below - } + if (value.isProxyObject() && value.asProxyObject() instanceof NbtCompoundTagProxyObject proxy) { + return ValueTypeNbt.ValueNbt.of(proxy.getTag()); } if (value.getMemberKeys().equals(Sets.newHashSet(KEY_END_TAG))) { diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java index 7d06c7f69..1c29c9515 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java @@ -53,12 +53,10 @@ public IValueType getValueType() { @Override public boolean canHandleGraalValue(Value value) { if (value.isProxyObject()) { - try { - ValueObjectProxyObject proxyObject = value.asProxyObject(); - return proxyObject.getValue() != null && proxyObject.getValue().getType() == this.valueType; - } catch (ClassCastException e) { - // Ignore error - } + Object proxyObject = value.asProxyObject(); + return proxyObject instanceof ValueObjectProxyObject valueObjectProxyObject + && valueObjectProxyObject.getValue() != null + && valueObjectProxyObject.getValue().getType() == this.valueType; } return value.getMemberKeys().equals(this.keys); } @@ -103,13 +101,8 @@ public Value translateToGraal(Context context, V value, IEvaluationExceptionFact @Override public V translateFromGraal(Context context, Value value, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { // Unwrap the value if it was translated in the opposite direction before. - if (value.isProxyObject()) { - try { - ValueObjectProxyObject proxyObject = value.asProxyObject(); - return (V) proxyObject.getValue(); - } catch (ClassCastException e) { - // Fallback to case below - } + if (value.isProxyObject() && value.asProxyObject() instanceof ValueObjectProxyObject proxyObject) { + return (V) proxyObject.getValue(); } Value idBlock = value.getMember(this.key); diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorOperator.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorOperator.java index 44b2a2294..7d6594c12 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorOperator.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorOperator.java @@ -51,13 +51,8 @@ public Value translateToGraal(Context context, ValueTypeOperator.ValueOperator v @Override public ValueTypeOperator.ValueOperator translateFromGraal(Context context, Value value, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { // Unwrap the value if it was translated in the opposite direction before. - if (value.isProxyObject()) { - try { - OperatorProxyExecutable cast = value.asProxyObject(); - return cast.getValue(); - } catch (ClassCastException classCastException) { - // Fallback to case below - } + if (value.isProxyObject() && value.asProxyObject() instanceof OperatorProxyExecutable proxy) { + return proxy.getValue(); } // Determine input args of the function From 397aa9c8af618fc9de0a90305e43bc96aad06aa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:49:15 +0000 Subject: [PATCH 5/6] Dispatch object value translators on their member key Determining which translator handles a Graal value scanned all translators in order, and each of the six object translators materialized the value's member keys to compare them against its own single key. For a plain object that meant six member key sets and six host boundary crossings before the NBT translator was even reached. Translators can now report the single member key they dispatch on, so the registry materializes the member key set once and matches all of them against it. The translator list and their keys are snapshotted on registration, so dispatching doesn't repeat the lookups either. Median of 3 paired runs: FromGraal-nbt 10.63us -> 7.56us (-29%) FromGraal-item 5.10us -> 3.73us (-27%) This does regress values that were translated to Graal before, since the object translators used to detect their own proxies before looking at any member keys: Proxy-unwrapItem 0.59us -> 1.17us (+99%) The next commit more than makes up for that by resolving such values from their proxy directly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- .../translation/IValueTranslator.java | 18 +++++++++ .../translation/ValueTranslatorRegistry.java | 38 +++++++++++++++++-- .../ValueTranslatorObjectAdapter.java | 11 ++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueTranslator.java b/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueTranslator.java index d76e48638..42fe73baf 100644 --- a/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueTranslator.java +++ b/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueTranslator.java @@ -8,6 +8,8 @@ import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; +import javax.annotation.Nullable; + /** * Translates ID values to and from Graal values. * @author rubensworks @@ -18,6 +20,22 @@ public interface IValueTranslator { public boolean canHandleGraalValue(Value value); + /** + * If this translator handles Graal values that have exactly one member with a fixed key, + * then returning that key here allows {@link IValueTranslatorRegistry} to dispatch on it directly. + * + * This is purely an optimization: it avoids having to inspect the member keys of a value + * once for every such translator, which is relatively expensive as it crosses the host boundary. + * Translators returning a non-null key here must handle exactly those Graal values + * whose member keys are exactly the returned key. + * + * @return The single member key this translator dispatches on, or null if it dispatches differently. + */ + @Nullable + public default String getGraalValueMemberKey() { + return null; + } + boolean canTranslateNbt(); public Value translateToGraal(Context context, V value, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException; diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java index 75eb2e549..59848c2d8 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; /** * @author rubensworks @@ -27,6 +28,10 @@ public class ValueTranslatorRegistry implements IValueTranslatorRegistry { private final List translators = Lists.newArrayList(); private final Map, IValueTranslator> valueTypeTranslators = Maps.newIdentityHashMap(); + // Snapshots of translators, and the member keys they dispatch on, to avoid repeated lookups while dispatching. + private IValueTranslator[] translatorsArray = new IValueTranslator[0]; + private String[] translatorMemberKeys = new String[0]; + private ValueTranslatorRegistry() { } @@ -41,6 +46,11 @@ public static ValueTranslatorRegistry getInstance() { public void register(IValueTranslator translator) { translators.add(translator); valueTypeTranslators.put(translator.getValueType(), translator); + + this.translatorsArray = translators.toArray(new IValueTranslator[0]); + this.translatorMemberKeys = translators.stream() + .map(IValueTranslator::getGraalValueMemberKey) + .toArray(String[]::new); } @Override @@ -59,9 +69,31 @@ public Value translateToGraal(Context context, V value, IEval @Override public IValueTranslator getScriptValueTranslator(Value scriptValue) { - for (IValueTranslator translator : translators) { - if (translator.canHandleGraalValue(scriptValue)) { - return translator; + // Translators that dispatch on a single member key are all matched against the same member key set, + // which is only materialized once, and only once such a translator is actually reached. + // Crossing the host boundary is relatively expensive, + // so the number of calls on the Graal value is deliberately kept as low as possible here. + Set valueMemberKeys = null; + boolean valueMembersResolved = false; + + IValueTranslator[] translators = this.translatorsArray; + String[] translatorMemberKeys = this.translatorMemberKeys; + for (int i = 0; i < translators.length; i++) { + String translatorMemberKey = translatorMemberKeys[i]; + if (translatorMemberKey == null) { + if (translators[i].canHandleGraalValue(scriptValue)) { + return translators[i]; + } + } else { + if (!valueMembersResolved) { + valueMembersResolved = true; + valueMemberKeys = scriptValue.hasMembers() ? scriptValue.getMemberKeys() : null; + } + if (valueMemberKeys != null + && valueMemberKeys.size() == 1 + && valueMemberKeys.contains(translatorMemberKey)) { + return translators[i]; + } } } return null; diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java index 1c29c9515..aa9c5f663 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueTranslatorObjectAdapter.java @@ -29,7 +29,6 @@ public class ValueTranslatorObjectAdapter implements IValueTranslator { private final String key; - private final Set keys; private final ValueObjectTypeBase valueType; @Nullable @@ -37,7 +36,6 @@ public class ValueTranslatorObjectAdapter implements IValueTra public ValueTranslatorObjectAdapter(String key, ValueObjectTypeBase valueType) { this.key = key; - this.keys = Sets.newHashSet(this.key); this.valueType = valueType; } @@ -58,7 +56,14 @@ public boolean canHandleGraalValue(Value value) { && valueObjectProxyObject.getValue() != null && valueObjectProxyObject.getValue().getType() == this.valueType; } - return value.getMemberKeys().equals(this.keys); + Set memberKeys = value.getMemberKeys(); + return memberKeys.size() == 1 && memberKeys.contains(this.key); + } + + @Nullable + @Override + public String getGraalValueMemberKey() { + return this.key; } @Override From 57102d93251a8fc6fccb45e24b512b358a6a394f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:54:57 +0000 Subject: [PATCH 6/6] Resolve value translators for round-tripped Graal proxies directly Values that a script passes back unchanged are Graal proxies wrapping an Integrated Dynamics value, so their translator is known up front, but they still went through the full translator scan to find it. Graal proxies wrapping such a value now report their value type through IValueProxy, which lets the registry look their translator up in two calls on the Graal value instead of scanning. Median of 3 paired runs: Proxy-unwrapItem 1.28us -> 0.54us (-58%) Operator-callJsFromId-item 1.71us -> 1.03us (-40%) Proxy-unwrapNbt 0.93us -> 0.56us (-39%) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014MtPuLF94baQNqedrqSY63 --- .../api/evaluate/translation/IValueProxy.java | 20 +++++++++++++++++++ .../translation/ValueTranslatorRegistry.java | 18 +++++++++++++++++ .../translator/NbtCompoundTagProxyObject.java | 10 +++++++++- .../translator/OperatorProxyExecutable.java | 10 +++++++++- .../translator/ValueObjectProxyObject.java | 9 ++++++++- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueProxy.java diff --git a/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueProxy.java b/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueProxy.java new file mode 100644 index 000000000..8ea561aa9 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/api/evaluate/translation/IValueProxy.java @@ -0,0 +1,20 @@ +package org.cyclops.integratedscripting.api.evaluate.translation; + +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; + +/** + * A Graal proxy that wraps an Integrated Dynamics value of a known value type. + * + * Proxies implementing this interface can be mapped to their {@link IValueTranslator} directly, + * instead of having to fall back to a linear scan over all registered translators. + * + * @author rubensworks + */ +public interface IValueProxy { + + /** + * @return The value type of the Integrated Dynamics value that is being proxied. + */ + public IValueType getProxiedValueType(); + +} diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java index 59848c2d8..4fa7ff825 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java @@ -9,11 +9,13 @@ import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integratedscripting.api.evaluate.translation.IEvaluationExceptionFactory; +import org.cyclops.integratedscripting.api.evaluate.translation.IValueProxy; import org.cyclops.integratedscripting.api.evaluate.translation.IValueTranslator; import org.cyclops.integratedscripting.api.evaluate.translation.IValueTranslatorRegistry; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Set; @@ -87,6 +89,14 @@ public IValueTranslator getScriptValueTranslator(Value scriptValue) { } else { if (!valueMembersResolved) { valueMembersResolved = true; + + // Fast path for values that were translated to Graal before: + // their proxy directly tells us which value type they correspond to. + IValueTranslator proxiedTranslator = getProxiedValueTranslator(scriptValue); + if (proxiedTranslator != null) { + return proxiedTranslator; + } + valueMemberKeys = scriptValue.hasMembers() ? scriptValue.getMemberKeys() : null; } if (valueMemberKeys != null @@ -99,6 +109,14 @@ public IValueTranslator getScriptValueTranslator(Value scriptValue) { return null; } + @Nullable + protected IValueTranslator getProxiedValueTranslator(Value scriptValue) { + if (scriptValue.isProxyObject() && scriptValue.asProxyObject() instanceof IValueProxy valueProxy) { + return getValueTypeTranslator(valueProxy.getProxiedValueType()); + } + return null; + } + @Override public V translateFromGraal(Context context, Value value, IEvaluationExceptionFactory exceptionFactory, ValueDeseralizationContext valueDeseralizationContext) throws EvaluationException { IValueTranslator translator = getScriptValueTranslator(value); diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/NbtCompoundTagProxyObject.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/NbtCompoundTagProxyObject.java index 340a2ffcb..6a5fb014f 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/NbtCompoundTagProxyObject.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/NbtCompoundTagProxyObject.java @@ -4,7 +4,10 @@ import net.minecraft.nbt.CompoundTag; import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; +import org.cyclops.integratedscripting.api.evaluate.translation.IValueProxy; import org.cyclops.integrateddynamics.core.evaluate.operator.CurriedOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.Variable; @@ -21,7 +24,7 @@ * A Graal proxy object for NBT CompoundTag values. * @author rubensworks */ -public class NbtCompoundTagProxyObject implements ProxyObject { +public class NbtCompoundTagProxyObject implements ProxyObject, IValueProxy { private final Context context; private final IEvaluationExceptionFactory exceptionFactory; @@ -46,6 +49,11 @@ public CompoundTag getTag() { return tag; } + @Override + public IValueType getProxiedValueType() { + return ValueTypes.NBT; + } + @Nullable public IValue getValue() { return value; diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java index 3711aff0d..b1575fad5 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java @@ -6,7 +6,10 @@ import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.Variable; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; import org.cyclops.integratedscripting.api.evaluate.translation.IEvaluationExceptionFactory; +import org.cyclops.integratedscripting.api.evaluate.translation.IValueProxy; import org.cyclops.integratedscripting.evaluate.translation.ValueTranslators; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; @@ -16,7 +19,7 @@ * A Graal proxy executable for operator values. * @author rubensworks */ -public class OperatorProxyExecutable implements ProxyExecutable { +public class OperatorProxyExecutable implements ProxyExecutable, IValueProxy { private final Context context; private final ValueTypeOperator.ValueOperator value; private final IEvaluationExceptionFactory exceptionFactory; @@ -33,6 +36,11 @@ public ValueTypeOperator.ValueOperator getValue() { return value; } + @Override + public IValueType getProxiedValueType() { + return ValueTypes.OPERATOR; + } + @SneakyThrows @Override public Object execute(Value... args) { diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueObjectProxyObject.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueObjectProxyObject.java index 4641f8353..0f3bb9c93 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueObjectProxyObject.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/ValueObjectProxyObject.java @@ -5,12 +5,14 @@ import net.minecraft.network.chat.Component; import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.core.evaluate.operator.CurriedOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueObjectTypeBase; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.Variable; import org.cyclops.integratedscripting.api.evaluate.translation.IEvaluationExceptionFactory; +import org.cyclops.integratedscripting.api.evaluate.translation.IValueProxy; import org.cyclops.integratedscripting.evaluate.translation.ValueTranslators; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; @@ -23,7 +25,7 @@ * A Graal proxy object for object values. * @author rubensworks */ -public class ValueObjectProxyObject implements ProxyObject { +public class ValueObjectProxyObject implements ProxyObject, IValueProxy { private final Context context; private final IEvaluationExceptionFactory exceptionFactory; @@ -52,6 +54,11 @@ public ValueObjectTypeBase getValueType() { return valueType; } + @Override + public IValueType getProxiedValueType() { + return this.valueType; + } + @Nullable public IValue getValue() { return value;