Skip to content

Commit 5236e27

Browse files
Optimize out no-op writes in attribute-table. (#31162)
* Optimize out no-op writes in attribute-table. This way we don't mark things dirty if they have not changed. Does best-effort optimizing out of string values that are unchanged, only for strings that we know can fit into our fixed-size buffer. Fixes #29136 * Address review comments. * Fix things so going from null to empty string is considered a change.
1 parent ee6c9e9 commit 5236e27

File tree

12 files changed

+17623
-56
lines changed

12 files changed

+17623
-56
lines changed

.github/workflows/tests.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ jobs:
249249
--chip-tool ./out/linux-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT}/chip-tool \
250250
run \
251251
--iterations 1 \
252-
--expected-failures 2 \
252+
--expected-failures 3 \
253253
--keep-going \
254254
--test-timeout-seconds 120 \
255255
--all-clusters-app ./out/linux-x64-all-clusters-${BUILD_VARIANT}/chip-all-clusters-app \
@@ -400,7 +400,7 @@ jobs:
400400
--chip-tool ./out/darwin-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT}/chip-tool \
401401
run \
402402
--iterations 1 \
403-
--expected-failures 2 \
403+
--expected-failures 3 \
404404
--keep-going \
405405
--test-timeout-seconds 120 \
406406
--all-clusters-app ./out/darwin-x64-all-clusters-${BUILD_VARIANT}/chip-all-clusters-app \

scripts/tests/chiptest/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def _GetPurposefulFailureTests() -> Set[str]:
277277
"""Tests that fail in YAML on purpose."""
278278
return {
279279
"TestPurposefulFailureEqualities.yaml",
280+
"TestPurposefulFailureExtraReportingOnToggle.yaml",
280281
"TestPurposefulFailureNotNullConstraint.yaml",
281282
}
282283

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) 2024 Project CHIP Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name:
16+
Test that "passes" if turning on On/Off cluster reports Level Control things
17+
18+
config:
19+
nodeId: 0x12344321
20+
cluster: "On/Off"
21+
endpoint: 1
22+
# We expect our test to time out, so set a timeout that's not too long, but
23+
# long enough that if the server does report the attribute change we will
24+
# almost certianly see it.
25+
timeout: 5
26+
27+
tests:
28+
- label: "Wait for the commissioned device to be retrieved"
29+
cluster: "DelayCommands"
30+
command: "WaitForCommissionee"
31+
arguments:
32+
values:
33+
- name: "nodeId"
34+
value: nodeId
35+
36+
- label: "Turn off the light"
37+
command: "Off"
38+
39+
- label: "Subscribe LevelControl RemainingTime Attribute"
40+
command: "subscribeAttribute"
41+
cluster: "LevelControl"
42+
attribute: "RemainingTime"
43+
minInterval: 0
44+
maxInterval: 5
45+
response:
46+
value: 0
47+
48+
- label: "Turn on the light to see attribute change, if any"
49+
command: "On"
50+
51+
- label: "Check for attribute report"
52+
command: "waitForReport"
53+
cluster: "LevelControl"
54+
attribute: "RemainingTime"
55+
# This test should fail, since there should be no reporting for an
56+
# attribute that did not actually change.
57+
response:
58+
value: 0

src/app/util/af-types.h

