Skip to content

Commit 683f088

Browse files
docs: Perf test
1 parent fa4bde2 commit 683f088

File tree

2 files changed

+130
-37
lines changed

2 files changed

+130
-37
lines changed

README.md

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
# cel-typescript
22

3-
A TypeScript binding for the Common Expression Language (CEL) using [cel-rust](https://github.com/clarkmcc/cel-rust). This project provides a Node.js native module that allows you to use CEL in your TypeScript/JavaScript projects.
3+
A TypeScript binding for the Common Expression Language (CEL) using
4+
[cel-rust](https://github.com/clarkmcc/cel-rust). This project provides a
5+
Node.js native module that allows you to use CEL in your TypeScript/JavaScript
6+
projects.
47

58
## What is CEL?
69

7-
[Common Expression Language (CEL)](https://github.com/google/cel-spec) is an expression language created by Google that implements common semantics for expression evaluation. It's a simple language for expressing boolean conditions, calculations, and variable substitutions. CEL is used in various Google products and open-source projects for policy enforcement, configuration validation, and business rule evaluation.
10+
[Common Expression Language (CEL)](https://github.com/google/cel-spec) is an
11+
expression language created by Google that implements common semantics for
12+
expression evaluation. It's a simple language for expressing boolean conditions,
13+
calculations, and variable substitutions. CEL is used in various Google products
14+
and open-source projects for policy enforcement, configuration validation, and
15+
business rule evaluation.
816

917
## Usage
1018

@@ -19,90 +27,121 @@ There are two ways to use CEL expressions in your code:
1927
For simple use cases where you evaluate an expression once:
2028

2129
```typescript
22-
import { CelProgram } from 'cel-typescript';
30+
import { CelProgram } from "cel-typescript";
2331

2432
// Basic string and numeric operations
25-
await CelProgram.evaluate('size(message) > 5', { message: 'Hello World' }); // true
33+
await CelProgram.evaluate("size(message) > 5", { message: "Hello World" }); // true
2634

2735
// Complex object traversal and comparison
28-
await CelProgram.evaluate(
29-
'user.age >= 18 && user.preferences.notifications',
30-
{
31-
user: {
32-
age: 25,
33-
preferences: { notifications: true }
34-
}
35-
}
36-
); // true
36+
await CelProgram.evaluate("user.age >= 18 && user.preferences.notifications", {
37+
user: {
38+
age: 25,
39+
preferences: { notifications: true },
40+
},
41+
}); // true
3742
```
3843

3944
### Compile Once, Execute Multiple Times
4045

41-
For better performance when evaluating the same expression multiple times with different contexts:
46+
For better performance when evaluating the same expression multiple times with
47+
different contexts:
4248

4349
```typescript
44-
import { CelProgram } from 'cel-typescript';
50+
import { CelProgram } from "cel-typescript";
4551

4652
// Compile the expression once
47-
const program = await CelProgram.compile('items.filter(i, i.price < max_price).size() > 0');
53+
const program = await CelProgram.compile(
54+
"items.filter(i, i.price < max_price).size() > 0",
55+
);
4856

4957
// Execute multiple times with different contexts
5058
await program.execute({
5159
items: [
52-
{ name: 'Book', price: 15 },
53-
{ name: 'Laptop', price: 1000 }
60+
{ name: "Book", price: 15 },
61+
{ name: "Laptop", price: 1000 },
5462
],
55-
max_price: 100
63+
max_price: 100,
5664
}); // true
5765

5866
await program.execute({
5967
items: [
60-
{ name: 'Phone', price: 800 },
61-
{ name: 'Tablet', price: 400 }
68+
{ name: "Phone", price: 800 },
69+
{ name: "Tablet", price: 400 },
6270
],
63-
max_price: 500
71+
max_price: 500,
6472
}); // true
6573

6674
// Date/time operations using timestamp() macro
67-
const timeProgram = await CelProgram.compile('timestamp(event_time) < timestamp("2025-01-01T00:00:00Z")');
75+
const timeProgram = await CelProgram.compile(
76+
'timestamp(event_time) < timestamp("2025-01-01T00:00:00Z")',
77+
);
6878
await timeProgram.execute({
69-
event_time: '2024-12-31T23:59:59Z'
79+
event_time: "2024-12-31T23:59:59Z",
7080
}); // true
7181
```
7282

83+
> [!NOTE]
84+
>
85+
> Performance measurements on an Apple M3 Pro show that compiling a complex CEL
86+
> expression (with map/filter operations) takes about 1.4ms, while execution
87+
> takes about 0.7ms. The one-step `evaluate()` function takes roughly 2ms as it
88+
> performs both steps.
89+
>
90+
> Consider pre-compiling expressions when:
91+
>
92+
> - You evaluate the same expression repeatedly with different data
93+
> - You're building a rules engine or validator that reuses expressions
94+
> - You want to amortize the compilation cost across multiple evaluations
95+
> - Performance is critical in your application
96+
>
97+
> For one-off evaluations or when expressions change frequently, the convenience
98+
> of `evaluate()` likely outweighs the performance benefit of pre-compilation.
99+
73100
## Architecture
74101

75102
This project consists of three main components:
76103

77-
1. **cel-rust**: The underlying Rust implementation of the CEL interpreter, created by clarkmcc. This provides the core CEL evaluation engine.
104+
1. **cel-rust**: The underlying Rust implementation of the CEL interpreter,
105+
created by clarkmcc. This provides the core CEL evaluation engine.
106+
107+
2. **NAPI-RS Bindings**: A thin Rust layer that bridges cel-rust with Node.js
108+
using [NAPI-RS](https://napi.rs/). NAPI-RS is a framework for building
109+
pre-compiled Node.js addons in Rust, providing:
78110

79-
2. **NAPI-RS Bindings**: A thin Rust layer that bridges cel-rust with Node.js using [NAPI-RS](https://napi.rs/). NAPI-RS is a framework for building pre-compiled Node.js addons in Rust, providing:
80111
- Type-safe bindings between Rust and Node.js
81112
- Cross-platform compilation support
82113
- Automatic TypeScript type definitions generation
83114

84-
3. **TypeScript Wrapper**: A TypeScript API that provides a clean interface to the native module, handling type conversions and providing a more idiomatic JavaScript experience.
115+
3. **TypeScript Wrapper**: A TypeScript API that provides a clean interface to
116+
the native module, handling type conversions and providing a more idiomatic
117+
JavaScript experience.
85118

86119
## Native Module Structure
87120

88121
The native module is built using NAPI-RS and provides cross-platform support:
89122

90-
- Platform-specific builds are named `cel-typescript.<platform>-<arch>.node` (e.g., `cel-typescript.darwin-arm64.node` for Apple Silicon Macs)
91-
- NAPI-RS generates a platform-agnostic loader (`index.js`) that automatically detects the current platform and loads the appropriate `.node` file
92-
- The module interface is defined in `src/binding.d.ts` which declares the types for the native module
93-
- At runtime, the TypeScript wrapper (`src/index.ts`) uses the NAPI-RS loader to dynamically load the correct native module
94-
- This structure allows for seamless cross-platform distribution while maintaining platform-specific optimizations
95-
123+
- Platform-specific builds are named `cel-typescript.<platform>-<arch>.node`
124+
(e.g., `cel-typescript.darwin-arm64.node` for Apple Silicon Macs)
125+
- NAPI-RS generates a platform-agnostic loader (`index.js`) that automatically
126+
detects the current platform and loads the appropriate `.node` file
127+
- The module interface is defined in `src/binding.d.ts` which declares the types
128+
for the native module
129+
- At runtime, the TypeScript wrapper (`src/index.ts`) uses the NAPI-RS loader to
130+
dynamically load the correct native module
131+
- This structure allows for seamless cross-platform distribution while
132+
maintaining platform-specific optimizations
96133

97134
## How it Works
98135

99136
When you build this project:
100137

101-
1. The Rust code in `src/lib.rs` is compiled into a native Node.js addon (`.node` file) using NAPI-RS
138+
1. The Rust code in `src/lib.rs` is compiled into a native Node.js addon
139+
(`.node` file) using NAPI-RS
102140
2. The TypeScript code in `src/index.ts` is compiled to JavaScript
103141
3. The native module is loaded by Node.js when you import the package
104142

105143
The build process creates several important files:
144+
106145
- `.node` file: The compiled native module containing the Rust code
107146
- `index.js`: The compiled JavaScript wrapper around the native module
108147
- `index.d.ts`: TypeScript type definitions generated from the Rust code
@@ -121,25 +160,31 @@ npm test
121160

122161
### Prerequisites
123162

124-
1. Clone the [cel-rust](https://github.com/clarkmcc/cel-rust) repository as a sibling to this project:
163+
1. Clone the [cel-rust](https://github.com/clarkmcc/cel-rust) repository as a
164+
sibling to this project:
165+
125166
```bash
126167
parent-directory/
127168
├── cel-rust/
128169
└── cel-typescript/
129170
```
130-
This is required because the project depends on `cel-interpreter` from the local cel-rust project (as specified in Cargo.toml).
171+
172+
This is required because the project depends on `cel-interpreter` from the
173+
local cel-rust project (as specified in Cargo.toml).
131174

132175
2. Ensure you have Rust and Node.js installed.
133176

134177
### Project Structure
135178

136179
The project uses:
180+
137181
- `napi-rs` for Rust/Node.js bindings
138182
- `vitest` for testing
139183
- TypeScript for type safety
140184
- CEL for expression evaluation
141185

142-
To modify the native module, edit `src/lib.rs`. To modify the TypeScript interface, edit `src/index.ts`.
186+
To modify the native module, edit `src/lib.rs`. To modify the TypeScript
187+
interface, edit `src/index.ts`.
143188

144189
## License
145190

__tests__/cel.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,54 @@ describe("CelProgram", () => {
8383
});
8484
});
8585

86+
describe("Performance measurements", () => {
87+
const measureTime = async <T>(fn: () => Promise<T>): Promise<[T, number]> => {
88+
const start = performance.now();
89+
const result = await fn();
90+
const end = performance.now();
91+
// Convert to nanoseconds (1ms = 1,000,000ns)
92+
return [result, (end - start) * 1_000_000];
93+
};
94+
95+
it("should measure compile vs execute time", async () => {
96+
// Complex expression that requires significant parsing
97+
const expr = 'has(items) && items.map(i, i.price).filter(p, p < max_price).size() > 0';
98+
const context = {
99+
items: Array.from({ length: 100 }, (_, i) => ({ id: i, price: i * 10 })),
100+
max_price: 500
101+
};
102+
103+
// Measure compilation time
104+
const [program, compileTime] = await measureTime(() =>
105+
CelProgram.compile(expr)
106+
);
107+
console.log(`Compilation took ${compileTime.toFixed(0)} nanoseconds`);
108+
109+
// Measure execution time
110+
const [result, executeTime] = await measureTime(() =>
111+
program.execute(context)
112+
);
113+
console.log(`Execution took ${executeTime.toFixed(0)} nanoseconds`);
114+
115+
// Measure one-step evaluation time
116+
const [, evaluateTime] = await measureTime(() =>
117+
CelProgram.evaluate(expr, context)
118+
);
119+
console.log(`One-step evaluation took ${evaluateTime.toFixed(0)} nanoseconds`);
120+
121+
// Basic sanity check that the timing data is reasonable
122+
expect(compileTime).toBeGreaterThan(0);
123+
expect(executeTime).toBeGreaterThan(0);
124+
expect(evaluateTime).toBeGreaterThan(0);
125+
126+
// The one-step evaluation should be approximately the sum of compile and execute
127+
const tolerance = 0.5; // Allow 50% variation due to system noise
128+
const expectedEvaluateTime = compileTime + executeTime;
129+
expect(evaluateTime).toBeGreaterThan(expectedEvaluateTime * (1 - tolerance));
130+
expect(evaluateTime).toBeLessThan(expectedEvaluateTime * (1 + tolerance));
131+
});
132+
});
133+
86134
describe("CelProgram.evaluate", () => {
87135
it("should evaluate a simple expression in one step", async () => {
88136
const result = await CelProgram.evaluate("size(message) > 5", {

0 commit comments

Comments
 (0)