Skip to content

Commit f538650

Browse files
committed
Further improve interpreted mode performance when the optimizer is disabled. This should make interpreted mode in JLE within 3% of JLJS, and optimized interpreted mode about 2.3x faster
1 parent da5531a commit f538650

File tree

6 files changed

+81
-38
lines changed

6 files changed

+81
-38
lines changed

asyncLogic.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,21 @@ class AsyncLogicEngine {
4646
* @param {*} logic The logic being executed.
4747
* @param {*} context The context of the logic being run (input to the function.)
4848
* @param {*} above The context above (can be used for handlebars-style data traversal.)
49-
* @returns {Promise<{ func: string, result: * }>}
49+
* @returns {Promise<*>}
5050
*/
5151
async _parse (logic, context, above) {
5252
const [func] = Object.keys(logic)
5353
const data = logic[func]
5454

55-
if (this.isData(logic, func)) return { result: logic, func }
55+
if (this.isData(logic, func)) return logic
5656

5757
if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
5858

5959
if (typeof this.methods[func] === 'function') {
6060
const input = await this.run(data, context, { above })
6161

6262
const result = await this.methods[func](input, context, above, this)
63-
return { result: Array.isArray(result) ? Promise.all(result) : result, func }
63+
return Array.isArray(result) ? Promise.all(result) : result
6464
}
6565

6666
if (typeof this.methods[func] === 'object') {
@@ -77,7 +77,7 @@ class AsyncLogicEngine {
7777
above,
7878
this
7979
)
80-
return { result: Array.isArray(result) ? Promise.all(result) : result, func }
80+
return Array.isArray(result) ? Promise.all(result) : result
8181
}
8282

8383
throw new Error(`Method '${func}' is not set up properly.`)
@@ -176,10 +176,7 @@ class AsyncLogicEngine {
176176
return result
177177
}
178178

179-
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) {
180-
const { result } = await this._parse(logic, data, above)
181-
return result
182-
}
179+
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above)
183180

184181
return logic
185182
}

defaultMethods.js

+47-18
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import declareSync from './utilities/declareSync.js'
77
import { build, buildString } from './compiler.js'
88
import chainingSupported from './utilities/chainingSupported.js'
99
import InvalidControlInput from './errors/InvalidControlInput.js'
10-
import { splitPath } from './utilities/splitPath.js'
10+
import { splitPathMemoized } from './utilities/splitPath.js'
1111