+12
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,15 @@ typedef chip::Protocols::InteractionModel::Status (*EmberAfClusterPreAttributeCh
297297
#define MAX_INT16U_VALUE (0xFFFF)
298298

299299
/** @} END addtogroup */
300+
301+
namespace chip {
302+
namespace app {
303+
304+
enum class MarkAttributeDirty
305+
{
306+
kIfChanged,
307+
kNo,
308+
};
309+
310+
} // namespace app
311+
} // namespace chip

src/app/util/attribute-table-detail.h

-34
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,3 @@
3030
chip::Protocols::InteractionModel::Status emAfWriteAttributeExternal(chip::EndpointId endpoint, chip::ClusterId cluster,
3131
chip::AttributeId attributeID, uint8_t * dataPtr,
3232
EmberAfAttributeType dataType);
33-
34-
/**
35-
* @brief write an attribute, performing all the checks.
36-
*
37-
* This function will attempt to write the attribute value from
38-
* the provided pointer. This function will only check that the
39-
* attribute exists. If it does it will write the value into
40-
* the attribute table for the given attribute.
41-
*
42-
* This function will not check to see if the attribute is
43-
* writable since the read only / writable characteristic
44-
* of an attribute only pertains to external devices writing
45-
* over the air. Because this function is being called locally
46-
* it assumes that the device knows what it is doing and has permission
47-
* to perform the given operation.
48-
*
49-
* if true is passed in for overrideReadOnlyAndDataType then the data type is
50-
* not checked and the read-only flag is ignored. This mode is meant for
51-
* testing or setting the initial value of the attribute on the device.
52-
*
53-
* this returns:
54-
* - Status::UnsupportedEndpoint: if endpoint isn't supported by the device.
55-
* - Status::UnsupportedCluster: if cluster isn't supported on the endpoint.
56-
* - Status::UnsupportedAttribute: if attribute isn't supported in the cluster.
57-
* - Status::InvalidDataType: if the data type passed in doesnt match the type
58-
* stored in the attribute table
59-
* - Status::UnsupportedWrite: if the attribute isnt writable
60-
* - Status::ConstraintError: if the value is set out of the allowable range for
61-
* the attribute
62-
* - Status::Success: if the attribute was found and successfully written
63-
*/
64-
chip::Protocols::InteractionModel::Status emAfWriteAttribute(chip::EndpointId endpoint, chip::ClusterId cluster,
65-
chip::AttributeId attributeID, uint8_t * data,
66-
EmberAfAttributeType dataType, bool overrideReadOnlyAndDataType);

src/app/util/attribute-table.cpp

+137-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <app/util/attribute-storage-detail.h>
2222
#include <app/util/attribute-storage.h>
2323
#include <app/util/config.h>
24+
#include <app/util/ember-strings.h>
2425
#include <app/util/generic-callbacks.h>
2526
#include <app/util/odd-sized-integers.h>
2627
#include <lib/core/CHIPConfig.h>
@@ -37,9 +38,9 @@
3738
using chip::Protocols::InteractionModel::Status;
3839

3940
using namespace chip;
41+
using namespace chip::app;
4042

4143
namespace {
42-
4344
// Zigbee spec says types between signed 8 bit and signed 64 bit
4445
bool emberAfIsTypeSigned(EmberAfAttributeType dataType)
4546
{
@@ -134,18 +135,58 @@ int8_t emberAfCompareValues(const uint8_t * val1, const uint8_t * val2, uint16_t
134135
return 0;
135136
}
136137

137-
} // namespace
138+
/**
139+
* @brief write an attribute, performing all the checks.
140+
*
141+
* This function will attempt to write the attribute value from
142+
* the provided pointer. This function will only check that the
143+
* attribute exists. If it does it will write the value into
144+
* the attribute table for the given attribute.
145+
*
146+
* This function will not check to see if the attribute is
147+
* writable since the read only / writable characteristic
148+
* of an attribute only pertains to external devices writing
149+
* over the air. Because this function is being called locally
150+
* it assumes that the device knows what it is doing and has permission
151+
* to perform the given operation.
152+
*
153+
* if true is passed in for overrideReadOnlyAndDataType then the data type is
154+
* not checked and the read-only flag is ignored. This mode is meant for
155+
* testing or setting the initial value of the attribute on the device.
156+
*
157+
* this returns:
158+
* - Status::UnsupportedEndpoint: if endpoint isn't supported by the device.
159+
* - Status::UnsupportedCluster: if cluster isn't supported on the endpoint.
160+
* - Status::UnsupportedAttribute: if attribute isn't supported in the cluster.
161+
* - Status::InvalidDataType: if the data type passed in doesnt match the type
162+
* stored in the attribute table
163+
* - Status::UnsupportedWrite: if the attribute isnt writable
164+
* - Status::ConstraintError: if the value is set out of the allowable range for
165+
* the attribute
166+
* - Status::Success: if the attribute was found and successfully written
167+
*/
168+
Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * data,
169+
EmberAfAttributeType dataType, bool overrideReadOnlyAndDataType, MarkAttributeDirty markDirty);
170+
} // anonymous namespace
138171

