Skip to content

Commit 8c0085a

Browse files
committed
[Darwin] MTRDevice should throttle writes to the attribute storage
1 parent 37af39c commit 8c0085a

File tree

1 file changed

+201
-24
lines changed

1 file changed

+201
-24
lines changed

src/darwin/Framework/CHIP/MTRDevice.mm

+201-24
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,46 @@ @implementation MTRDevice {
419419
// Tracking of initial subscribe latency. When _initialSubscribeStart is
420420
// nil, we are not tracking the latency.
421421
NSDate * _Nullable _initialSubscribeStart;
422+
423+
//
424+
425+
// Each time a report comes in, MTRDevice will wait _reportToPersistDelayTime before persisting the
426+
// changes to storage. If another report comes in during this internal, MTRDevice will wait another
427+
// _reportToPersistDelayTime interval, until _reportToPersistDelayTimeMax is reached, at which
428+
// point all the changes so far will be written to storage.
429+
//
430+
// MTRDevice will also track a fixed number of report times. If the running average time between
431+
// reports dips below _timeBetweenReportsTooShortThreshold, a portion of the
432+
// _reportToPersistenceDalayMaxMultiplier will be applied to both the _reportToPersistenceDelayTime
433+
// and _reportToPersistenceDelayTimeMax. The multiplier will reach the max when the average time
434+
// between reports reach _timeBetweenReportsTooShortMinThreshold.
435+
//
436+
// When the running average time between reports dips below _timeBetweenReportsTooShortMinThreshold
437+
// for the first time, the time will be noted. If the device remains in this state for longer than
438+
// _deviceReportingTooFrequentlyThreshold, persistence will stop until the average time between
439+
// reports go back above _timeBetweenReportsTooShortMinThreshold.
440+
NSDate * _Nullable _clusterDataPersistenceFirstScheduledTime;
441+
NSMutableArray<NSDate *> * _mostRecentReportTimes;
442+
NSDate * _Nullable _deviceReportingExcessivelyStartTime;
443+
double _reportToPersistenceDalayCurrentMultiplier;
444+
445+
// Threshold values and limits for the above, so they can be tested
446+
NSTimeInterval _reportToPersistenceDelayTime;
447+
NSTimeInterval _reportToPersistenceDelayTimeMax;
448+
NSUInteger _recentReportTimesMaxCount;
449+
NSTimeInterval _timeBetweenReportsTooShortThreshold;
450+
NSTimeInterval _timeBetweenReportsTooShortMinThreshold;
451+
double _reportToPersistenceDalayMaxMultiplier;
452+
NSTimeInterval _deviceReportingExcessivelyIntervalThreshold;
453+
454+
// Default values for the thresholds and limits
455+
#define kReportToPersistenceDelayTimeDefault (15)
456+
#define kReportToPersistenceDelayTimeMaxDefault (20 * kReportToPersistenceDelayTimeDefault)
457+
#define kRecentReportTimesMaxCountDefault (12)
458+
#define kTimeBetweenReportsTooShortThresholdDefault (15)
459+
#define kTimeBetweenReportsTooShortMinThresholdDefault (5)
460+
#define kReportToPersistenceDelayMaxMultiplierDefault (10)
461+
#define kDeviceReportingExcessivelyIntervalThresholdDefault (5 * 60)
422462
}
423463

424464
- (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
@@ -442,6 +482,18 @@ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceControlle
442482
}
443483
_clusterDataToPersist = nil;
444484
_persistedClusters = [NSMutableSet set];
485+
486+
_clusterDataPersistenceFirstScheduledTime = nil;
487+
_mostRecentReportTimes = [NSMutableArray array];
488+
_deviceReportingExcessivelyStartTime = nil;
489+
_reportToPersistenceDalayCurrentMultiplier = 1;
490+
_reportToPersistenceDelayTime = kReportToPersistenceDelayTimeDefault;
491+
_reportToPersistenceDelayTimeMax = kReportToPersistenceDelayTimeMaxDefault;
492+
_recentReportTimesMaxCount = kRecentReportTimesMaxCountDefault;
493+
_timeBetweenReportsTooShortThreshold = kTimeBetweenReportsTooShortThresholdDefault;
494+
_timeBetweenReportsTooShortMinThreshold = kTimeBetweenReportsTooShortMinThresholdDefault;
495+
_reportToPersistenceDalayMaxMultiplier = kReportToPersistenceDelayMaxMultiplierDefault;
496+
_deviceReportingExcessivelyIntervalThreshold = kDeviceReportingExcessivelyIntervalThresholdDefault;
445497
MTR_LOG_INFO("%@ init with hex nodeID 0x%016llX", self, _nodeID.unsignedLongLongValue);
446498
}
447499
return self;
@@ -1287,37 +1339,162 @@ - (void)_handleReportBegin
12871339
return clusterDataToReturn;
12881340
}
12891341

