diff --git a/src/main/java/com/dashjoin/jsonata/Tokenizer.java b/src/main/java/com/dashjoin/jsonata/Tokenizer.java index fb538e7..89142c6 100644 --- a/src/main/java/com/dashjoin/jsonata/Tokenizer.java +++ b/src/main/java/com/dashjoin/jsonata/Tokenizer.java @@ -83,6 +83,9 @@ public class Tokenizer implements java.io.Serializable { // = function (path) { put("t", "\t"); }}; + // compiled once (like the regex literal in jsonata-js), not per token + static final Pattern numregex = Pattern.compile("^-?(0|([1-9][0-9]*))(\\.[0-9]+)?([Ee][-+]?[0-9]+)?"); + // Tokenizer (lexer) - invoked by the parser to return one token at a time String path; int position = 0; @@ -296,7 +299,6 @@ Token next(boolean prefix) { throw new JException("S0101", position); } // test for numbers - Pattern numregex = Pattern.compile("^-?(0|([1-9][0-9]*))(\\.[0-9]+)?([Ee][-+]?[0-9]+)?"); Matcher match = numregex.matcher(path.substring(position)); if (match.find()) { double num = Double.parseDouble(match.group(0)); diff --git a/src/main/java/com/dashjoin/jsonata/utils/Signature.java b/src/main/java/com/dashjoin/jsonata/utils/Signature.java index 875af74..b5e515f 100644 --- a/src/main/java/com/dashjoin/jsonata/utils/Signature.java +++ b/src/main/java/com/dashjoin/jsonata/utils/Signature.java @@ -299,12 +299,11 @@ void throwValidationError(List badArgs, String badSig, String functionName) { @SuppressWarnings({"rawtypes", "unchecked"}) public Object validate(Object _args, Object context) { - var result = new ArrayList<>(); - var args = (List)_args; - String suppliedSig = ""; + StringBuilder sb = new StringBuilder(args.size()); for (Object arg : args) - suppliedSig += getSymbol(arg); + sb.append(getSymbol(arg)); + String suppliedSig = sb.toString(); Matcher isValid = _regex.matcher(suppliedSig); if (isValid != null && isValid.matches()) { @@ -338,11 +337,11 @@ public Object validate(Object _args, Object context) { } else { // may have matched multiple args (if the regex ends with a '+' // split into single tokens - String[] singles = match.split(""); - for (String single : singles) { + // toCharArray instead of split("") which compiles a regex on every call + for (char single : match.toCharArray()) { //match.split('').forEach(function (single) { if (param.type.equals("a")) { - if (single.equals("m")) { + if (single == 'm') { // missing (undefined) arg = null; } else { @@ -350,9 +349,9 @@ public Object validate(Object _args, Object context) { var arrayOK = true; // is there type information on the contents of the array? if (param.subtype != null) { - if (!single.equals("a") && !match.equals(param.subtype)) { + if (single != 'a' && !match.equals(param.subtype)) { arrayOK = false; - } else if (single.equals("a")) { + } else if (single == 'a') { List argArr = (List)arg; if (argArr.size() > 0) { var itemType = getSymbol(argArr.get(0)); @@ -378,7 +377,7 @@ public Object validate(Object _args, Object context) { ); } // the function expects an array. If it's not one, make it so - if (!single.equals("a")) { + if (single != 'a') { List _arg = new ArrayList<>(); _arg.add(arg); arg = _arg; } diff --git a/src/test/java/com/dashjoin/jsonata/HotspotTest.java b/src/test/java/com/dashjoin/jsonata/HotspotTest.java new file mode 100644 index 0000000..fca3110 --- /dev/null +++ b/src/test/java/com/dashjoin/jsonata/HotspotTest.java @@ -0,0 +1,129 @@ +package com.dashjoin.jsonata; + +import static com.dashjoin.jsonata.Jsonata.jsonata; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import com.dashjoin.jsonata.Jsonata.JFunction; + +/** + * Exercises two hot paths discussed in PR #102: + * + * The correctness tests guard the semantics; the loop tests print timings so both code paths can + * be profiled / compared before and after an optimization. + */ +public class HotspotTest { + + // override with -Dhotspot.iterations=... for a longer run; each loop runs once as warmup first + static final int ITERATIONS = Integer.getInteger("hotspot.iterations", 20_000); + + // --------------------------------------------------------------------------------------------- + // Signature.validate + // --------------------------------------------------------------------------------------------- + + /** + * $zip has signature <a+>; three array args produce the match "aaa" + */ + @Test + public void signatureVariadicArrays() { + Assertions.assertEquals( + Arrays.asList(Arrays.asList(1, 3, 5), Arrays.asList(2, 4, 6)), + jsonata("$zip([1,2],[3,4],[5,6])").evaluate(null)); + } + + /** + * a non-array arg for an 'a' param must be wrapped into an array (single != 'a' branch) + */ + @Test + public void signatureVariadicWrapsNonArrays() { + Assertions.assertEquals( + Arrays.asList(Arrays.asList(1, 3, 5)), + jsonata("$zip(1,[3],5)").evaluate(null)); + } + + /** + * non-array variadic param (else branch): match "nnnn" + */ + @Test + public void signatureVariadicNumbers() { + Jsonata expr = jsonata("$add(1,2,3,4)"); + expr.registerFunction("add", new JFunction((input, args) -> { + double sum = 0; + for (Object o : args) + sum += ((Number) o).doubleValue(); + return sum; + }, "")); + Assertions.assertEquals(10, ((Number) expr.evaluate(null)).intValue()); + } + + /** + * typed array subtype check: a<n>+ + */ + @Test + public void signatureVariadicTypedArrays() { + Jsonata expr = jsonata("$count2([1,2],[3])"); + expr.registerFunction("count2", new JFunction((input, args) -> args.size(), "+:n>")); + Assertions.assertEquals(2, expr.evaluate(null)); + + Jsonata bad = jsonata("$count2([1,2],['x'])"); + bad.registerFunction("count2", new JFunction((input, args) -> args.size(), "+:n>")); + Assertions.assertThrows(JException.class, () -> bad.evaluate(null)); + } + + @Test + public void signatureLoop() { + Jsonata expr = jsonata("$zip([1,2],[3,4],[5,6],[7,8])"); + for (int i = 0; i < ITERATIONS; i++) + expr.evaluate(null); + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) + expr.evaluate(null); + System.out.println("Signature.validate x" + ITERATIONS + ": " + + (System.nanoTime() - start) / 1_000_000 + " ms"); + } + + // --------------------------------------------------------------------------------------------- + // Tokenizer number regex + // --------------------------------------------------------------------------------------------- + + @Test + public void tokenizerNumbers() { + Assertions.assertEquals(0, jsonata("0").evaluate(null)); + Assertions.assertEquals(-42, jsonata("-42").evaluate(null)); + Assertions.assertEquals(3.25, jsonata("3.25").evaluate(null)); + Assertions.assertEquals(1500, jsonata("1.5e3").evaluate(null)); + Assertions.assertEquals(0.015, jsonata("1.5E-2").evaluate(null)); + Assertions.assertEquals(List.of(1, 2.5, 300), jsonata("[1, 2.5, 3e2]").evaluate(null)); + Assertions.assertEquals(9, jsonata("1+2*4").evaluate(null)); + } + + /** + * builds a long expression so the tokenizer runs the number regex many times per parse + */ + @Test + public void tokenizerLoop() { + StringBuilder sb = new StringBuilder("$sum(["); + for (int i = 0; i < 200; i++) { + if (i > 0) + sb.append(','); + sb.append(i).append(".5e0"); + } + sb.append("])"); + String src = sb.toString(); + + Assertions.assertEquals(20000, jsonata(src).evaluate(null)); + + for (int i = 0; i < ITERATIONS / 10; i++) + jsonata(src); + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS / 10; i++) + jsonata(src); + System.out.println("Tokenizer numbers x" + (ITERATIONS / 10) + " parses: " + + (System.nanoTime() - start) / 1_000_000 + " ms"); + } +}