Skip to content

Commit 8b86799

Browse files
Implement write coalescing in MTRDevice. (#32994)
This uses the existing batching infrastructure to replace a write immediately followed by another write to the same attribute with just a single write with the second value.
1 parent f28a8a2 commit 8b86799

File tree

4 files changed

+141
-6
lines changed

4 files changed

+141
-6
lines changed

src/darwin/Framework/CHIP/MTRDevice.mm

+71-6
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,16 @@ typedef NS_ENUM(NSUInteger, MTRDeviceReadRequestFieldIndex) {
165165
MTRDeviceReadRequestFieldParamsIndex = 1
166166
};
167167

168+
typedef NS_ENUM(NSUInteger, MTRDeviceWriteRequestFieldIndex) {
169+
MTRDeviceWriteRequestFieldPathIndex = 0,
170+
MTRDeviceWriteRequestFieldValueIndex = 1,
171+
MTRDeviceWriteRequestFieldTimeoutIndex = 2,
172+
MTRDeviceWriteRequestFieldExpectedValueIDIndex = 3,
173+
};
174+
168175
typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemBatchingID) {
169176
MTRDeviceWorkItemBatchingReadID = 1,
177+
MTRDeviceWorkItemBatchingWriteID = 2,
170178
};
171179

172180
typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemDuplicateTypeID) {
@@ -1658,6 +1666,50 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
16581666

16591667
MTRAsyncWorkItem * workItem = [[MTRAsyncWorkItem alloc] initWithQueue:self.queue];
16601668
uint64_t workItemID = workItem.uniqueID; // capture only the ID, not the work item
1669+
NSNumber * nodeID = _nodeID;
1670+
1671+
// Write request data is an array of items (for now always length 1). Each
1672+
// item is an array containing:
1673+
//
1674+
// [ attribute path, value, timedWriteTimeout, expectedValueID ]
1675+
//
1676+
// where expectedValueID is stored as NSNumber and NSNull represents nil timeouts
1677+
auto * writeData = @[ attributePath, [value copy], timeout ?: [NSNull null], @(expectedValueID) ];
1678+
1679+
NSMutableArray<NSArray *> * writeRequests = [NSMutableArray arrayWithObject:writeData];
1680+
1681+
[workItem setBatchingID:MTRDeviceWorkItemBatchingWriteID data:writeRequests handler:^(id opaqueDataCurrent, id opaqueDataNext) {
1682+
mtr_hide(self); // don't capture self accidentally
1683+
NSMutableArray<NSArray *> * writeRequestsCurrent = opaqueDataCurrent;
1684+
NSMutableArray<NSArray *> * writeRequestsNext = opaqueDataNext;
1685+
1686+
if (writeRequestsCurrent.count != 1) {
1687+
// Very unexpected!
1688+
MTR_LOG_ERROR("Batching write attribute work item [%llu]: Unexpected write request count %tu", workItemID, writeRequestsCurrent.count);
1689+
return MTRNotBatched;
1690+
}
1691+
1692+
MTRBatchingOutcome outcome = MTRNotBatched;
1693+
while (writeRequestsNext.count) {
1694+
// If paths don't match, we cannot replace the earlier write
1695+
// with the later one.
1696+
if (![writeRequestsNext[0][MTRDeviceWriteRequestFieldPathIndex]
1697+
isEqual:writeRequestsCurrent[0][MTRDeviceWriteRequestFieldPathIndex]]) {
1698+
MTR_LOG_INFO("Batching write attribute work item [%llu]: cannot replace with next work item due to path mismatch", workItemID);
1699+
return outcome;
1700+
}
1701+
1702+
// Replace our one request with the first one from the next item.
1703+
auto writeItem = writeRequestsNext.firstObject;
1704+
[writeRequestsNext removeObjectAtIndex:0];
1705+
[writeRequestsCurrent replaceObjectAtIndex:0 withObject:writeItem];
1706+
MTR_LOG_INFO("Batching write attribute work item [%llu]: replaced with new write value %@ [0x%016llX]",
1707+
workItemID, writeItem, nodeID.unsignedLongLongValue);
1708+
outcome = MTRBatchedPartially;
1709+
}
1710+
NSCAssert(writeRequestsNext.count == 0, @"should have batched everything or returned early");
1711+
return MTRBatchedFully;
1712+
}];
16611713
// The write operation will install a duplicate check handler, to return NO for "isDuplicate". Since a write operation may
16621714
// change values, only read requests after this should be considered for duplicate requests.
16631715
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
@@ -1666,18 +1718,31 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
16661718
}];
16671719
[workItem setReadyHandler:^(MTRDevice * self, NSInteger retryCount, MTRAsyncWorkCompletionBlock completion) {
16681720
MTRBaseDevice * baseDevice = [self newBaseDevice];
1721+
// Make sure to use writeRequests here, because that's what our batching
1722+
// handler will modify as needed.
1723+
NSCAssert(writeRequests.count == 1, @"Incorrect number of write requests: %tu", writeRequests.count);
1724+
1725+
auto * request = writeRequests[0];
1726+
MTRAttributePath * path = request[MTRDeviceWriteRequestFieldPathIndex];
1727+
1728+
id timedWriteTimeout = request[MTRDeviceWriteRequestFieldTimeoutIndex];
1729+
if (timedWriteTimeout == [NSNull null]) {
1730+
timedWriteTimeout = nil;
1731+
}
1732+
16691733
[baseDevice
1670-
writeAttributeWithEndpointID:endpointID
1671-
clusterID:clusterID
1672-
attributeID:attributeID
1673-
value:value
1674-
timedWriteTimeout:timeout
1734+
writeAttributeWithEndpointID:path.endpoint
1735+
clusterID:path.cluster
1736+
attributeID:path.attribute
1737+
value:request[MTRDeviceWriteRequestFieldValueIndex]
1738+
timedWriteTimeout:timedWriteTimeout
16751739
queue:self.queue
16761740
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
16771741
if (error) {
16781742
MTR_LOG_ERROR("Write attribute work item [%llu] failed: %@", workItemID, error);
16791743
if (useValueAsExpectedValue) {
1680-
[self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
1744+
NSNumber * expectedValueID = request[MTRDeviceWriteRequestFieldExpectedValueIDIndex];
1745+
[self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID.unsignedLongLongValue];
16811746
}
16821747
}
16831748
completion(MTRAsyncWorkComplete);

src/darwin/Framework/CHIPTests/MTRDeviceTests.m

+65
Original file line numberDiff line numberDiff line change
@@ -2563,6 +2563,71 @@ - (void)test028_TimeZoneAndDST
25632563
#endif // MTR_ENABLE_PROVISIONAL
25642564
}
25652565

2566+
- (void)test029_MTRDeviceWriteCoalescing
2567+
{
2568+
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
2569+
[sController.controllerDataStore clearAllStoredAttributes];
2570+
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
2571+
XCTAssertEqual(storedAttributesAfterClear.count, 0);
2572+
2573+
__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
2574+
dispatch_queue_t queue = dispatch_get_main_queue();
2575+
2576+
// Given reachable state becomes true before underlying OnSubscriptionEstablished callback, this expectation is necessary but
2577+
// not sufficient as a mark to the end of reports
2578+
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
2579+
2580+
__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
2581+
delegate.onReportEnd = ^() {
2582+
[gotReportsExpectation fulfill];
2583+
};
2584+
// Skip reports for expected values so we actually have some idea of what
2585+
// the server is reporting.
2586+
delegate.skipExpectedValuesForWrite = YES;
2587+
2588+
[device setDelegate:delegate queue:queue];
2589+
2590+
[self waitForExpectations:@[ gotReportsExpectation ] timeout:60];
2591+
2592+
delegate.onReportEnd = nil;
2593+
2594+
uint16_t testOnTimeValue = 10;
2595+
XCTestExpectation * onTimeWriteSuccess = [self expectationWithDescription:@"OnTime write success"];
2596+
delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
2597+
for (NSDictionary<NSString *, id> * attributeReponseValue in data) {
2598+
MTRAttributePath * path = attributeReponseValue[MTRAttributePathKey];
2599+
if (path.cluster.unsignedIntValue == MTRClusterIDTypeOnOffID && path.attribute.unsignedLongValue == MTRAttributeIDTypeClusterOnOffAttributeOnTimeID) {
2600+
NSDictionary * dataValue = attributeReponseValue[MTRDataKey];
2601+
NSNumber * onTimeValue = dataValue[MTRValueKey];
2602+
if ([onTimeValue isEqual:@(testOnTimeValue + 4)]) {
2603+
[onTimeWriteSuccess fulfill];
2604+
} else {
2605+
// The first write we did might get reported, but none of
2606+
// the other ones should be.
2607+
XCTAssertEqualObjects(onTimeValue, @(testOnTimeValue + 1));
2608+
}
2609+
}
2610+
}
2611+
};
2612+
2613+
__auto_type writeOnTimeValue = ^(uint16_t value) {
2614+
NSDictionary * writeValue = @{ MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(value) };
2615+
[device writeAttributeWithEndpointID:@(1)
2616+
clusterID:@(MTRClusterIDTypeOnOffID)
2617+
attributeID:@(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID)
2618+
value:writeValue
2619+
expectedValueInterval:@(0)
2620+
timedWriteTimeout:nil];
2621+
};
2622+
2623+
writeOnTimeValue(testOnTimeValue + 1);
2624+
writeOnTimeValue(testOnTimeValue + 2);
2625+
writeOnTimeValue(testOnTimeValue + 3);
2626+
writeOnTimeValue(testOnTimeValue + 4);
2627+
2628+
[self waitForExpectations:@[ onTimeWriteSuccess ] timeout:10];
2629+
}
2630+
25662631
- (void)test900_SubscribeAllAttributes
25672632
{
25682633
MTRBaseDevice * device = GetConnectedDevice();

src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray<NSDictionary<NSString *
2828
@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onEventDataReceived;
2929
@property (nonatomic, nullable) dispatch_block_t onReportEnd;
3030
@property (nonatomic, nullable) dispatch_block_t onDeviceCachePrimed;
31+
@property (nonatomic) BOOL skipExpectedValuesForWrite;
3132
@end
3233

3334
NS_ASSUME_NONNULL_END

src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m

+4
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ - (void)deviceCachePrimed:(MTRDevice *)device
6060
}
6161
}
6262

63+
- (BOOL)unitTestShouldSkipExpectedValuesForWrite:(MTRDevice *)device
64+
{
65+
return self.skipExpectedValuesForWrite;
66+
}
6367
@end

0 commit comments

Comments
 (0)