Skip to content

Commit 2e182fa

Browse files
authored
perf: optimize tokenizer and parser performance (#21)
* perf: optimize tokenizer and parser performance * fix: ci * perf: benchmark & readme
1 parent e80859c commit 2e182fa

File tree

11 files changed

+654
-493
lines changed

11 files changed

+654
-493
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ jobs:
3434
- name: Run tests
3535
run: pnpm test
3636

37-
- name: Run benchmarks
38-
run: pnpm run benchmark
39-
4037
- name: Build package
4138
run: pnpm run build
39+
40+
- name: Run benchmarks
41+
run: pnpm run benchmark

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Now we have solved this problem for you. We have designed a simple and easy-to-u
99
- 🔒 **Secure by default** - No access to global objects or prototype chain, does not use `eval` or `new Function`
1010
- 🚀 **High performance** - Supports pre-compilation of expressions for improved performance with repeated evaluations
1111
- 🛠️ **Extensible** - Register custom functions to easily extend functionality
12-
- 🪩 **Lightweight** - Zero dependencies, small footprint
12+
- 🪩 **Lightweight** - Zero dependencies, small footprint, only 7.8KB
1313

1414
## Installation
1515

@@ -174,6 +174,17 @@ const result = evaluate('@formatCurrency(price * quantity)', {
174174
```
175175
**Default Global Functions:** `['abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'max', 'min']`
176176

177+
## Benchmarks
178+
179+
Performance comparison of different evaluation methods: (baseline: new Function)
180+
181+
| Expression Type | new Function vs evaluate after compile | new Function vs evaluate without compile | new Function vs [expr-eval](https://www.npmjs.com/package/expr-eval?activeTab=readme) Parser |
182+
|-----------------------|----------------------------------------|------------------------------------------|----------------------------------|
183+
| Simple Expressions | 1.59x faster | 6.36x faster | 23.94x faster |
184+
| Medium Expressions | 2.16x faster | 9.81x faster | 37.81x faster |
185+
| Complex Expressions | 1.59x faster | 4.89x faster | 32.74x faster |
186+
187+
177188
## Advanced Usage
178189

179190
### Timeout Handling

bench/expr.bench.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { Parser } from "expr-eval";
12
import { bench, describe } from "vitest";
2-
import { compile, evaluate, register } from "../src";
3+
import { compile, evaluate, register } from "../dist/index.esm.js";
34

45
const context = {
56
user: {
@@ -27,7 +28,6 @@ const context = {
2728
},
2829
};
2930

30-
// 测试表达式
3131
const simpleExpression = "user.age + 5";
3232
const mediumExpression = 'user.scores[2] > 80 ? "Good" : "Needs improvement"';
3333
const complexExpression =
@@ -38,12 +38,15 @@ const complexExpression2 =
3838

3939
const simpleExpressionCompiler = compile(simpleExpression);
4040
const mediumExpressionCompiler = compile(mediumExpression);
41-
const complexExpression2Compiler = compile(complexExpression);
41+
const complexExpressionCompiler = compile(complexExpression);
4242

4343
register("calculateTotal", context.calculateTotal);
4444
register("applyDiscount", context.applyDiscount);
4545

46-
// 创建 Function 对象
46+
const parser = new Parser();
47+
parser.functions.calculateTotal = context.calculateTotal;
48+
parser.functions.applyDiscount = context.applyDiscount;
49+
4750
const newFunctionSimple = new Function(
4851
"context",
4952
`with(context) { return ${simpleExpression}; }`,
@@ -58,43 +61,67 @@ const newFunctionComplex = new Function(
5861
);
5962

6063
describe("Simple Expression Benchmarks", () => {
61-
bench("evaluate after compile (baseline)", () => {
64+
bench("evaluate after compile (baseline) only interpreter", () => {
6265
simpleExpressionCompiler(context);
6366
});
6467

6568
bench("new Function (vs evaluate)", () => {
6669
newFunctionSimple(context);
6770
});
6871

69-
bench("evaluate without compile (vs evaluate)", () => {
70-
evaluate(simpleExpression, context);
72+
bench(
73+
"evaluate without compile (vs evaluate) tokenize + parse + interpreter",
74+
() => {
75+
evaluate(simpleExpression, context);
76+
},
77+
);
78+
79+
bench("expr-eval Parser (vs evaluate)", () => {
80+
// @ts-ignore
81+
Parser.evaluate(simpleExpression, context);
7182
});
7283
});
7384

7485
describe("Medium Expression Benchmarks", () => {
75-
bench("evaluate after compile (baseline)", () => {
86+
bench("evaluate after compile (baseline) only interpreter", () => {
7687
mediumExpressionCompiler(context);
7788
});
7889

7990
bench("new Function (vs evaluate)", () => {
8091
newFunctionMedium(context);
8192
});
8293

83-
bench("evaluate without compile (vs evaluate)", () => {
84-
evaluate(mediumExpression, context);
94+
bench(
95+
"evaluate without compile (vs evaluate) tokenize + parse + interpreter",
96+
() => {
97+
evaluate(mediumExpression, context);
98+
},
99+
);
100+
101+
bench("expr-eval Parser (vs evaluate)", () => {
102+
// @ts-ignore
103+
Parser.evaluate(mediumExpression, context);
85104
});
86105
});
87106

88107
describe("Complex Expression Benchmarks", () => {
89-
bench("evaluate after compile (baseline)", () => {
90-
complexExpression2Compiler(context);
108+
bench("evaluate after compile (baseline) only interpreter", () => {
109+
complexExpressionCompiler(context);
91110
});
92111

93112
bench("new Function (vs evaluate)", () => {
94113
newFunctionComplex(context);
95114
});
96115

97-
bench("evaluate without compile (vs evaluate)", () => {
98-
evaluate(complexExpression2, context);
116+
bench(
117+
"evaluate without compile (vs evaluate) tokenize + parse + interpreter",
118+
() => {
119+
evaluate(complexExpression2, context);
120+
},
121+
);
122+
123+
bench("expr-eval Parser (vs evaluate)", () => {
124+
// @ts-ignore
125+
parser.evaluate(complexExpression2, context);
99126
});
100127
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@rollup/plugin-terser": "^0.4.4",
2020
"@rollup/plugin-typescript": "^12.1.2",
2121
"@vitest/coverage-v8": "^3.0.8",
22+
"expr-eval": "^2.0.2",
2223
"rollup": "^4.34.6",
2324
"tslib": "^2.8.1",
2425
"vitest": "^3.0.8"

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/interpreter.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import type {
2-
BinaryExpression,
3-
CallExpression,
4-
ConditionalExpression,
5-
Expression,
6-
Identifier,
7-
Literal,
8-
MemberExpression,
9-
Program,
10-
UnaryExpression,
1+
import {
2+
type BinaryExpression,
3+
type CallExpression,
4+
type ConditionalExpression,
5+
type Expression,
6+
type Identifier,
7+
type Literal,
8+
type MemberExpression,
9+
NodeType,
10+
type Program,
11+
type UnaryExpression,
1112
} from "./parser";
1213
import { ExpressionError } from "./utils";
1314

@@ -225,19 +226,19 @@ export const evaluateAst = (
225226
const evaluateNode = (node: Expression): unknown => {
226227
try {
227228
switch (node.type) {
228-
case "Literal":
229+
case NodeType.Literal:
229230
return evaluateLiteral(node);
230-
case "Identifier":
231+
case NodeType.Identifier:
231232
return evaluateIdentifier(node);
232-
case "MemberExpression":
233+
case NodeType.MemberExpression:
233234
return evaluateMemberExpression(node);
234-
case "CallExpression":
235+
case NodeType.CallExpression:
235236
return evaluateCallExpression(node);
236-
case "BinaryExpression":
237+
case NodeType.BinaryExpression:
237238
return evaluateBinaryExpression(node);
238-
case "UnaryExpression":
239+
case NodeType.UnaryExpression:
239240
return evaluateUnaryExpression(node);
240-
case "ConditionalExpression":
241+
case NodeType.ConditionalExpression:
241242
return evaluateConditionalExpression(node);
242243
default:
243244
throw new ExpressionError(

0 commit comments

Comments
 (0)