Skip to content

Commit a0bd56f

Browse files
MTRDevice should remember new DataVersion values even if the attribute value did not change.
We used to ignore "same value but new DataVersion" because some devices would spam reports that looked like that and we were constantly hitting storage for the DataVersion updates. But now that we have backoffs on the storage of our cluster data, we can go ahead and just note the new DataVersion and rely on that backoff to prevent hitting storage too much. The test update verifies that: 1) Reports with same value but new DataVersion do get persisted but are subject to the persistence backoff settings. 2) Reports with the same value and same DataVersion do not lead to any storage traffic.
1 parent e527611 commit a0bd56f

File tree

2 files changed

+81
-48
lines changed

2 files changed

+81
-48
lines changed

src/darwin/Framework/CHIP/MTRDevice_Concrete.mm

+2-4
Original file line numberDiff line numberDiff line change
@@ -3533,6 +3533,8 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray<NSDictionary<NSSt
35333533
NSNumber * dataVersion = attributeDataValue[MTRDataVersionKey];
35343534
MTRClusterPath * clusterPath = [MTRClusterPath clusterPathWithEndpointID:attributePath.endpoint clusterID:attributePath.cluster];
35353535
if (dataVersion) {
3536+
[self _noteDataVersion:dataVersion forClusterPath:clusterPath];
3537+
35363538
// Remove data version from what we cache in memory
35373539
attributeDataValue = [self _dataValueWithoutDataVersion:attributeDataValue];
35383540
}
@@ -3545,10 +3547,6 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray<NSDictionary<NSSt
35453547
#endif
35463548
// Now that we have grabbed previousValue, update our cache with the attribute value.
35473549
if (readCacheValueChanged) {
3548-
if (dataVersion) {
3549-
[self _noteDataVersion:dataVersion forClusterPath:clusterPath];
3550-
}
3551-
35523550
[self _pruneStoredDataForPath:attributePath missingFrom:attributeDataValue];
35533551

35543552
if (!_deviceConfigurationChanged) {

src/darwin/Framework/CHIPTests/MTRDeviceTests.m

+79-44
Original file line numberDiff line numberDiff line change
@@ -3746,11 +3746,16 @@ - (void)test035_TestMTRDeviceSubscriptionNotEstablishedOverXPC
37463746
}
37473747

37483748
- (NSArray<NSDictionary<NSString *, id> *> *)_testAttributeReportWithValue:(unsigned int)testValue
3749+
{
3750+
return [self _testAttributeReportWithValue:testValue dataVersion:testValue];
3751+
}
3752+
3753+
- (NSArray<NSDictionary<NSString *, id> *> *)_testAttributeReportWithValue:(unsigned int)testValue dataVersion:(unsigned int)dataVersion
37493754
{
37503755
return @[ @{
37513756
MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeLevelControlID) attributeID:@(MTRAttributeIDTypeClusterLevelControlAttributeCurrentLevelID)],
37523757
MTRDataKey : @ {
3753-
MTRDataVersionKey : @(testValue),
3758+
MTRDataVersionKey : @(dataVersion),
37543759
MTRTypeKey : MTRUnsignedIntegerValueType,
37553760
MTRValueKey : @(testValue),
37563761
}
@@ -3809,7 +3814,7 @@ - (void)test036_TestStorageBehaviorConfiguration
38093814
[device setDelegate:delegate queue:queue];
38103815

38113816
// Use a counter that will be incremented for each report as the value.
3812-
unsigned int currentTestValue = 1;
3817+
__block unsigned int currentTestValue = 1;
38133818

38143819
// Initial setup: Inject report and see that the attribute persisted. No delay is
38153820
// expected for the first (priming) report.
@@ -3928,54 +3933,84 @@ - (void)test036_TestStorageBehaviorConfiguration
39283933
XCTAssertLessThan(reportToPersistenceDelay, baseTestDelayTime * 2 * 5 * 1.3);
39293934

39303935
// Test 4: test reporting excessively, and see that persistence does not happen until
3931-
// reporting frequency goes back above the threshold
3932-
reportEndTime = nil;
3933-
dataPersistedTime = nil;
3934-
XCTestExpectation * dataPersisted4 = [self expectationWithDescription:@"data persisted 4"];
3935-
delegate.onClusterDataPersisted = ^{
3936-
os_unfair_lock_lock(&lock);
3937-
if (!dataPersistedTime) {
3938-
dataPersistedTime = [NSDate now];
3939-
}
3940-
os_unfair_lock_unlock(&lock);
3941-
[dataPersisted4 fulfill];
3942-
};
3943-
3944-
// Set report times with short delay and check that the multiplier is engaged
3945-
[device unitTestSetMostRecentReportTimes:[NSMutableArray arrayWithArray:@[
3946-
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 4)],
3947-
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 3)],
3948-
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 2)],
3949-
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1)],
3950-
]]];
3951-
3952-
// Inject report that makes MTRDevice detect the device is reporting excessively
3953-
[device unitTestInjectAttributeReport:[self _testAttributeReportWithValue:currentTestValue++] fromSubscription:YES];
3936+
// reporting frequency goes back below the threshold
3937+
__auto_type excessiveReportTest = ^(unsigned int testId, NSArray<NSDictionary<NSString *, id> *> * (^reportGenerator)(void), bool expectPersistence) {
3938+
reportEndTime = nil;
3939+
dataPersistedTime = nil;
3940+
XCTestExpectation * dataPersisted = [self expectationWithDescription:[NSString stringWithFormat:@"data persisted %u", testId]];
3941+
dataPersisted.inverted = !expectPersistence;
3942+
delegate.onClusterDataPersisted = ^{
3943+
os_unfair_lock_lock(&lock);
3944+
if (!dataPersistedTime) {
3945+
dataPersistedTime = [NSDate now];
3946+
}
3947+
os_unfair_lock_unlock(&lock);
3948+
[dataPersisted fulfill];
3949+
};
39543950

3955-
// Now keep reporting excessively for base delay time max times max multiplier, plus a bit more
3956-
NSDate * excessiveStartTime = [NSDate now];
3957-
for (;;) {
3958-
usleep((useconds_t) (baseTestDelayTime * 0.1 * USEC_PER_SEC));
3959-
[device unitTestInjectAttributeReport:[self _testAttributeReportWithValue:currentTestValue++] fromSubscription:YES];
3960-
NSTimeInterval elapsed = -[excessiveStartTime timeIntervalSinceNow];
3961-
if (elapsed > (baseTestDelayTime * 2 * 5 * 1.2)) {
3962-
break;
3951+
// Set report times with short delay and check that the multiplier is engaged
3952+
[device unitTestSetMostRecentReportTimes:[NSMutableArray arrayWithArray:@[
3953+
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 4)],
3954+
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 3)],
3955+
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1 * 2)],
3956+
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 0.1)],
3957+
]]];
3958+
3959+
// Inject report that makes MTRDevice detect the device is reporting excessively
3960+
[device unitTestInjectAttributeReport:reportGenerator() fromSubscription:YES];
3961+
3962+
// Now keep reporting excessively for base delay time max times max multiplier, plus a bit more
3963+
NSDate * excessiveStartTime = [NSDate now];
3964+
for (;;) {
3965+
usleep((useconds_t) (baseTestDelayTime * 0.1 * USEC_PER_SEC));
3966+
[device unitTestInjectAttributeReport:reportGenerator() fromSubscription:YES];
3967+
NSTimeInterval elapsed = -[excessiveStartTime timeIntervalSinceNow];
3968+
if (elapsed > (baseTestDelayTime * 2 * 5 * 1.2)) {
3969+
break;
3970+
}
39633971
}
3964-
}
39653972