139172
Status emAfWriteAttributeExternal(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr,
140173
EmberAfAttributeType dataType)
141174
{
142-
return emAfWriteAttribute(endpoint, cluster, attributeID, dataPtr, dataType, false /* override read-only */);
175+
return emAfWriteAttribute(endpoint, cluster, attributeID, dataPtr, dataType, false /* override read-only */,
176+
MarkAttributeDirty::kIfChanged);
143177
}
144178

145179
Status emberAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr,
146180
EmberAfAttributeType dataType)
147181
{
148-
return emAfWriteAttribute(endpoint, cluster, attributeID, dataPtr, dataType, true /* override read-only */);
182+
return emAfWriteAttribute(endpoint, cluster, attributeID, dataPtr, dataType, true /* override read-only */,
183+
MarkAttributeDirty::kIfChanged);
184+
}
185+
186+
Status emberAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr,
187+
EmberAfAttributeType dataType, MarkAttributeDirty markDirty)
188+
{
189+
return emAfWriteAttribute(endpoint, cluster, attributeID, dataPtr, dataType, true /* override read-only */, markDirty);
149190
}
150191

151192
//------------------------------------------------------------------------------
@@ -207,8 +248,78 @@ static bool IsNullValue(const uint8_t * data, uint16_t dataLen, bool isAttribute
207248
return false;
208249
}
209250

251+
namespace {
252+
253+
/**
254+
* Helper function to determine whether the attribute value for the given
255+
* attribute is changing. On success, the isChanging outparam will be set to
256+
* whether the value is changing.
257+
*/
258+
Status AttributeValueIsChanging(EndpointId endpoint, ClusterId cluster, AttributeId attributeID,
259+
const EmberAfAttributeMetadata * metadata, uint8_t * newValueData, bool * isChanging)
260+
{
261+
EmberAfAttributeType attributeType = metadata->attributeType;
262+
263+
// We don't know how to size our buffer for strings in general, but if the
264+
// string happens to fit into our fixed-size buffer, great.
265+
size_t valueSize = metadata->size;
266+
constexpr size_t kMaxValueSize = 16; // ipv6adr
267+
if (valueSize > kMaxValueSize)
268+
{
269+
if (emberAfIsStringAttributeType(attributeType) || emberAfIsLongStringAttributeType(attributeType))
270+
{
271+
// It's a string that may not fit in our buffer. Just claim it's
272+
// changing, since we have no way to tell.
273+
*isChanging = true;
274+
return Status::Success;
275+
}
276+
277+
// Very much unexpected
278+
ChipLogError(Zcl, "Attribute type %d has too-large size %u", attributeType, static_cast<unsigned>(valueSize));
279+
return Status::ConstraintError;
280+
}
281+
282+
uint8_t oldValueBuffer[kMaxValueSize];
283+
// Cast to uint16_t is safe, because we checked valueSize <= kMaxValueSize above.
284+
if (emberAfReadAttribute(endpoint, cluster, attributeID, oldValueBuffer, static_cast<uint16_t>(valueSize)) != Status::Success)
285+
{
286+
// We failed to read the old value, so flag the value as changing to be safe.
287+
*isChanging = true;
288+
return Status::Success;
289+
}
290+
291+
if (emberAfIsStringAttributeType(attributeType))
292+
{
293+
size_t oldLength = emberAfStringLength(oldValueBuffer);
294+
size_t newLength = emberAfStringLength(newValueData);
295+
// The first byte of the buffer is the string length, and
296+
// oldLength/newLength refer to the number of bytes after that. We want
297+
// to include that first byte in our comparison, because null and empty
298+
// string have different values there but both return 0 from
299+
// emberAfStringLength.
300+
*isChanging = (oldLength != newLength) || (memcmp(oldValueBuffer, newValueData, oldLength + 1) != 0);
301+
}
302+
else if (emberAfIsLongStringAttributeType(attributeType))
303+
{
304+
size_t oldLength = emberAfLongStringLength(oldValueBuffer);
305+
size_t newLength = emberAfLongStringLength(newValueData);
306+
// The first two bytes of the buffer are the string length, and
307+
// oldLength/newLength refer to the number of bytes after that. We want
308+
// to include those first two bytes in our comparison, because null and
309+
// empty string have different values there but both return 0 from
310+
// emberAfLongStringLength.
311+
*isChanging = (oldLength != newLength) || (memcmp(oldValueBuffer, newValueData, oldLength + 2) != 0);
312+
}
313+
else
314+
{
315+
*isChanging = (memcmp(newValueData, oldValueBuffer, valueSize) != 0);
316+
}
317+
318+
return Status::Success;
319+
}
320+
210321
Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * data,
211-
EmberAfAttributeType dataType, bool overrideReadOnlyAndDataType)
322+
EmberAfAttributeType dataType, bool overrideReadOnlyAndDataType, MarkAttributeDirty markDirty)
212323
{
213324
const EmberAfAttributeMetadata * metadata = nullptr;
214325
EmberAfAttributeSearchRecord record;
@@ -286,12 +397,25 @@ Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId at
286397
}
287398
}
288399

