Skip to content

Commit

Permalink
Caches for ClusterType and ClusterBehavior
Browse files Browse the repository at this point in the history
This should improve CPU and memory usage when dynamically configuring endpoints, though I'm unsure how significant the improvement will be.
  • Loading branch information
lauckhart committed Feb 11, 2025
1 parent 56b2c53 commit c99dc49
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 16 deletions.
51 changes: 51 additions & 0 deletions packages/node/src/behavior/cluster/ClusterBehaviorCache.ts
Original file line number Diff line number Diff line change
@@ -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<ClusterType, WeakMap<Schema, WeakRef<ClusterBehavior.Type<any>>>>
>();

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));
}
}
19 changes: 14 additions & 5 deletions packages/node/src/behavior/cluster/ClusterBehaviorUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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}<C>.
*/
export function createType<const C extends ClusterType>(cluster: C, base: Behavior.Type, schema?: Schema) {
const namesUsed = new Set<string>();

export function createType(cluster: ClusterType, base: Behavior.Type, schema?: Schema) {
if (schema === undefined) {
if (base.schema) {
schema = base.schema;
Expand All @@ -59,6 +58,11 @@ export function createType<const C extends ClusterType>(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;
Expand All @@ -69,7 +73,8 @@ export function createType<const C extends ClusterType>(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<string>();
const type = GeneratedClass({
name,
base,

Expand Down Expand Up @@ -99,7 +104,11 @@ export function createType<const C extends ClusterType>(cluster: C, base: Behavi
},

instanceDescriptors: createDefaultCommandDescriptors(cluster, base),
});
}) as ClusterBehavior.Type;

ClusterBehaviorCache.set(cluster, base, schema, type);

return type;
}

/**
Expand Down
32 changes: 32 additions & 0 deletions packages/node/test/behavior/cluster/ClusterBehaviorCacheTest.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
53 changes: 42 additions & 11 deletions packages/types/src/cluster/mutation/ClusterComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClusterType, Record<string, WeakRef<ClusterType>>>();

/**
* A "cluster composer" manages cluster configuration based on feature selection.
*/
Expand All @@ -26,9 +33,28 @@ export class ClusterComposer<const T extends ClusterType> {
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<T, SelectionT>;
}

// Modify feature selection
if (extensions) {
// Feature selection modifies elements
const base = this.cluster.base ?? this.cluster;

const baseElements = (type: "attributes" | "commands" | "events") => {
Expand Down Expand Up @@ -57,16 +83,21 @@ export class ClusterComposer<const T extends ClusterType> {
}
}
} 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<T, SelectionT>;
Expand Down

0 comments on commit c99dc49

Please sign in to comment.