Skip to content

Commit

Permalink
Implement write coalescing in MTRDevice. (#32994)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bzbarsky-apple authored Apr 16, 2024
1 parent f28a8a2 commit 8b86799
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 6 deletions.
77 changes: 71 additions & 6 deletions src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,16 @@ typedef NS_ENUM(NSUInteger, MTRDeviceReadRequestFieldIndex) {
MTRDeviceReadRequestFieldParamsIndex = 1
};

typedef NS_ENUM(NSUInteger, MTRDeviceWriteRequestFieldIndex) {
MTRDeviceWriteRequestFieldPathIndex = 0,
MTRDeviceWriteRequestFieldValueIndex = 1,
MTRDeviceWriteRequestFieldTimeoutIndex = 2,
MTRDeviceWriteRequestFieldExpectedValueIDIndex = 3,
};

typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemBatchingID) {
MTRDeviceWorkItemBatchingReadID = 1,
MTRDeviceWorkItemBatchingWriteID = 2,
};

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

MTRAsyncWorkItem * workItem = [[MTRAsyncWorkItem alloc] initWithQueue:self.queue];
uint64_t workItemID = workItem.uniqueID; // capture only the ID, not the work item
NSNumber * nodeID = _nodeID;

// Write request data is an array of items (for now always length 1). Each
// item is an array containing:
//
// [ attribute path, value, timedWriteTimeout, expectedValueID ]
//
// where expectedValueID is stored as NSNumber and NSNull represents nil timeouts
auto * writeData = @[ attributePath, [value copy], timeout ?: [NSNull null], @(expectedValueID) ];

NSMutableArray<NSArray *> * writeRequests = [NSMutableArray arrayWithObject:writeData];

