Skip to content

Commit 1252403

Browse files
agnersmarcelveldt
authored andcommitted
[python] update cached attributes incrementally (#26774)
* [python] update cached attributes incrementally Instead of rebuilding the cache from scratch on every report, just update the Endpoint/Cluster/Attributes which actually changed. Obviously, this is significantly faster, especially for small updates. * Update src/controller/python/chip/clusters/Attribute.py Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com> * Address review feedback * Address one more unnecessary if bracket * Add attribute cache tests using integration tests * Fix lintinig issues * Apply same report interval * Increase timeout for mobile-device-test.py --------- Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
1 parent 41484ab commit 1252403

File tree

3 files changed

+137
-52
lines changed

3 files changed

+137
-52
lines changed

src/controller/python/chip/clusters/Attribute.py

+55-51
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def UpdateTLV(self, path: AttributePath, dataVersion: int, data: Union[bytes, V
383383

384384
clusterCache[path.AttributeId] = data
385385

386-
def UpdateCachedData(self):
386+
def UpdateCachedData(self, changedPathSet: set[AttributePath]):
387387
''' This converts the raw TLV data into a cluster object format.
388388
389389
Two formats are available:
@@ -401,68 +401,72 @@ def UpdateCachedData(self):
401401
tlvCache = self.attributeTLVCache
402402
attributeCache = self.attributeCache
403403

404-
for endpoint in tlvCache:
405-
if (endpoint not in attributeCache):
406-
attributeCache[endpoint] = {}
404+
for attributePath in changedPathSet:
405+
endpointId = attributePath.EndpointId
407406

408-
endpointCache = attributeCache[endpoint]
407+
if endpointId not in attributeCache:
408+
attributeCache[endpointId] = {}
409409

410-
for cluster in tlvCache[endpoint]:
411-
if cluster not in _ClusterIndex:
410+
endpointCache = attributeCache[endpointId]
411+
412+
clusterId = attributePath.ClusterId
413+
414+
if clusterId not in _ClusterIndex:
415+
#
416+
# #22599 tracks dealing with unknown clusters more
417+
# gracefully so that clients can still access this data.
418+
#
419+
continue
420+
421+
clusterType = _ClusterIndex[clusterId]
422+
423+
if clusterType not in endpointCache:
424+
endpointCache[clusterType] = {}
425+
426+
clusterCache = endpointCache[clusterType]
427+
clusterDataVersion = self.versionList.get(
428+
endpointId, {}).get(clusterId, None)
429+
430+
if self.returnClusterObject:
431+
try:
432+
# Since the TLV data is already organized by attribute tags, we can trivially convert to a cluster object representation.
433+
endpointCache[clusterType] = clusterType.FromDict(
434+
data=clusterType.descriptor.TagDictToLabelDict([], tlvCache[endpointId][clusterId]))
435+
endpointCache[clusterType].SetDataVersion(
436+
clusterDataVersion)
437+
except Exception as ex:
438+
decodedValue = ValueDecodeFailure(
439+
tlvCache[endpointId][clusterId], ex)
440+
endpointCache[clusterType] = decodedValue
441+
else:
442+
clusterCache[DataVersion] = clusterDataVersion
443+
444+
attributeId = attributePath.AttributeId
445+
446+
value = tlvCache[endpointId][clusterId][attributeId]
447+
448+
if (clusterId, attributeId) not in _AttributeIndex:
412449
#
413450
# #22599 tracks dealing with unknown clusters more
414451
# gracefully so that clients can still access this data.
415452
#
416453
continue
417454

418-
clusterType = _ClusterIndex[cluster]
419-
420-
if (clusterType not in endpointCache):
421-
endpointCache[clusterType] = {}
455+
attributeType = _AttributeIndex[(clusterId, attributeId)][0]
422456

423-
clusterCache = endpointCache[clusterType]
424-
clusterDataVersion = self.versionList.get(
425-
endpoint, {}).get(cluster, None)
457+
if attributeType not in clusterCache:
458+
clusterCache[attributeType] = {}
426459

427-
if (self.returnClusterObject):
460+
if isinstance(value, ValueDecodeFailure):
461+
clusterCache[attributeType] = value
462+
else:
428463
try:
429-
# Since the TLV data is already organized by attribute tags, we can trivially convert to a cluster object representation.
430-
endpointCache[clusterType] = clusterType.FromDict(
431-
data=clusterType.descriptor.TagDictToLabelDict([], tlvCache[endpoint][cluster]))
432-
endpointCache[clusterType].SetDataVersion(
433-
clusterDataVersion)
464+
decodedValue = attributeType.FromTagDictOrRawValue(
465+
tlvCache[endpointId][clusterId][attributeId])
434466
except Exception as ex:
435-
decodedValue = ValueDecodeFailure(
436-
tlvCache[endpoint][cluster], ex)
437-
endpointCache[clusterType] = decodedValue
438-
else:
439-
clusterCache[DataVersion] = clusterDataVersion
440-
for attribute in tlvCache[endpoint][cluster]:
441-
value = tlvCache[endpoint][cluster][attribute]
442-
443-
if (cluster, attribute) not in _AttributeIndex:
444-
#
445-
# #22599 tracks dealing with unknown clusters more
446-
# gracefully so that clients can still access this data.
447-
#
448-
continue
449-
450-
attributeType = _AttributeIndex[(
451-
cluster, attribute)][0]
452-
453-
if (attributeType not in clusterCache):
454-
clusterCache[attributeType] = {}
455-
456-
if (type(value) is ValueDecodeFailure):
457-
clusterCache[attributeType] = value
458-
else:
459-
try:
460-
decodedValue = attributeType.FromTagDictOrRawValue(
461-
tlvCache[endpoint][cluster][attribute])
462-
except Exception as ex:
463-
decodedValue = ValueDecodeFailure(value, ex)
467+
decodedValue = ValueDecodeFailure(value, ex)
464468

465-
clusterCache[attributeType] = decodedValue
469+
clusterCache[attributeType] = decodedValue
466470

467471

468472
class SubscriptionTransaction:
@@ -766,7 +770,7 @@ def _handleReportBegin(self):
766770
pass
767771

768772
def _handleReportEnd(self):
769-
self._cache.UpdateCachedData()
773+
self._cache.UpdateCachedData(self._changedPathSet)
770774

771775
if (self._subscription_handler is not None):
772776
for change in self._changedPathSet:

src/controller/python/test/test_scripts/cluster_objects.py

+81
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,85 @@ def subUpdate(path: TypedAttributePath, transaction: SubscriptionTransaction):
200200

201201
sub.Shutdown()
202202

203+
@ classmethod
204+
@ base.test_case
205+
async def TestAttributeCacheAttributeView(cls, devCtrl):
206+
logger.info("Test AttributeCache Attribute-View")
207+
sub: SubscriptionTransaction = await devCtrl.ReadAttribute(nodeid=NODE_ID, attributes=[(1, Clusters.OnOff.Attributes.OnOff)], returnClusterObject=False, reportInterval=(3, 10))
208+
209+
event = asyncio.Event()
210+
211+
def subUpdate(path: TypedAttributePath, transaction: SubscriptionTransaction):
212+
event.set()
213+
214+
sub.SetAttributeUpdateCallback(subUpdate)
215+
216+
try:
217+
data = sub.GetAttributes()
218+
req = Clusters.OnOff.Commands.On()
219+
await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=req)
220+
221+
await asyncio.wait_for(event.wait(), timeout=11)
222+
223+
if (data[1][Clusters.OnOff][Clusters.OnOff.Attributes.OnOff] != 1):
224+
raise ValueError("Current On/Off state should be 1")
225+
226+
event.clear()
227+
228+
req = Clusters.OnOff.Commands.Off()
229+
await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=req)
230+
231+
await asyncio.wait_for(event.wait(), timeout=11)
232+
233+
if (data[1][Clusters.OnOff][Clusters.OnOff.Attributes.OnOff] != 0):
234+
raise ValueError("Current On/Off state should be 0")
235+
236+
except TimeoutError:
237+
raise AssertionError("Did not receive updated attribute")
238+
finally:
239+
sub.Shutdown()
240+
241+
@ classmethod
242+
@ base.test_case
243+
async def TestAttributeCacheClusterView(cls, devCtrl):
244+
logger.info("Test AttributeCache Cluster-View")
245+
sub: SubscriptionTransaction = await devCtrl.ReadAttribute(nodeid=NODE_ID, attributes=[(1, Clusters.OnOff.Attributes.OnOff)], returnClusterObject=True, reportInterval=(3, 10))
246+
247+
event = asyncio.Event()
248+
249+
def subUpdate(path: TypedAttributePath, transaction: SubscriptionTransaction):
250+
event.set()
251+
252+
sub.SetAttributeUpdateCallback(subUpdate)
253+
254+
try:
255+
data = sub.GetAttributes()
256+
257+
req = Clusters.OnOff.Commands.On()
258+
await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=req)
259+
260+
await asyncio.wait_for(event.wait(), timeout=11)
261+
262+
cluster: Clusters.OnOff = data[1][Clusters.OnOff]
263+
if (not cluster.onOff):
264+
raise ValueError("Current On/Off state should be True")
265+
266+
event.clear()
267+
268+
req = Clusters.OnOff.Commands.Off()
269+
await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=req)
270+
271+
await asyncio.wait_for(event.wait(), timeout=11)
272+
273+
cluster: Clusters.OnOff = data[1][Clusters.OnOff]
274+
if (cluster.onOff):
275+
raise ValueError("Current On/Off state should be False")
276+
277+
except TimeoutError:
278+
raise AssertionError("Did not receive updated attribute")
279+
finally:
280+
sub.Shutdown()
281+
203282
@ classmethod
204283
@ base.test_case
205284
async def TestSubscribeZeroMinInterval(cls, devCtrl):
@@ -638,6 +717,8 @@ async def RunTest(cls, devCtrl):
638717
await cls.TestReadAttributeRequests(devCtrl)
639718
await cls.TestSubscribeZeroMinInterval(devCtrl)
640719
await cls.TestSubscribeAttribute(devCtrl)
720+
await cls.TestAttributeCacheAttributeView(devCtrl)
721+
await cls.TestAttributeCacheClusterView(devCtrl)
641722
await cls.TestMixedReadAttributeAndEvents(devCtrl)
642723
# Note: Write will change some attribute values, always put it after read tests
643724
await cls.TestWriteRequest(devCtrl)

src/test_driver/linux-cirque/MobileDeviceTest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def run_controller_test(self):
9999
CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_repl-0.0-py3-none-any.whl")))
100100

101101
command = ("gdb -batch -return-child-result -q -ex run -ex \"thread apply all bt\" "
102-
"--args python3 {} -t 240 -a {} --paa-trust-store-path {}").format(
102+
"--args python3 {} -t 300 -a {} --paa-trust-store-path {}").format(
103103
os.path.join(
104104
CHIP_REPO, "src/controller/python/test/test_scripts/mobile-device-test.py"), ethernet_ip,
105105
os.path.join(CHIP_REPO, MATTER_DEVELOPMENT_PAA_ROOT_CERTS))

0 commit comments

Comments
 (0)