Skip to content

Commit

Permalink
perf: cache AST for evaluator on reearth/core (#473)
Browse files Browse the repository at this point in the history
* perf: cache AST for evaluator on reearth/core

* fix: a way to clear cache

* test: fix
  • Loading branch information
keiya01 committed Feb 20, 2023
1 parent ebb50de commit da6bb3a
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 25 deletions.
27 changes: 25 additions & 2 deletions src/core/Map/Layer/hooks.ts
@@ -1,7 +1,14 @@
import { useAtom } from "jotai";
import { useCallback, useLayoutEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";

import { computeAtom, DataType, type Atom, evalFeature, ComputedFeature } from "../../mantle";
import {
clearAllExpressionCaches,
computeAtom,
DataType,
type Atom,
evalFeature,
ComputedFeature,
} from "../../mantle";
import type { DataRange, Feature, Layer } from "../../mantle";

export type { Atom as Atoms } from "../../mantle";
Expand Down Expand Up @@ -74,6 +81,22 @@ export default function useHooks(
};
}, [layer, forceUpdateFeatures]);

// Clear expression cache if layer is unmounted
useEffect(
() => () => {
window.requestIdleCallback(() => {
// This is a little heavy task, and not critical for main functionality, so we can run this at idle time.
computedLayer?.originalFeatures.forEach(f => {
clearAllExpressionCaches(
computedLayer.layer.type === "simple" ? computedLayer.layer : undefined,
f,
);
});
});
},
[computedLayer],
);