[workItem setBatchingID:MTRDeviceWorkItemBatchingWriteID data:writeRequests handler:^(id opaqueDataCurrent, id opaqueDataNext) {
mtr_hide(self); // don't capture self accidentally
NSMutableArray<NSArray *> * writeRequestsCurrent = opaqueDataCurrent;
NSMutableArray<NSArray *> * writeRequestsNext = opaqueDataNext;

if (writeRequestsCurrent.count != 1) {
// Very unexpected!
MTR_LOG_ERROR("Batching write attribute work item [%llu]: Unexpected write request count %tu", workItemID, writeRequestsCurrent.count);
return MTRNotBatched;
}

MTRBatchingOutcome outcome = MTRNotBatched;
while (writeRequestsNext.count) {
// If paths don't match, we cannot replace the earlier write
// with the later one.
if (![writeRequestsNext[0][MTRDeviceWriteRequestFieldPathIndex]
isEqual:writeRequestsCurrent[0][MTRDeviceWriteRequestFieldPathIndex]]) {
MTR_LOG_INFO("Batching write attribute work item [%llu]: cannot replace with next work item due to path mismatch", workItemID);
return outcome;
}

// Replace our one request with the first one from the next item.
auto writeItem = writeRequestsNext.firstObject;
[writeRequestsNext removeObjectAtIndex:0];
[writeRequestsCurrent replaceObjectAtIndex:0 withObject:writeItem];
MTR_LOG_INFO("Batching write attribute work item [%llu]: replaced with new write value %@ [0x%016llX]",
workItemID, writeItem, nodeID.unsignedLongLongValue);
outcome = MTRBatchedPartially;
}
NSCAssert(writeRequestsNext.count == 0, @"should have batched everything or returned early");
return MTRBatchedFully;
}];
// The write operation will install a duplicate check handler, to return NO for "isDuplicate". Since a write operation may
// change values, only read requests after this should be considered for duplicate requests.
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
Expand All @@ -1666,18 +1718,31 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
}];
[workItem setReadyHandler:^(MTRDevice * self, NSInteger retryCount, MTRAsyncWorkCompletionBlock completion) {
MTRBaseDevice * baseDevice = [self newBaseDevice];
// Make sure to use writeRequests here, because that's what our batching
// handler will modify as needed.
NSCAssert(writeRequests.count == 1, @"Incorrect number of write requests: %tu", writeRequests.count);

auto * request = writeRequests[0];
MTRAttributePath * path = request[MTRDeviceWriteRequestFieldPathIndex];

id timedWriteTimeout = request[MTRDeviceWriteRequestFieldTimeoutIndex];
if (timedWriteTimeout == [NSNull null]) {
timedWriteTimeout = nil;
}

[baseDevice
writeAttributeWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID
value:value
timedWriteTimeout:timeout
writeAttributeWithEndpointID:path.endpoint
clusterID:path.cluster
attributeID:path.attribute
value:request[MTRDeviceWriteRequestFieldValueIndex]
timedWriteTimeout:timedWriteTimeout
queue:self.queue
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
if (error) {
MTR_LOG_ERROR("Write attribute work item [%llu] failed: %@", workItemID, error);
if (useValueAsExpectedValue) {
[self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
NSNumber * expectedValueID = request[MTRDeviceWriteRequestFieldExpectedValueIDIndex];
[self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID.unsignedLongLongValue];
}
}
completion(MTRAsyncWorkComplete);
Expand Down
65 changes: 65 additions & 0 deletions src/darwin/Framework/CHIPTests/MTRDeviceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -2563,6 +2563,71 @@ - (void)test028_TimeZoneAndDST
#endif // MTR_ENABLE_PROVISIONAL
}

- (void)test029_MTRDeviceWriteCoalescing
{
// Ensure the test starts with clean slate, even with MTRDeviceControllerLocalTestStorage enabled
[sController.controllerDataStore clearAllStoredAttributes];
NSArray * storedAttributesAfterClear = [sController.controllerDataStore getStoredAttributesForNodeID:@(kDeviceId)];
XCTAssertEqual(storedAttributesAfterClear.count, 0);

__auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId deviceController:sController];
dispatch_queue_t queue = dispatch_get_main_queue();

// Given reachable state becomes true before underlying OnSubscriptionEstablished callback, this expectation is necessary but
// not sufficient as a mark to the end of reports
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];

__auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
delegate.onReportEnd = ^() {
[gotReportsExpectation fulfill];
};
// Skip reports for expected values so we actually have some idea of what
// the server is reporting.
delegate.skipExpectedValuesForWrite = YES;

[device setDelegate:delegate queue:queue];

[self waitForExpectations:@[ gotReportsExpectation ] timeout:60];

delegate.onReportEnd = nil;

uint16_t testOnTimeValue = 10;
XCTestExpectation * onTimeWriteSuccess = [self expectationWithDescription:@"OnTime write success"];
delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
for (NSDictionary<NSString *, id> * attributeReponseValue in data) {
MTRAttributePath * path = attributeReponseValue[MTRAttributePathKey];
if (path.cluster.unsignedIntValue == MTRClusterIDTypeOnOffID && path.attribute.unsignedLongValue == MTRAttributeIDTypeClusterOnOffAttributeOnTimeID) {
NSDictionary * dataValue = attributeReponseValue[MTRDataKey];
NSNumber * onTimeValue = dataValue[MTRValueKey];
if ([onTimeValue isEqual:@(testOnTimeValue + 4)]) {
[onTimeWriteSuccess fulfill];
} else {
// The first write we did might get reported, but none of
// the other ones should be.
XCTAssertEqualObjects(onTimeValue, @(testOnTimeValue + 1));
}
}
}
};

__auto_type writeOnTimeValue = ^(uint16_t value) {
NSDictionary * writeValue = @{ MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(value) };
[device writeAttributeWithEndpointID:@(1)
clusterID:@(MTRClusterIDTypeOnOffID)
attributeID:@(MTRAttributeIDTypeClusterOnOffAttributeOnTimeID)
value:writeValue
expectedValueInterval:@(0)
timedWriteTimeout:nil];
};

writeOnTimeValue(testOnTimeValue + 1);
writeOnTimeValue(testOnTimeValue + 2);
writeOnTimeValue(testOnTimeValue + 3);
writeOnTimeValue(testOnTimeValue + 4);

[self waitForExpectations:@[ onTimeWriteSuccess ] timeout:10];
}

- (void)test900_SubscribeAllAttributes
{
MTRBaseDevice * device = GetConnectedDevice();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray<NSDictionary<NSString *
@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onEventDataReceived;
@property (nonatomic, nullable) dispatch_block_t onReportEnd;
@property (nonatomic, nullable) dispatch_block_t onDeviceCachePrimed;
@property (nonatomic) BOOL skipExpectedValuesForWrite;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@ - (void)deviceCachePrimed:(MTRDevice *)device
}
}

- (BOOL)unitTestShouldSkipExpectedValuesForWrite:(MTRDevice *)device
{
return self.skipExpectedValuesForWrite;
}
@end

0 comments on commit 8b86799

Please sign in to comment.