Skip to content

Commit 3dce50e

Browse files
committed
cache compiled global scripts to cut per-binding overhead
1 parent 252b869 commit 3dce50e

File tree

3 files changed

+86
-26
lines changed

3 files changed

+86
-26
lines changed

apps/kitchen-sink/src/ensemble/screens/forms.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,3 +588,6 @@ Global: |
588588
}]
589589
}
590590
}
591+
const sayHello = () => {
592+
window.alert("hello world!");
593+
};

apps/kitchen-sink/src/ensemble/screens/home.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ View:
5656
onTap:
5757
executeCode: |
5858
// Calls a function defined in test.js
59+
debugger;
5960
sayHello();
6061
6162
- Button:
@@ -402,7 +403,7 @@ View:
402403
onResponse:
403404
executeCode: |
404405
ensemble.storage.set('email', response.body.results[0].email)
405-
ensemble.storage.set('emails', [...ensemble.storage.get("emails"),response.body.results[0].email])
406+
ensemble.storage.set('emails', [...(ensemble.storage.get("emails") || []),response.body.results[0].email])
406407
console.log('getData', response.body.results[0].email, ensemble.storage.get('emails'));
407408
- Column:
408409
item-template:
@@ -463,7 +464,7 @@ View:
463464
data: ${ensemble.storage.get('products')}
464465
onSearch:
465466
executeCode: |
466-
ensemble.invokeAPI('getProducts', { search: search }).then((res) => {
467+
ensemble.invokeAPI('getProducts', { search }).then((res) => {
467468
const users = res?.body?.users || [];
468469
console.log(users , "users");
469470
const newUsers = users.map((i) => ({ ...i, label: i.firstName + ' ' + i.lastName, name: i.firstName + ' ' + i.lastName, value: i.email }));
@@ -473,7 +474,7 @@ View:
473474
console.log("onSearch values: ", search);
474475
onChange:
475476
executeCode: |
476-
console.log("onChange values: ", search);
477+
console.log("onChange values: ", value);
477478
478479
Global:
479480
scriptName: test.js

packages/framework/src/evaluate/evaluate.ts

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isEmpty, merge, toString } from "lodash-es";
2+
import { parse as acornParse } from "acorn";
23
import type { ScreenContextDefinition } from "../state/screen";
34
import type { InvokableMethods, WidgetState } from "../state/widget";
45
import {
@@ -9,6 +10,73 @@ import {
910
replace,
1011
} from "../shared";
1112

13+
/**
14+
* Cache of compiled global / imported scripts keyed by the full script string.
15+
* Each entry stores the symbol names and their corresponding values so that we
16+
* can inject them as parameters when evaluating bindings, removing the need to
17+
* re-parse the same script for every binding.
18+
*/
19+
interface CachedScriptEntry {
20+
symbols: string[];
21+
// compiled function that, given a context, returns an object of exports
22+
fn: (ctx: { [key: string]: unknown }) => { [key: string]: unknown };
23+
}
24+
25+
const globalScriptCache = new Map<string, CachedScriptEntry>();
26+
27+
const parseScriptSymbols = (script: string): string[] => {
28+
const symbols = new Set<string>();
29+
try {
30+
const ast: any = acornParse(script, {
31+
ecmaVersion: 2020,
32+
sourceType: "script",
33+
});
34+
35+
ast.body?.forEach((node: any) => {
36+
if (node.type === "FunctionDeclaration" && node.id) {
37+
symbols.add(node.id.name);
38+
}
39+
if (node.type === "VariableDeclaration") {
40+
node.declarations.forEach((decl: any) => {
41+
if (decl.id?.type === "Identifier") {
42+
symbols.add(decl.id.name);
43+
}
44+
});
45+
}
46+
});
47+
} catch (e) {
48+
debug(e);
49+
}
50+
return Array.from(symbols);
51+
};
52+
53+
const getCachedGlobals = (
54+
script: string,
55+
ctx: { [key: string]: unknown },
56+
): { symbols: string[]; values: unknown[] } => {
57+
if (isEmpty(script.trim())) return { symbols: [], values: [] };
58+
59+
let entry = globalScriptCache.get(script);
60+
if (!entry) {
61+
const symbols = parseScriptSymbols(script);
62+
63+
// build a function that executes the script within the provided context using `with`
64+
// and returns an object containing the exported symbols
65+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
66+
const compiled = new Function(
67+
"ctx",
68+
`with (ctx) {\n${script}\nreturn { ${symbols.join(", ")} };\n}`,
69+
) as CachedScriptEntry["fn"];
70+
71+
entry = { symbols, fn: compiled };
72+
globalScriptCache.set(script, entry);
73+
}
74+
75+
const exportsObj = entry.fn(ctx);
76+
const values = entry.symbols.map((name) => exportsObj[name]);
77+
return { symbols: entry.symbols, values };
78+
};
79+
1280
export const widgetStatesToInvokables = (widgets: {
1381
[key: string]: WidgetState | undefined;
1482
}): [string, InvokableMethods | undefined][] => {
@@ -38,17 +106,23 @@ export const buildEvaluateFn = (
38106
// Need to filter out invalid JS identifiers
39107
].filter(([key, _]) => !key.includes(".")),
40108
);
41-
const globalBlock = screen.model?.global;
42-
const importedScriptBlock = screen.model?.importedScripts;
109+
const globalBlock = screen.model?.global ?? "";
110+
const importedScriptBlock = screen.model?.importedScripts ?? "";
111+
const combinedScript = `${importedScriptBlock}\n${globalBlock}`;
112+
113+
const { symbols: globalSymbols, values: globalValues } = getCachedGlobals(
114+
combinedScript,
115+
merge({}, context, invokableObj),
116+
);
43117

44118
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
45119
const jsFunc = new Function(
46120
...Object.keys(invokableObj),
47-
addScriptBlock(formatJs(js), globalBlock, importedScriptBlock),
121+
...globalSymbols,
122+
formatJs(js),
48123
);
49124

50-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
51-
return () => jsFunc(...Object.values(invokableObj));
125+
return () => jsFunc(...Object.values(invokableObj), ...globalValues);
52126
};
53127

54128
const formatJs = (js?: string): string => {
@@ -96,24 +170,6 @@ const formatJs = (js?: string): string => {
96170
return `return ${sanitizedJs}`;
97171
};
98172

99-
const addScriptBlock = (
100-
js: string,
101-
globalBlock?: string,
102-
importedScriptBlock?: string,
103-
): string => {
104-
let jsString = ``;
105-
106-
if (importedScriptBlock) {
107-
jsString += `${importedScriptBlock}\n\n`;
108-
}
109-
110-
if (globalBlock) {
111-
jsString += `${globalBlock}\n\n`;
112-
}
113-
114-
return (jsString += `${js}`);
115-
};
116-
117173
/**
118174
* @deprecated Consider using useEvaluate or createBinding which will
119175
* optimize creating the evaluation context

0 commit comments

Comments
 (0)