3966-
// Check that persistence has not happened because it's now turned off
3967-
XCTAssertNil(dataPersistedTime);
3973+
// Check that persistence has not happened because it's now turned off
3974+
XCTAssertNil(dataPersistedTime);
39683975

3969-
// Now force report times to large number, to simulate time passage
3970-
[device unitTestSetMostRecentReportTimes:[NSMutableArray arrayWithArray:@[
3971-
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 10)],
3972-
]]];
3976+
// Now force report times to large number, to simulate time passage
3977+
[device unitTestSetMostRecentReportTimes:[NSMutableArray arrayWithArray:@[
3978+
[NSDate dateWithTimeIntervalSinceNow:-(baseTestDelayTime * 10)],
3979+
]]];
39733980

3974-
// And inject a report to trigger MTRDevice to recalculate that this device is no longer
3975-
// reporting excessively
3976-
[device unitTestInjectAttributeReport:[self _testAttributeReportWithValue:currentTestValue++] fromSubscription:YES];
3981+
// And inject a report to trigger MTRDevice to recalculate that this device is no longer
3982+
// reporting excessively
3983+
[device unitTestInjectAttributeReport:reportGenerator() fromSubscription:YES];
3984+
3985+
[self waitForExpectations:@[ dataPersisted ] timeout:60];
3986+
};
39773987

3978-
[self waitForExpectations:@[ dataPersisted4 ] timeout:60];
3988+
excessiveReportTest(
3989+
4, ^{
3990+
return [self _testAttributeReportWithValue:currentTestValue++];
3991+
}, true);
3992+
3993+
// Test 5: test reporting excessively with the same value and different data
3994+
// versions, and see that persistence does not happen until reporting
3995+
// frequency goes back below the threshold.
3996+
__block __auto_type dataVersion = currentTestValue;
3997+
// We incremented currentTestValue after injecting the last report. Make sure all the new
3998+
// reports use that last-reported value.
3999+
__auto_type lastReportedValue = currentTestValue - 1;
4000+
excessiveReportTest(
4001+
5, ^{
4002+
return [self _testAttributeReportWithValue:lastReportedValue dataVersion:dataVersion++];
4003+
}, true);
4004+
4005+
// Test 6: test reporting excessively with the same value and same data
4006+
// version, and see that persistence does not happen at all.
4007+
// We incremented dataVersion after injecting the last report. Make sure all the new
4008+
// reports use that last-reported value.
4009+
__block __auto_type lastReportedDataVersion = dataVersion - 1;
4010+
excessiveReportTest(
4011+
6, ^{
4012+
return [self _testAttributeReportWithValue:lastReportedValue dataVersion:lastReportedDataVersion];
4013+
}, false);
39794014

39804015
delegate.onReportEnd = nil;
39814016
delegate.onClusterDataPersisted = nil;

0 commit comments

Comments
 (0)