400+
// Check whether anything is actually changing, before we do any work here.
401+
bool valueChanging;
402+
Status imStatus = AttributeValueIsChanging(endpoint, cluster, attributeID, metadata, data, &valueChanging);
403+
if (imStatus != Status::Success)
404+
{
405+
return imStatus;
406+
}
407+
408+
if (!valueChanging)
409+
{
410+
// Just do nothing.
411+
return Status::Success;
412+
}
413+
289414
const app::ConcreteAttributePath attributePath(endpoint, cluster, attributeID);
290415

291416
// Pre write attribute callback for all attribute changes,
292417
// regardless of cluster.
293-
Protocols::InteractionModel::Status imStatus =
294-
MatterPreAttributeChangeCallback(attributePath, dataType, emberAfAttributeSize(metadata), data);
418+
imStatus = MatterPreAttributeChangeCallback(attributePath, dataType, emberAfAttributeSize(metadata), data);
295419
if (imStatus != Protocols::InteractionModel::Status::Success)
296420
{
297421
return imStatus;
@@ -328,7 +452,10 @@ Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId at
328452
// The callee will weed out attributes that do not need to be stored.
329453
emAfSaveAttributeToStorageIfNeeded(data, endpoint, cluster, metadata);
330454

331-
MatterReportingAttributeChangeCallback(endpoint, cluster, attributeID);
455+
if (markDirty != MarkAttributeDirty::kNo)
456+
{
457+
MatterReportingAttributeChangeCallback(endpoint, cluster, attributeID);
458+
}
332459

333460
// Post write attribute callback for all attributes changes, regardless
334461
// of cluster.
@@ -341,6 +468,8 @@ Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId at
341468
return Status::Success;
342469
}
343470

471+
} // anonymous namespace
472+
344473
Status emberAfReadAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr, uint16_t readLength)
345474
{
346475
const EmberAfAttributeMetadata * metadata = nullptr;

src/app/util/attribute-table.h

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#pragma once
1919

20+
#include <app/util/af-types.h>
2021
#include <app/util/attribute-metadata.h>
2122
#include <lib/core/DataModelTypes.h>
2223
#include <protocols/interaction_model/StatusCode.h>
@@ -44,6 +45,17 @@ chip::Protocols::InteractionModel::Status emberAfWriteAttribute(chip::EndpointId
4445
chip::AttributeId attributeID, uint8_t * dataPtr,
4546
EmberAfAttributeType dataType);
4647

48+
/**
49+
* A version of emberAfWriteAttribute that allows controlling when the attribute
50+
* should be marked dirty. This is an overload, not an optional argument, to
51+
* reduce codesize at all the callsites that want to write without doing
52+
* anything special to control the dirty marking.
53+
*/
54+
chip::Protocols::InteractionModel::Status emberAfWriteAttribute(chip::EndpointId endpoint, chip::ClusterId cluster,
55+
chip::AttributeId attributeID, uint8_t * dataPtr,
56+
EmberAfAttributeType dataType,
57+
chip::app::MarkAttributeDirty markDirty);
58+
4759
/**
4860
* @brief Read the attribute value, performing all the checks.
4961
*

0 commit comments

Comments
 (0)