1342+
- (NSTimeInterval)_reportToPersistenceDelayTimeAfterMutiplier
1343+
{
1344+
return _reportToPersistenceDelayTime * _reportToPersistenceDalayCurrentMultiplier;
1345+
}
1346+
1347+
- (NSTimeInterval)_reportToPersistenceDelayTimeMaxAfterMutiplier
1348+
{
1349+
return _reportToPersistenceDelayTimeMax * _reportToPersistenceDalayCurrentMultiplier;
1350+
}
1351+
1352+
- (void)_persistClusterDataAsNeeded
1353+
{
1354+
std::lock_guard lock(_lock);
1355+
1356+
// No persisted data / lack of controller data store
1357+
if (!_persistedClusterData) {
1358+
return;
1359+
}
1360+
1361+
// Nothing to persist
1362+
if (!_clusterDataToPersist.count) {
1363+
return;
1364+
}
1365+
1366+
NSDate * lastReportTime = [_mostRecentReportTimes lastObject];
1367+
NSTimeInterval intervalSinceLastReport = -[lastReportTime timeIntervalSinceNow];
1368+
if (intervalSinceLastReport < [self _reportToPersistenceDelayTimeAfterMutiplier]) {
1369+
// A report came in after this call was scheduled
1370+
1371+
if (!_clusterDataPersistenceFirstScheduledTime) {
1372+
MTR_LOG_ERROR("%@ _persistClusterDataAsNeeded expects _clusterDataPersistenceFirstScheduledTime if _clusterDataToPersist exists", self);
1373+
return;
1374+
}
1375+
1376+
NSTimeInterval intervalSinceFirstScheduledPersistence = -[_clusterDataPersistenceFirstScheduledTime timeIntervalSinceNow];
1377+
if (intervalSinceFirstScheduledPersistence < [self _reportToPersistenceDelayTimeMaxAfterMutiplier]) {
1378+
// The max delay is also not reached - do not persist yet
1379+
return;
1380+
}
1381+
}
1382+
1383+
// At this point, there is data to persist, and either _reportToPersistenceDelayTime was
1384+
// reached, or _reportToPersistenceDelayTimeMax was reached. Time to persist:
1385+
1386+
MTR_LOG_DEFAULT("%@ Storing cluster information (data version and attributes) count: %lu", self, static_cast<unsigned long>(_clusterDataToPersist.count));
1387+
// We're going to hand out these MTRDeviceClusterData objects to our
1388+
// storage implementation, which will try to read them later. Make sure
1389+
// we snapshot the state here instead of handing out live copies.
1390+
NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> * clusterData = [self _clusterDataToPersistSnapshot];
1391+
[_deviceController.controllerDataStore storeClusterData:clusterData forNodeID:_nodeID];
1392+
for (MTRClusterPath * clusterPath in _clusterDataToPersist) {
1393+
[_persistedClusterData setObject:_clusterDataToPersist[clusterPath] forKey:clusterPath];
1394+
[_persistedClusters addObject:clusterPath];
1395+
}
1396+
1397+
// TODO: There is one edge case not handled well here: if the
1398+
// storeClusterData call above fails somehow, and then the data gets
1399+
// evicted from _persistedClusterData, we could end up in a situation
1400+
// where when we page things in from storage we have stale values and
1401+
// hence effectively lose the delta that we failed to persist.
1402+
//
1403+
// The only way to handle this would be to detect it when it happens,
1404+
// then re-subscribe at that point, which would cause the relevant data
1405+
// to be sent to us via the priming read.
1406+
_clusterDataToPersist = nil;
1407+
1408+
_clusterDataPersistenceFirstScheduledTime = nil;
1409+
}
1410+
1411+
- (void)_scheduleClusterDataPersistence
1412+
{
1413+
os_unfair_lock_assert_owner(&self->_lock);
1414+
1415+
// No persisted data / lack of controller data store
1416+
if (!_persistedClusterData) {
1417+
return;
1418+
}
1419+
1420+
// Nothing to persist
1421+
if (!_clusterDataToPersist.count) {
1422+
return;
1423+
}
1424+
1425+
// Mark when first report comes in to know when _reportToPersistenceDelayTimeMax is hit
1426+
if (!_clusterDataPersistenceFirstScheduledTime) {
1427+
_clusterDataPersistenceFirstScheduledTime = [NSDate now];
1428+
}
1429+
1430+
// Make sure there is space in the array, and note report time
1431+
while (_mostRecentReportTimes.count >= _recentReportTimesMaxCount) {
1432+
[_mostRecentReportTimes removeObjectAtIndex:0];
1433+
}
1434+
[_mostRecentReportTimes addObject:[NSDate now]];
1435+
1436+
// Calculate running average and update multiplier - need at least 2 items to calculate intervals
1437+
if (_mostRecentReportTimes.count > 2) {
1438+
NSTimeInterval cumulativeIntervals = 0;
1439+
for (int i = 1; i < _mostRecentReportTimes.count; i++) {
1440+
NSDate * lastDate = [_mostRecentReportTimes objectAtIndex:i - 1];
1441+
NSDate * currentDate = [_mostRecentReportTimes objectAtIndex:i];
1442+
NSTimeInterval intervalSinceLastReport = [currentDate timeIntervalSinceDate:lastDate];
1443+
// Check to guard against clock change
1444+
if (intervalSinceLastReport > 0) {
1445+
cumulativeIntervals += intervalSinceLastReport;
1446+
}
1447+
}
1448+
NSTimeInterval averageTimeBetweenReports = cumulativeIntervals / (_mostRecentReportTimes.count - 1);
1449+
1450+
if (averageTimeBetweenReports < _timeBetweenReportsTooShortThreshold) {
1451+
// Multiplier goes from 1 to _reportToPersistenceDalayMaxMultiplier uniformly, as
1452+
// averageTimeBetweenReports go from _timeBetweenReportsTooShortThreshold to
1453+
// _timeBetweenReportsTooShortMinThreshold
1454+
1455+
double intervalAmountBelowThreshold = _timeBetweenReportsTooShortThreshold - averageTimeBetweenReports;
1456+
double proportionTowardMinThreshold = intervalAmountBelowThreshold / (_timeBetweenReportsTooShortThreshold - _timeBetweenReportsTooShortMinThreshold);
1457+
if (proportionTowardMinThreshold > 1) {
1458+
// Clamp to 100%
1459+
proportionTowardMinThreshold = 1;
1460+
}
1461+
1462+
// Set current multiplier to [1, MaxMultiplier]
1463+
_reportToPersistenceDalayCurrentMultiplier = 1 + (proportionTowardMinThreshold * (_reportToPersistenceDalayMaxMultiplier - 1));
1464+
} else {
1465+
_reportToPersistenceDalayCurrentMultiplier = 1;
1466+
}
1467+
1468+
// Also note when the running average first dips below the min threshold
1469+
if (averageTimeBetweenReports < _timeBetweenReportsTooShortMinThreshold) {
1470+
if (!_deviceReportingExcessivelyStartTime) {
1471+
_deviceReportingExcessivelyStartTime = [NSDate now];
1472+
MTR_LOG_DEFAULT("Device is ");
1473+
}
1474+
} else {
1475+
_deviceReportingExcessivelyStartTime = nil;
1476+
}
1477+
}
1478+
1479+
// Do not schedule persistence if device is reporting excessively
1480+
NSTimeInterval intervalSinceDeviceReportingExcessively = -[_deviceReportingExcessivelyStartTime timeIntervalSinceNow];
1481+
if (intervalSinceDeviceReportingExcessively > _deviceReportingExcessivelyIntervalThreshold) {
1482+
return;
1483+
}
1484+
1485+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) ([self _reportToPersistenceDelayTimeAfterMutiplier] * NSEC_PER_SEC)), self.queue, ^{
1486+
[self _persistClusterDataAsNeeded];
1487+
});
1488+
}
1489+
12901490
- (void)_handleReportEnd
12911491
{
12921492
std::lock_guard lock(_lock);
12931493
_receivingReport = NO;
12941494
_receivingPrimingReport = NO;
12951495
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
12961496

1297-
BOOL dataStoreExists = _deviceController.controllerDataStore != nil;
1298-
if (dataStoreExists && _clusterDataToPersist != nil && _clusterDataToPersist.count) {
1299-
MTR_LOG_DEFAULT("%@ Storing cluster information (data version and attributes) count: %lu", self, static_cast<unsigned long>(_clusterDataToPersist.count));
1300-
// We're going to hand out these MTRDeviceClusterData objects to our
1301-
// storage implementation, which will try to read them later. Make sure
1302-
// we snapshot the state here instead of handing out live copies.
1303-
NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> * clusterData = [self _clusterDataToPersistSnapshot];
1304-
[_deviceController.controllerDataStore storeClusterData:clusterData forNodeID:_nodeID];
1305-
for (MTRClusterPath * clusterPath in _clusterDataToPersist) {
1306-
[_persistedClusterData setObject:_clusterDataToPersist[clusterPath] forKey:clusterPath];
1307-
[_persistedClusters addObject:clusterPath];
1308-
}
1309-
1310-
// TODO: There is one edge case not handled well here: if the
1311-
// storeClusterData call above fails somehow, and then the data gets
1312-
// evicted from _persistedClusterData, we could end up in a situation
1313-
// where when we page things in from storage we have stale values and
1314-
// hence effectively lose the delta that we failed to persist.
1315-
//
1316-
// The only way to handle this would be to detect it when it happens,
1317-
// then re-subscribe at that point, which would cause the relevant data
1318-
// to be sent to us via the priming read.
1319-
_clusterDataToPersist = nil;
1320-
}
1497+
[self _scheduleClusterDataPersistence];
13211498

13221499
// After the handling of the report, if we detected a device configuration change, notify the delegate
13231500
// of the same.

0 commit comments

Comments
 (0)