1212
function isDeterministic (method, engine, buildState) {
1313
if (Array.isArray(method)) {
@@ -34,17 +34,36 @@ function isDeterministic (method, engine, buildState) {
3434
}
3535

3636
const defaultMethods = {
37-
'+': (data) => [].concat(data).reduce((a, b) => +a + +b, 0),
38-
'*': (data) => data.reduce((a, b) => +a * +b),
39-
'/': (data) => data.reduce((a, b) => +a / +b),
40-
41-
'-': (data) =>
42-
// @ts-ignore Type checking is incorrect on the following line.
43-
((a) => (a.length === 1 ? (a[0] = -a[0]) : a) & 0 || a)(
44-
[].concat(data)
45-
// @ts-ignore Type checking is incorrect on the following line.
46-
).reduce((a, b) => +a - +b),
47-
'%': (data) => data.reduce((a, b) => +a % +b),
37+
'+': (data) => {
38+
if (typeof data === 'string') return +data
39+
if (typeof data === 'number') return +data
40+
let res = 0
41+
for (let i = 0; i < data.length; i++) res += +data[i]
42+
return res
43+
},
44+
'*': (data) => {
45+
let res = 1
46+
for (let i = 0; i < data.length; i++) res *= +data[i]
47+
return res
48+
},
49+
'/': (data) => {
50+
let res = data[0]
51+
for (let i = 1; i < data.length; i++) res /= +data[i]
52+
return res
53+
},
54+
'-': (data) => {
55+
if (typeof data === 'string') return -data
56+
if (typeof data === 'number') return -data
57+
if (data.length === 1) return -data[0]
58+
let res = data[0]
59+
for (let i = 1; i < data.length; i++) res -= +data[i]
60+
return res
61+
},
62+
'%': (data) => {
63+
let res = data[0]
64+
for (let i = 1; i < data.length; i++) res %= +data[i]
65+
return res
66+
},
4867
max: (data) => Math.max(...data),
4968
min: (data) => Math.min(...data),
5069
in: ([item, array]) => (array || []).includes(item),
@@ -145,8 +164,18 @@ const defaultMethods = {
145164
'!=': ([a, b]) => a != b,
146165
'!==': ([a, b]) => a !== b,
147166
xor: ([a, b]) => a ^ b,
148-
or: (arr) => arr.reduce((a, b) => a || b, false),
149-
and: (arr) => arr.reduce((a, b) => a && b),
167+
or: (arr) => {
168+
for (let i = 0; i < arr.length; i++) {
169+
if (arr[i]) return arr[i]
170+
}
171+
return arr[arr.length - 1]
172+
},
173+
and: (arr) => {
174+
for (let i = 0; i < arr.length; i++) {
175+
if (!arr[i]) return arr[i]
176+
}
177+
return arr[arr.length - 1]
178+
},
150179
substr: ([string, from, end]) => {
151180
if (end < 0) {
152181
const result = string.substr(from)
@@ -163,7 +192,7 @@ const defaultMethods = {
163192
method: ([data, key, defaultValue], context, above, engine) => {
164193
const notFound = defaultValue === undefined ? null : defaultValue
165194

166-
const subProps = splitPath(String(key))
195+
const subProps = splitPathMemoized(String(key))
167196
for (let i = 0; i < subProps.length; i++) {
168197
if (data === null || data === undefined) {
169198
return notFound
@@ -205,7 +234,7 @@ const defaultMethods = {
205234
}
206235
return null
207236
}
208-
const subProps = splitPath(String(key))
237+
const subProps = splitPathMemoized(String(key))
209238
for (let i = 0; i < subProps.length; i++) {
210239
if (context === null || context === undefined) {
211240
return notFound
@@ -815,7 +844,7 @@ defaultMethods.get.compile = function (data, buildState) {
815844
if (key && typeof key === 'object') return false
816845

817846
key = key.toString()
818-
const pieces = splitPath(key)
847+
const pieces = splitPathMemoized(key)
819848
if (!chainingSupported) {
820849
return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
821850
(text, i) => {
@@ -864,7 +893,7 @@ defaultMethods.var.compile = function (data, buildState) {
864893
buildState.useContext = true
865894
return false
866895
}
867-
const pieces = splitPath(key)
896+
const pieces = splitPathMemoized(key)
868897
const [top] = pieces
869898
buildState.varTop.add(top)
870899

index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import Compiler from './compiler.js'
77
import Constants from './constants.js'
88
import defaultMethods from './defaultMethods.js'
99
import { asLogicSync, asLogicAsync } from './asLogic.js'
10-
import { splitPath } from './utilities/splitPath.js'
10+
import { splitPath, splitPathMemoized } from './utilities/splitPath.js'
1111

12-
export { splitPath }
12+
export { splitPath, splitPathMemoized }
1313
export { LogicEngine }
1414
export { AsyncLogicEngine }
1515
export { Compiler }
@@ -18,4 +18,4 @@ export { defaultMethods }
1818
export { asLogicSync }
1919
export { asLogicAsync }
2020

21-
export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath }
21+
export default { LogicEngine, AsyncLogicEngine, Compiler, Constants, defaultMethods, asLogicSync, asLogicAsync, splitPath, splitPathMemoized }

logic.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ class LogicEngine {
4949
const [func] = Object.keys(logic)
5050
const data = logic[func]
5151

52-
if (this.isData(logic, func)) return { result: logic, func }
52+
if (this.isData(logic, func)) return logic
5353

5454
if (!this.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
5555

5656
if (typeof this.methods[func] === 'function') {
5757
const input = this.run(data, context, { above })
58-
return { result: this.methods[func](input, context, above, this), func }
58+
return this.methods[func](input, context, above, this)
5959
}
6060

6161
if (typeof this.methods[func] === 'object') {
@@ -65,7 +65,7 @@ class LogicEngine {
6565
? this.run(data, context, { above })
6666
: data
6767

68-
return { result: method(parsedData, context, above, this), func }
68+
return method(parsedData, context, above, this)
6969
}
7070

7171
throw new Error(`Method '${func}' is not set up properly.`)
@@ -144,10 +144,7 @@ class LogicEngine {
144144

145145
if (Array.isArray(logic)) return logic.map((i) => this.run(i, data, { above }))
146146

147-
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) {
148-
const { result } = this._parse(logic, data, above)
149-
return result
150-
}
147+
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above)
151148

152149
return logic
153150
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-logic-engine",
3-
"version": "2.0.0",
3+
"version": "2.0.1",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

utilities/splitPath.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
const parsedPaths = new Map()
2+
3+
/**
4+
* Splits a path string into an array of parts; lightly memoized.
5+
* It will reset the entire cache after 2048 paths, this could be improved
6+
* by implementing an LRU cache or something, but I'm trying to keep
7+
* this library fairly dep free, and the code not too cumbersome.
8+
*
9+
* Memoizing the splitPath function can be seen as cheating, but I think it's likely
10+
* that a lot of the same paths will be used for logic, so it's a good optimization.
11+
*
12+
* @param {string} str
13+
* @returns {string[]}
14+
*/
15+
export function splitPathMemoized (str) {
16+
if (parsedPaths.has(str)) return parsedPaths.get(str)
17+
if (parsedPaths.size > 2048) parsedPaths.clear()
18+
const parts = splitPath(str)
19+
parsedPaths.set(str, parts)
20+
return parts
21+
}
122

223
/**
324
* Splits a path string into an array of parts.
@@ -34,6 +55,5 @@ export function splitPath (str, separator = '.', escape = '\\') {
3455
} else current += char
3556
}
3657
parts.push(current)
37-
3858
return parts
3959
}

0 commit comments

Comments
 (0)