Skip to content

Commit 9ea3eb1

Browse files
committed
Add some optimizations to the async optimizer to execute parts of the logic synchronously if possible.
1 parent 3d996cd commit 9ea3eb1

File tree

7 files changed

+99
-17
lines changed

7 files changed

+99
-17
lines changed

asyncLogic.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class AsyncLogicEngine {
240240
}, top !== true && isSync(constructedFunction))
241241
// we can avoid the async pool if the constructed function is synchronous since the data
242242
// can't be updated :)
243-
if (top === true && constructedFunction && !constructedFunction[Sync] && typeof constructedFunction === 'function') {
243+
if (top === true && constructedFunction && !isSync(constructedFunction) && typeof constructedFunction === 'function') {
244244
// we use this async pool so that we can execute these in parallel without having
245245
// concerns about the data.
246246
return asyncPool({

async_optimizer.js

+27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// This is the synchronous version of the optimizer; which the Async one should be based on.
22
import { isDeterministic } from './compiler.js'
33
import { map } from './async_iterators.js'
4+
import { isSync, Sync } from './constants.js'
5+
import declareSync from './utilities/declareSync.js'
46

57
/**
68
* Turns an expression like { '+': [1, 2] } into a function that can be called with data.
@@ -14,6 +16,7 @@ function getMethod (logic, engine, methodName, above) {
1416
const method = engine.methods[methodName]
1517
const called = method.asyncMethod ? method.asyncMethod : method.method ? method.method : method
1618

19+
// Todo: Like "deterministic", we should add "sync" to the method object to determine if the structure can be run synchronously.
1720
if (method.traverse === false) {
1821
const args = logic[methodName]
1922
return (data, abv) => called(args, data, abv || above, engine)
@@ -23,12 +26,27 @@ function getMethod (logic, engine, methodName, above) {
2326

2427
if (Array.isArray(args)) {
2528
const optimizedArgs = args.map(l => optimize(l, engine, above))
29+
30+
if (isSync(optimizedArgs) && (method.method || method[Sync])) {
31+
const called = method.method ? method.method : method
32+
return declareSync((data, abv) => {
33+
const evaluatedArgs = optimizedArgs.map(l => typeof l === 'function' ? l(data, abv) : l)
34+
return called(evaluatedArgs, data, abv || above, engine)
35+
}, true)
36+
}
37+
2638
return async (data, abv) => {
2739
const evaluatedArgs = await map(optimizedArgs, l => typeof l === 'function' ? l(data, abv) : l)
2840
return called(evaluatedArgs, data, abv || above, engine)
2941
}
3042
} else {
3143
const optimizedArgs = optimize(args, engine, above)
44+
45+
if (isSync(optimizedArgs) && (method.method || method[Sync])) {
46+
const called = method.method ? method.method : method
47+
return declareSync((data, abv) => called(typeof optimizedArgs === 'function' ? optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine), true)
48+
}
49+
3250
return async (data, abv) => {
3351
return called(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine)
3452
}
@@ -45,6 +63,7 @@ function getMethod (logic, engine, methodName, above) {
4563
export function optimize (logic, engine, above = []) {
4664
if (Array.isArray(logic)) {
4765
const arr = logic.map(l => optimize(l, engine, above))
66+
if (isSync(arr)) return declareSync((data, abv) => arr.map(l => typeof l === 'function' ? l(data, abv) : l), true)
4867
return async (data, abv) => map(arr, l => typeof l === 'function' ? l(data, abv) : l)
4968
};
5069

@@ -63,6 +82,14 @@ export function optimize (logic, engine, above = []) {
6382
const result = getMethod(logic, engine, methodName, above)
6483
if (deterministic) {
6584
let computed
85+
86+
if (isSync(result)) {
87+
return declareSync(() => {
88+
if (!computed) computed = result()
89+
return computed
90+
}, true)
91+
}
92+
6693
// For async, it's a little less straightforward since it could be a promise,
6794
// so we'll make it a closure.
6895
return async () => {

bench/package-lock.json

+36-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bench/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"author": "Jesse Daniel Mitchell",
1010
"license": "ISC",
1111
"dependencies": {
12+
"@bestow/jsonlogic-rs": "^0.4.0",
1213
"json-logic-js": "^2.0.1",
1314
"json-rules-engine": "^6.0.1"
1415
},

bench/test.js

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs'
33
import { isDeepStrictEqual } from 'util'
44
// import traverseCopy from '../utilities/traverseCopy.js'
55
import jl from 'json-logic-js'
6+
import rust from '@bestow/jsonlogic-rs'
67

78
const x = new LogicEngine(undefined, { compatible: true })
89
const y = new AsyncLogicEngine(undefined, { compatible: true })
@@ -73,6 +74,14 @@ for (let j = 0; j < tests.length; j++) {
7374
}
7475
console.timeEnd('json-logic-js')
7576

77+
console.time('json-logic-rs')
78+
for (let j = 0; j < tests.length; j++) {
79+
for (let i = 0; i < 1e5; i++) {
80+
rust.apply(tests[j][0], tests[j][1])
81+
}
82+
}
83+
console.timeEnd('json-logic-rs')
84+
7685
x.disableInterpretedOptimization = true
7786
console.time('le interpreted')
7887
for (let j = 0; j < other.length; j++) {
@@ -104,6 +113,15 @@ async function run () {
104113
return y.build(i[0])
105114
})
106115
)
116+
117+
console.time('le async interpreted')
118+
for (let j = 0; j < tests.length; j++) {
119+
for (let i = 0; i < 1e5; i++) {
120+
await y.run(tests[j][0], tests[j][1])
121+
}
122+
}
123+
console.timeEnd('le async interpreted')
124+
107125
console.time('le async built')
108126
for (let j = 0; j < tests.length; j++) {
109127
for (let i = 0; i < 1e5; i++) {

constants.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,21 @@
44
export const Sync = Symbol.for('json_logic_sync')
55
export const Override = Symbol.for('json_logic_override')
66
export const EfficientTop = Symbol.for('json_logic_efficientTop')
7-
export const isSync = (x) => Boolean(typeof x !== 'function' || x[Sync])
7+
8+
/**
9+
* Checks if an item is synchronous.
10+
* This allows us to optimize the logic a bit
11+
* further so that we don't need to await everything.
12+
*
13+
* @param {*} item
14+
* @returns {Boolean}
15+
*/
16+
export function isSync (item) {
17+
if (typeof item === 'function') return item[Sync] === true
18+
if (Array.isArray(item)) return item.every(isSync)
19+
return true
20+
}
21+
822
export default {
923
Sync,
1024
Override,

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.1.0",
3+
"version": "2.1.1",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

0 commit comments

Comments
 (0)