diff --git a/packages/node/src/behavior/cluster/ClusterBehaviorCache.ts b/packages/node/src/behavior/cluster/ClusterBehaviorCache.ts new file mode 100644 index 000000000..a25292749 --- /dev/null +++ b/packages/node/src/behavior/cluster/ClusterBehaviorCache.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2022-2025 Project CHIP Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Behavior } from "#behavior/Behavior.js"; +import { ClusterBehavior } from "#index.js"; +import { ClusterType } from "#types"; +import { Schema } from "../supervision/Schema.js"; + +/** + * To save memory we cache behavior implementations specialized for specific clusters. This allows for efficient + * configuration of behaviors with conditional runtime logic. + * + * We use the cluster and schema as cache keys so this relies on similar caching for those items. + */ +const typeCache = new WeakMap< + Behavior.Type, + WeakMap>>> +>(); + +export namespace ClusterBehaviorCache { + export function get(cluster: ClusterType, base: Behavior.Type, schema: Schema) { + const baseCache = typeCache.get(base); + if (baseCache === undefined) { + return; + } + + const clusterCache = baseCache.get(cluster); + if (clusterCache === undefined) { + return; + } + + return clusterCache.get(schema)?.deref(); + } + + export function set(cluster: ClusterType, base: Behavior.Type, schema: Schema, type: ClusterBehavior.Type) { + let baseCache = typeCache.get(base); + if (baseCache === undefined) { + typeCache.set(base, (baseCache = new WeakMap())); + } + + let clusterCache = baseCache.get(cluster); + if (clusterCache === undefined) { + baseCache.set(cluster, (clusterCache = new WeakMap())); + } + + clusterCache.set(schema, new WeakRef(type)); + } +} diff --git a/packages/node/src/behavior/cluster/ClusterBehaviorUtil.ts b/packages/node/src/behavior/cluster/ClusterBehaviorUtil.ts index 1d4cde4e0..c406c139b 100644 --- a/packages/node/src/behavior/cluster/ClusterBehaviorUtil.ts +++ b/packages/node/src/behavior/cluster/ClusterBehaviorUtil.ts @@ -22,6 +22,7 @@ import { DerivedState } from "../state/StateType.js"; import { Val } from "../state/Val.js"; import { Schema } from "../supervision/Schema.js"; import type { ClusterBehavior } from "./ClusterBehavior.js"; +import { ClusterBehaviorCache } from "./ClusterBehaviorCache.js"; const KNOWN_DEFAULTS = Symbol("knownDefaults"); @@ -45,9 +46,7 @@ export function introspectionInstanceOf(type: Behavior.Type) { /** * This is the actual implementation of ClusterBehavior.for(). The result must match {@link ClusterBehavior.Type}. */ -export function createType(cluster: C, base: Behavior.Type, schema?: Schema) { - const namesUsed = new Set(); - +export function createType(cluster: ClusterType, base: Behavior.Type, schema?: Schema) { if (schema === undefined) { if (base.schema) { schema = base.schema; @@ -59,6 +58,11 @@ export function createType(cluster: C, base: Behavi schema = syncFeatures(schema, cluster); + const cached = ClusterBehaviorCache.get(cluster, base, schema); + if (cached) { + return cached; + } + let name; if (base.name.startsWith(cluster.name)) { name = base.name; @@ -69,7 +73,8 @@ export function createType(cluster: C, base: Behavi // Mutation of schema will almost certainly result in logic errors so ensure that can't happen schema.freeze(); - return GeneratedClass({ + const namesUsed = new Set(); + const type = GeneratedClass({ name, base, @@ -99,7 +104,11 @@ export function createType(cluster: C, base: Behavi }, instanceDescriptors: createDefaultCommandDescriptors(cluster, base), - }); + }) as ClusterBehavior.Type; + + ClusterBehaviorCache.set(cluster, base, schema, type); + + return type; } /** diff --git a/packages/node/test/behavior/cluster/ClusterBehaviorCacheTest.ts b/packages/node/test/behavior/cluster/ClusterBehaviorCacheTest.ts new file mode 100644 index 000000000..25900d99b --- /dev/null +++ b/packages/node/test/behavior/cluster/ClusterBehaviorCacheTest.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2022-2025 Project CHIP Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OnOffServer } from "#behaviors/on-off"; + +describe("ClusterBehaviorCache", () => { + it("caches for with", () => { + const Type1 = OnOffServer.with("Lighting"); + const Type2 = OnOffServer.with("Lighting"); + expect(Type1).equals(Type2); + }); + + it("doesn't confuse base with variant", () => { + const Type1 = OnOffServer.with("Lighting"); + expect(Type1).not.equals(OnOffServer); + }); + + it("doesn't confuse multiple variants", () => { + const Type1 = OnOffServer.with("Lighting"); + const Type2 = OnOffServer.with("DeadFrontBehavior"); + expect(Type1).not.equals(Type2); + }); + + it("is not sensitive to feature order", () => { + const Type1 = OnOffServer.with("Lighting", "DeadFrontBehavior"); + const Type2 = OnOffServer.with("DeadFrontBehavior", "Lighting"); + expect(Type1).equals(Type2); + }); +}); diff --git a/packages/types/src/cluster/mutation/ClusterComposer.ts b/packages/types/src/cluster/mutation/ClusterComposer.ts index 49ebb565d..13ca6db94 100644 --- a/packages/types/src/cluster/mutation/ClusterComposer.ts +++ b/packages/types/src/cluster/mutation/ClusterComposer.ts @@ -4,12 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { camelize, isDeepEqual, MatterError, serialize } from "#general"; +import { camelize, MatterError, serialize } from "#general"; +import { FeatureSet } from "@matter/model"; import { BitFlags } from "../../schema/BitmapSchema.js"; import { ClusterType } from "../ClusterType.js"; export class IllegalClusterError extends MatterError {} +/** + * To save memory we cache clusters with specific feature variants. Otherwise code that configures clusters dynamically + * may create multiple redundant copies. + */ +const featureSelectionCache = new WeakMap>>(); + /** * A "cluster composer" manages cluster configuration based on feature selection. */ @@ -26,9 +33,28 @@ export class ClusterComposer { this.validateFeatureSelection(selection); const extensions = this.cluster.extensions; - let cluster: ClusterType; + let cluster: ClusterType | undefined; + + // First check cache + const cacheKey = [...selection].sort().join("␜"); + cluster = featureSelectionCache.get(this.cluster)?.[cacheKey]?.deref(); + + // Next check whether feature set remains unchanged + if (!cluster) { + const currentCacheKey = [...new FeatureSet(this.cluster.supportedFeatures)].sort().join("␜"); + if (currentCacheKey === cacheKey) { + cluster = featureSelectionCache.get(this.cluster)?.[cacheKey]?.deref(); + } + } + + // Done if either optimization above succeeded + if (cluster) { + return cluster as ClusterComposer.Of; + } + // Modify feature selection if (extensions) { + // Feature selection modifies elements const base = this.cluster.base ?? this.cluster; const baseElements = (type: "attributes" | "commands" | "events") => { @@ -57,16 +83,21 @@ export class ClusterComposer { } } } else { + // Feature selection does not modify elements const supportedFeatures = BitFlags(this.cluster.features, ...selection); - if (isDeepEqual(supportedFeatures, this.cluster.supportedFeatures)) { - cluster = this.cluster; - } else { - cluster = ClusterType({ - ...this.cluster, - supportedFeatures, - base: this.cluster.base ?? this.cluster, - }); - } + cluster = ClusterType({ + ...this.cluster, + supportedFeatures, + base: this.cluster.base ?? this.cluster, + }); + } + + // Update cache + const baseVariants = featureSelectionCache.get(this.cluster); + if (baseVariants === undefined) { + featureSelectionCache.set(this.cluster, { [cacheKey]: new WeakRef(cluster) }); + } else { + baseVariants[cacheKey] = new WeakRef(cluster); } return cluster as ClusterComposer.Of;