return {
computedLayer,
handleFeatureRequest: requestFetch,
Expand Down
11 changes: 7 additions & 4 deletions src/core/engines/Cesium/Feature/utils.tsx
Expand Up @@ -20,7 +20,7 @@ import {
} from "react";
import { type CesiumComponentRef, Entity } from "resium";

import { Data, LayerSimple, TimeInterval } from "@reearth/core/mantle";
import { Data, Layer, LayerSimple, TimeInterval } from "@reearth/core/mantle";

import type { ComputedFeature, ComputedLayer, FeatureComponentProps, Geometry } from "../..";

Expand Down Expand Up @@ -141,11 +141,14 @@ const tagKeys = Object.keys(tagObj) as (keyof Tag)[];

const tagKey = "__reearth_tag";

export const extractSimpleLayer = (layer: ComputedLayer | undefined): LayerSimple | void => {
if (layer?.layer.type !== "simple") {
export const extractSimpleLayer = (
layer: ComputedLayer | Layer | undefined,
): LayerSimple | void => {
const l = layer && "layer" in layer ? layer.layer : layer;
if (l?.type !== "simple") {
return;
}
return layer.layer;
return l;
};

export const extractSimpleLayerData = (layer: ComputedLayer | undefined): Data | void => {
Expand Down
2 changes: 2 additions & 0 deletions src/core/mantle/evaluator/index.ts
Expand Up @@ -10,6 +10,8 @@ import {

import { evalSimpleLayerFeature, evalSimpleLayer } from "./simple";

export { clearAllExpressionCaches } from "./simple";

export type EvalContext = {
getFeatures: (d: Data, r?: DataRange) => Promise<Feature[] | undefined>;
getAllFeatures: (d: Data) => Promise<Feature[] | undefined>;
Expand Down
31 changes: 29 additions & 2 deletions src/core/mantle/evaluator/simple/expression/expression.test.ts
@@ -1,6 +1,16 @@
import { expect, test, describe } from "vitest";
import jsep from "jsep";
import { expect, test, describe, beforeEach } from "vitest";

import { replaceDefines, removeBackslashes } from "./expression";
import { Feature } from "../../../types";

import {
replaceDefines,
removeBackslashes,
Expression,
EXPRESSION_CACHES,
clearExpressionCaches,
} from "./expression";
import { createRuntimeAst } from "./runtime";

describe("replaceDefines", () => {
test("should replace defined placeholders with the corresponding values in the expression string", () => {
Expand Down Expand Up @@ -28,3 +38,20 @@ describe("removeBackslashes", () => {
expect(result).toBe("@#%@#%");
});
});

describe("expression caches", () => {
beforeEach(() => {
EXPRESSION_CACHES.clear();
});

test("should remove caches", () => {
const key = "czm_HEIGHT > 2 ? color('red') : color('blue')";
const feature = { properties: { HEIGHT: 1 } } as Feature;
const expression = new Expression("${HEIGHT} > 2 ? color('red') : color('blue')", feature);
expect(EXPRESSION_CACHES.get(key)).toEqual(createRuntimeAst(expression, jsep(key)));

clearExpressionCaches(key, feature, undefined);

expect(EXPRESSION_CACHES.get(key)).toBeUndefined();
});
});
46 changes: 32 additions & 14 deletions src/core/mantle/evaluator/simple/expression/expression.ts
Expand Up @@ -13,6 +13,8 @@ export type JPLiteral = {
literalValue: any;
};

export const EXPRESSION_CACHES = new Map<string, Node | Error>();

export class Expression {
#expression: string;
#runtimeAst: Node | Error;
Expand All @@ -28,24 +30,30 @@ export class Expression {
this.#feature?.properties,
);

if (literalJP.length !== 0) {
for (const elem of literalJP) {
jsep.addLiteral(elem.literalName, elem.literalValue);
const cachedAST = EXPRESSION_CACHES.get(expression);
if (cachedAST) {
this.#runtimeAst = cachedAST;
} else {
if (literalJP.length !== 0) {
for (const elem of literalJP) {
jsep.addLiteral(elem.literalName, elem.literalValue);
}
}
}

// customize jsep operators
jsep.addBinaryOp("=~", 0);
jsep.addBinaryOp("!~", 0);
// customize jsep operators
jsep.addBinaryOp("=~", 0);
jsep.addBinaryOp("!~", 0);

let ast;
try {
ast = jsep(expression);
} catch (e) {
throw new Error(`failed to generate ast: ${e}`);
}
let ast;
try {
ast = jsep(expression);
} catch (e) {
throw new Error(`failed to generate ast: ${e}`);
}

this.#runtimeAst = createRuntimeAst(this, ast);
this.#runtimeAst = createRuntimeAst(this, ast);
EXPRESSION_CACHES.set(expression, this.#runtimeAst);
}
}

evaluate() {
Expand All @@ -71,3 +79,13 @@ export function replaceDefines(expression: string, defines: any): string {
export function removeBackslashes(expression: string): string {
return expression.replace(backslashRegex, backslashReplacement);
}

export function clearExpressionCaches(
expression: string,
feature: Feature | undefined,
defines: any | undefined,
) {
expression = replaceDefines(expression, defines);
[expression] = replaceVariables(removeBackslashes(expression), feature?.properties);
EXPRESSION_CACHES.delete(expression);
}
2 changes: 1 addition & 1 deletion src/core/mantle/evaluator/simple/expression/index.ts
@@ -1 +1 @@
export { Expression } from "./expression";
export { Expression, clearExpressionCaches } from "./expression";
27 changes: 26 additions & 1 deletion src/core/mantle/evaluator/simple/index.ts
Expand Up @@ -14,7 +14,7 @@ import {
import { defined } from "../../utils";

import { ConditionalExpression } from "./conditionalExpression";
import { Expression } from "./expression";
import { clearExpressionCaches, Expression } from "./expression";
import { evalTimeInterval } from "./interval";

export async function evalSimpleLayer(
Expand Down Expand Up @@ -72,6 +72,31 @@ export function evalLayerAppearances(
);
}

export function clearAllExpressionCaches(
layer: LayerSimple | undefined,
feature: Feature | undefined,
) {
const appearances: Partial<LayerAppearanceTypes> = pick(layer, appearanceKeys);

Object.entries(appearances).forEach(([, v]) => {
Object.entries(v).forEach(([, expressionContainer]) => {
if (hasExpression(expressionContainer)) {
const styleExpression = expressionContainer.expression;
if (typeof styleExpression === "object" && styleExpression.conditions) {
styleExpression.conditions.forEach(([expression1, expression2]) => {
clearExpressionCaches(expression1, feature, layer?.defines);
clearExpressionCaches(expression2, feature, layer?.defines);
});
} else if (typeof styleExpression === "boolean" || typeof styleExpression === "number") {
clearExpressionCaches(String(styleExpression), feature, layer?.defines);
} else if (typeof styleExpression === "string") {
clearExpressionCaches(styleExpression, feature, layer?.defines);
}
}
});
});
}

function hasExpression(e: any): e is ExpressionContainer {
return typeof e === "object" && e && "expression" in e;
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/mantle/index.ts
@@ -1,5 +1,5 @@
export * from "./types";
export * from "./compat";
export { evalFeature } from "./evaluator";
export { evalFeature, clearAllExpressionCaches } from "./evaluator";
export { computeAtom } from "./atoms";
export type { Atom } from "./atoms";
5 changes: 5 additions & 0 deletions src/test/setup.ts
Expand Up @@ -33,4 +33,9 @@ Object.defineProperty(window, "matchMedia", {
})),
});

Object.defineProperty(window, "requestIdleCallback", {
writable: true,
value: vi.fn(),
});

afterEach(cleanup);

0 comments on commit da6bb3a

Please sign in to comment.