@@ -419,6 +419,46 @@ @implementation MTRDevice {
419
419
// Tracking of initial subscribe latency. When _initialSubscribeStart is
420
420
// nil, we are not tracking the latency.
421
421
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 )
422
462
}
423
463
424
464
- (instancetype )initWithNodeID : (NSNumber *)nodeID controller : (MTRDeviceController *)controller
@@ -442,6 +482,18 @@ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceControlle
442
482
}
443
483
_clusterDataToPersist = nil ;
444
484
_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 ;
445
497
MTR_LOG_INFO (" %@ init with hex nodeID 0x%016llX" , self, _nodeID.unsignedLongLongValue );
446
498
}
447
499
return self;
@@ -1287,37 +1339,162 @@ - (void)_handleReportBegin
1287
1339
return clusterDataToReturn;
1288
1340
}
1289
1341
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
+
1290
1490
- (void )_handleReportEnd
1291
1491
{
1292
1492
std::lock_guard lock (_lock);
1293
1493
_receivingReport = NO ;
1294
1494
_receivingPrimingReport = NO ;
1295
1495
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil ;
1296
1496
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 ];
1321
1498
1322
1499
// After the handling of the report, if we detected a device configuration change, notify the delegate
1323
1500
// of the same.
0 commit comments