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:
+ *
+ * - Signature.validate: called on every function invocation; variadic params ('+') make the
+ * matched signature group longer than one char, which hits the per-char split loop
+ * - Tokenizer number regex: evaluated for every token that is not a string literal
+ *
+ * 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");
+ }
+}