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/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/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/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/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/ValueTranslatorRegistry.java index 75eb2e549..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,13 +9,16 @@ 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; /** * @author rubensworks @@ -27,6 +30,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 +48,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,14 +71,52 @@ 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; + + // 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 + && valueMemberKeys.size() == 1 + && valueMemberKeys.contains(translatorMemberKey)) { + return translators[i]; + } } } 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; 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..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 @@ -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()); @@ -107,16 +112,11 @@ 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("nbt_end"))) { + if (value.getMemberKeys().equals(Sets.newHashSet(KEY_END_TAG))) { return ValueTypeNbt.ValueNbt.of(EndTag.INSTANCE); } 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..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; } @@ -53,14 +51,19 @@ 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); + Set memberKeys = value.getMemberKeys(); + return memberKeys.size() == 1 && memberKeys.contains(this.key); + } + + @Nullable + @Override + public String getGraalValueMemberKey() { + return this.key; } @Override @@ -103,13 +106,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 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 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..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,25 @@ 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"); + + // 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");