From 0dcc530677e5a87c5a5d99852dcc41d520a9a34f Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:17:12 -0400 Subject: [PATCH] [ReadHandler] Synchronized report scheduler (#27943) * Added a Synchronized ReportScheduler along with test to confirm the behavior on the scheduler with up to 4 ReadHandlers as well logging mechanism to find out what handler fires at what time. * Added a quick fix because TestDecoding won't compile otherwise, this doesn't belong here * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Refactored ReportScheduler Impls to better take advantage of inheritance, removed bloat, excluded test for platform in which problems are caused due to unprocessed engine runs * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Applied comment review and refactoed next timeout calculation logic * Completed unit test and logic * Passing a ReportSchedulerPointer instead of an std::function to avoid dynamical memory allocation * undid ReadHandler changes * Update src/app/reporting/ReportScheduler.h Co-authored-by: Boris Zbarsky * Removed un-necessary nullptr check, addressed comments regarding tests and added doc on unclear behavior * Addressed redundant test --------- Co-authored-by: Boris Zbarsky --- src/app/BUILD.gn | 2 + src/app/reporting/ReportScheduler.h | 54 ++- src/app/reporting/ReportSchedulerImpl.cpp | 141 +++--- src/app/reporting/ReportSchedulerImpl.h | 29 +- .../SynchronizedReportSchedulerImpl.cpp | 196 ++++++++ .../SynchronizedReportSchedulerImpl.h | 65 +++ src/app/tests/BUILD.gn | 11 +- src/app/tests/TestReportScheduler.cpp | 454 +++++++++++++++++- 8 files changed, 814 insertions(+), 138 deletions(-) create mode 100644 src/app/reporting/SynchronizedReportSchedulerImpl.cpp create mode 100644 src/app/reporting/SynchronizedReportSchedulerImpl.h diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index 487c43e3182a5b..7686195cb7b956 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -196,6 +196,8 @@ static_library("app") { "reporting/ReportScheduler.h", "reporting/ReportSchedulerImpl.cpp", "reporting/ReportSchedulerImpl.h", + "reporting/SynchronizedReportSchedulerImpl.cpp", + "reporting/SynchronizedReportSchedulerImpl.h", "reporting/reporting.h", ] diff --git a/src/app/reporting/ReportScheduler.h b/src/app/reporting/ReportScheduler.h index 80d391c171c7e5..0449ba3114f857 100644 --- a/src/app/reporting/ReportScheduler.h +++ b/src/app/reporting/ReportScheduler.h @@ -56,14 +56,12 @@ class ReportScheduler : public ReadHandler::Observer class ReadHandlerNode : public IntrusiveListNodeBase<> { public: - using TimerCompleteCallback = void (*)(); - - ReadHandlerNode(ReadHandler * aReadHandler, TimerDelegate * aTimerDelegate, TimerCompleteCallback aCallback) : - mTimerDelegate(aTimerDelegate), mCallback(aCallback) + ReadHandlerNode(ReadHandler * aReadHandler, TimerDelegate * aTimerDelegate, ReportScheduler * aScheduler) : + mTimerDelegate(aTimerDelegate), mScheduler(aScheduler) { VerifyOrDie(aReadHandler != nullptr); VerifyOrDie(aTimerDelegate != nullptr); - VerifyOrDie(aCallback != nullptr); + VerifyOrDie(aScheduler != nullptr); mReadHandler = aReadHandler; SetIntervalTimeStamps(aReadHandler); @@ -78,29 +76,50 @@ class ReportScheduler : public ReadHandler::Observer // the scheduler in the ReadHandler Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); return (mReadHandler->IsGeneratingReports() && - ((now >= mMinTimestamp && mReadHandler->IsDirty()) || now >= mMaxTimestamp)); + (now >= mMinTimestamp && (mReadHandler->IsDirty() || now >= mMaxTimestamp || now >= mSyncTimestamp))); } + bool IsEngineRunScheduled() const { return mEngineRunScheduled; } + void SetEngineRunScheduled(bool aEnginRunScheduled) { mEngineRunScheduled = aEnginRunScheduled; } + void SetIntervalTimeStamps(ReadHandler * aReadHandler) { uint16_t minInterval, maxInterval; aReadHandler->GetReportingIntervals(minInterval, maxInterval); - Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); - mMinTimestamp = now + System::Clock::Seconds16(minInterval); - mMaxTimestamp = now + System::Clock::Seconds16(maxInterval); + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + mMinTimestamp = now + System::Clock::Seconds16(minInterval); + mMaxTimestamp = now + System::Clock::Seconds16(maxInterval); + mSyncTimestamp = mMaxTimestamp; + } + + void RunCallback() + { + mScheduler->ReportTimerCallback(); + SetEngineRunScheduled(true); } - void RunCallback() { mCallback(); } + void SetSyncTimestamp(System::Clock::Timestamp aSyncTimestamp) + { + // Prevents the sync timestamp being set to a value lower than the min timestamp to prevent it to appear as reportable + // on the next timeout calculation and cause the scheduler to run the engine too early + VerifyOrReturn(aSyncTimestamp >= mMinTimestamp); + mSyncTimestamp = aSyncTimestamp; + } - Timestamp GetMinTimestamp() const { return mMinTimestamp; } - Timestamp GetMaxTimestamp() const { return mMaxTimestamp; } + System::Clock::Timestamp GetMinTimestamp() const { return mMinTimestamp; } + System::Clock::Timestamp GetMaxTimestamp() const { return mMaxTimestamp; } + System::Clock::Timestamp GetSyncTimestamp() const { return mSyncTimestamp; } private: TimerDelegate * mTimerDelegate; - TimerCompleteCallback mCallback; ReadHandler * mReadHandler; + ReportScheduler * mScheduler; Timestamp mMinTimestamp; Timestamp mMaxTimestamp; + Timestamp mSyncTimestamp; // Timestamp at which the read handler will be allowed to emit a report so it can be synced with + // other handlers that have an earlier max timestamp + bool mEngineRunScheduled = false; // Flag to indicate if the engine run is already scheduled so the scheduler can ignore + // it when calculating the next run time }; ReportScheduler(TimerDelegate * aTimerDelegate) : mTimerDelegate(aTimerDelegate) {} @@ -109,11 +128,14 @@ class ReportScheduler : public ReadHandler::Observer */ virtual ~ReportScheduler() = default; - /// @brief Check if a ReadHandler is scheduled for reporting - virtual bool IsReportScheduled(ReadHandler * aReadHandler) = 0; + virtual void ReportTimerCallback() = 0; + /// @brief Check whether a ReadHandler is reportable right now, taking into account its minimum and maximum intervals. /// @param aReadHandler read handler to check - bool IsReportableNow(ReadHandler * aReadHandler) { return FindReadHandlerNode(aReadHandler)->IsReportableNow(); }; + bool IsReportableNow(ReadHandler * aReadHandler) + { + return FindReadHandlerNode(aReadHandler)->IsReportableNow(); + } // TODO: Change the IsReportableNow to IsReportable() for readHandlers /// @brief Check if a ReadHandler is reportable without considering the timing bool IsReadHandlerReportable(ReadHandler * aReadHandler) const { diff --git a/src/app/reporting/ReportSchedulerImpl.cpp b/src/app/reporting/ReportSchedulerImpl.cpp index 4b45ab9d1b6deb..b49884aa3f699f 100644 --- a/src/app/reporting/ReportSchedulerImpl.cpp +++ b/src/app/reporting/ReportSchedulerImpl.cpp @@ -22,15 +22,12 @@ namespace chip { namespace app { namespace reporting { -using Seconds16 = System::Clock::Seconds16; -using Milliseconds32 = System::Clock::Milliseconds32; -using Timeout = System::Clock::Timeout; -using Timestamp = System::Clock::Timestamp; +using namespace System::Clock; using ReadHandlerNode = ReportScheduler::ReadHandlerNode; /// @brief Callback called when the report timer expires to schedule an engine run regardless of the state of the ReadHandlers, as /// the engine already verifies that read handlers are reportable before sending a report -static void ReportTimerCallback() +void ReportSchedulerImpl::ReportTimerCallback() { InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun(); } @@ -43,7 +40,23 @@ ReportSchedulerImpl::ReportSchedulerImpl(TimerDelegate * aTimerDelegate) : Repor /// @brief When a ReadHandler is added, register it, which will schedule an engine run void ReportSchedulerImpl::OnReadHandlerCreated(ReadHandler * aReadHandler) { - RegisterReadHandler(aReadHandler); + ReadHandlerNode * newNode = FindReadHandlerNode(aReadHandler); + // Handler must not be registered yet; it's just being constructed. + VerifyOrDie(nullptr == newNode); + // The NodePool is the same size as the ReadHandler pool from the IM Engine, so we don't need a check for size here since if a + // ReadHandler was created, space should be available. + newNode = mNodesPool.CreateObject(aReadHandler, mTimerDelegate, this); + mReadHandlerList.PushBack(newNode); + + ChipLogProgress(DataManagement, + "Registered a ReadHandler that will schedule a report between system Timestamp: %" PRIu64 + " and system Timestamp %" PRIu64 ".", + newNode->GetMinTimestamp().count(), newNode->GetMaxTimestamp().count()); + + Milliseconds32 newTimeout; + // No need to check for error here, since the node is already in the list otherwise we would have Died + CalculateNextReportTimeout(newTimeout, newNode); + ScheduleReport(newTimeout, newNode); } /// @brief When a ReadHandler becomes reportable, schedule, verifies if the min interval of a handleris elapsed. If not, @@ -54,17 +67,7 @@ void ReportSchedulerImpl::OnBecameReportable(ReadHandler * aReadHandler) VerifyOrReturn(nullptr != node); Milliseconds32 newTimeout; - if (node->IsReportableNow()) - { - // If the handler is reportable now, just schedule a report immediately - newTimeout = Milliseconds32(0); - } - else - { - // If the handler is not reportable now, schedule a report for the min interval - newTimeout = node->GetMinTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp(); - } - + CalculateNextReportTimeout(newTimeout, node); ScheduleReport(newTimeout, node); } @@ -72,62 +75,31 @@ void ReportSchedulerImpl::OnSubscriptionAction(ReadHandler * apReadHandler) { ReadHandlerNode * node = FindReadHandlerNode(apReadHandler); VerifyOrReturn(nullptr != node); - // Schedule callback for max interval by computing the difference between the max timestamp and the current timestamp node->SetIntervalTimeStamps(apReadHandler); - Milliseconds32 newTimeout = node->GetMaxTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp(); + Milliseconds32 newTimeout; + CalculateNextReportTimeout(newTimeout, node); ScheduleReport(newTimeout, node); + node->SetEngineRunScheduled(false); } /// @brief When a ReadHandler is removed, unregister it, which will cancel any scheduled report void ReportSchedulerImpl::OnReadHandlerDestroyed(ReadHandler * aReadHandler) { - UnregisterReadHandler(aReadHandler); -} - -CHIP_ERROR ReportSchedulerImpl::RegisterReadHandler(ReadHandler * aReadHandler) -{ - ReadHandlerNode * newNode = FindReadHandlerNode(aReadHandler); - // Handler must not be registered yet; it's just being constructed. - VerifyOrDie(nullptr == newNode); - // The NodePool is the same size as the ReadHandler pool from the IM Engine, so we don't need a check for size here since if a - // ReadHandler was created, space should be available. - newNode = mNodesPool.CreateObject(aReadHandler, mTimerDelegate, ReportTimerCallback); - mReadHandlerList.PushBack(newNode); - - ChipLogProgress(DataManagement, - "Registered a ReadHandler that will schedule a report between system Timestamp: %" PRIu64 - " and system Timestamp %" PRIu64 ".", - newNode->GetMinTimestamp().count(), newNode->GetMaxTimestamp().count()); + CancelReport(aReadHandler); - Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); - Milliseconds32 newTimeout; - // If the handler is reportable, schedule a report for the min interval, otherwise schedule a report for the max interval - if (newNode->IsReportableNow()) - { - // If the handler is reportable now, just schedule a report immediately - newTimeout = Milliseconds32(0); - } - else if (IsReadHandlerReportable(aReadHandler) && (newNode->GetMinTimestamp() > now)) - { - // If the handler is reportable now, but the min interval is not elapsed, schedule a report for the moment the min interval - // has elapsed - newTimeout = newNode->GetMinTimestamp() - now; - } - else - { - // If the handler is not reportable now, schedule a report for the max interval - newTimeout = newNode->GetMaxTimestamp() - now; - } + ReadHandlerNode * removeNode = FindReadHandlerNode(aReadHandler); + // Nothing to remove if the handler is not found in the list + VerifyOrReturn(nullptr != removeNode); - ReturnErrorOnFailure(ScheduleReport(newTimeout, newNode)); - return CHIP_NO_ERROR; + mReadHandlerList.Remove(removeNode); + mNodesPool.ReleaseObject(removeNode); } CHIP_ERROR ReportSchedulerImpl::ScheduleReport(Timeout timeout, ReadHandlerNode * node) { // Cancel Report if it is currently scheduled - CancelSchedulerTimer(node); - StartSchedulerTimer(node, timeout); + mTimerDelegate->CancelTimer(node); + ReturnErrorOnFailure(mTimerDelegate->StartTimer(node, timeout)); return CHIP_NO_ERROR; } @@ -136,19 +108,7 @@ void ReportSchedulerImpl::CancelReport(ReadHandler * aReadHandler) { ReadHandlerNode * node = FindReadHandlerNode(aReadHandler); VerifyOrReturn(nullptr != node); - CancelSchedulerTimer(node); -} - -void ReportSchedulerImpl::UnregisterReadHandler(ReadHandler * aReadHandler) -{ - CancelReport(aReadHandler); - - ReadHandlerNode * removeNode = FindReadHandlerNode(aReadHandler); - // Nothing to remove if the handler is not found in the list - VerifyOrReturn(nullptr != removeNode); - - mReadHandlerList.Remove(removeNode); - mNodesPool.ReleaseObject(removeNode); + mTimerDelegate->CancelTimer(node); } void ReportSchedulerImpl::UnregisterAllHandlers() @@ -156,7 +116,7 @@ void ReportSchedulerImpl::UnregisterAllHandlers() while (!mReadHandlerList.Empty()) { ReadHandler * firstReadHandler = mReadHandlerList.begin()->GetReadHandler(); - UnregisterReadHandler(firstReadHandler); + OnReadHandlerDestroyed(firstReadHandler); } } @@ -164,23 +124,32 @@ bool ReportSchedulerImpl::IsReportScheduled(ReadHandler * aReadHandler) { ReadHandlerNode * node = FindReadHandlerNode(aReadHandler); VerifyOrReturnValue(nullptr != node, false); - return CheckSchedulerTimerActive(node); -} - -CHIP_ERROR ReportSchedulerImpl::StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout) -{ - // Schedule Report - return mTimerDelegate->StartTimer(node, aTimeout); + return mTimerDelegate->IsTimerActive(node); } -void ReportSchedulerImpl::CancelSchedulerTimer(ReadHandlerNode * node) +CHIP_ERROR ReportSchedulerImpl::CalculateNextReportTimeout(Timeout & timeout, ReadHandlerNode * aNode) { - mTimerDelegate->CancelTimer(node); -} + VerifyOrReturnError(mReadHandlerList.Contains(aNode), CHIP_ERROR_INVALID_ARGUMENT); + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); -bool ReportSchedulerImpl::CheckSchedulerTimerActive(ReadHandlerNode * node) -{ - return mTimerDelegate->IsTimerActive(node); + // If the handler is reportable now, just schedule a report immediately + if (aNode->IsReportableNow()) + { + // If the handler is reportable now, just schedule a report immediately + timeout = Milliseconds32(0); + } + else if (IsReadHandlerReportable(aNode->GetReadHandler()) && (aNode->GetMinTimestamp() > now)) + { + // If the handler is reportable now, but the min interval is not elapsed, schedule a report for the moment the min interval + // has elapsed + timeout = aNode->GetMinTimestamp() - now; + } + else + { + // If the handler is not reportable now, schedule a report for the max interval + timeout = aNode->GetMaxTimestamp() - now; + } + return CHIP_NO_ERROR; } } // namespace reporting diff --git a/src/app/reporting/ReportSchedulerImpl.h b/src/app/reporting/ReportSchedulerImpl.h index 849f9b797b5f93..3ac47449f834bc 100644 --- a/src/app/reporting/ReportSchedulerImpl.h +++ b/src/app/reporting/ReportSchedulerImpl.h @@ -27,35 +27,30 @@ namespace reporting { class ReportSchedulerImpl : public ReportScheduler { public: + using Timeout = System::Clock::Timeout; + ReportSchedulerImpl(TimerDelegate * aTimerDelegate); ~ReportSchedulerImpl() override { UnregisterAllHandlers(); } // ReadHandlerObserver - void OnReadHandlerCreated(ReadHandler * aReadHandler) override; - void OnBecameReportable(ReadHandler * aReadHandler) override; - void OnSubscriptionAction(ReadHandler * aReadHandler) override; + void OnReadHandlerCreated(ReadHandler * aReadHandler) final; + void OnBecameReportable(ReadHandler * aReadHandler) final; + void OnSubscriptionAction(ReadHandler * aReadHandler) final; void OnReadHandlerDestroyed(ReadHandler * aReadHandler) override; + bool IsReportScheduled(ReadHandler * aReadHandler); + + void ReportTimerCallback() override; + protected: - virtual CHIP_ERROR RegisterReadHandler(ReadHandler * aReadHandler); - virtual CHIP_ERROR ScheduleReport(System::Clock::Timeout timeout, ReadHandlerNode * node); - virtual void CancelReport(ReadHandler * aReadHandler); - virtual void UnregisterReadHandler(ReadHandler * aReadHandler); + virtual CHIP_ERROR ScheduleReport(Timeout timeout, ReadHandlerNode * node); + void CancelReport(ReadHandler * aReadHandler); virtual void UnregisterAllHandlers(); private: friend class chip::app::reporting::TestReportScheduler; - bool IsReportScheduled(ReadHandler * aReadHandler) override; - - /// @brief Start a timer for a given ReadHandlerNode, ensures that if a timer is already running for this node, it is cancelled - /// @param node Node of the ReadHandler list to start a timer for - /// @param aTimeout Delay before the timer expires - virtual CHIP_ERROR StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout); - /// @brief Cancel the timer for a given ReadHandlerNode - virtual void CancelSchedulerTimer(ReadHandlerNode * node); - /// @brief Check if the timer for a given ReadHandlerNode is active - virtual bool CheckSchedulerTimerActive(ReadHandlerNode * node); + virtual CHIP_ERROR CalculateNextReportTimeout(Timeout & timeout, ReadHandlerNode * aNode); }; } // namespace reporting diff --git a/src/app/reporting/SynchronizedReportSchedulerImpl.cpp b/src/app/reporting/SynchronizedReportSchedulerImpl.cpp new file mode 100644 index 00000000000000..16713e37e05741 --- /dev/null +++ b/src/app/reporting/SynchronizedReportSchedulerImpl.cpp @@ -0,0 +1,196 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +namespace chip { +namespace app { +namespace reporting { + +using namespace System::Clock; +using ReadHandlerNode = ReportScheduler::ReadHandlerNode; + +void SynchronizedReportSchedulerImpl::OnReadHandlerDestroyed(ReadHandler * aReadHandler) +{ + // Verify list is populated + VerifyOrReturn((!mReadHandlerList.Empty())); + + ReadHandlerNode * removeNode = FindReadHandlerNode(aReadHandler); + // Nothing to remove if the handler is not found in the list + VerifyOrReturn(nullptr != removeNode); + + mReadHandlerList.Remove(removeNode); + mNodesPool.ReleaseObject(removeNode); + + if (mReadHandlerList.Empty()) + { + // Only cancel the timer if there are no more handlers registered + CancelReport(); + } +} + +CHIP_ERROR SynchronizedReportSchedulerImpl::ScheduleReport(Timeout timeout, ReadHandlerNode * node) +{ + // Cancel Report if it is currently scheduled + mTimerDelegate->CancelTimer(this); + ReturnErrorOnFailure(mTimerDelegate->StartTimer(this, timeout)); + mTestNextReportTimestamp = mTimerDelegate->GetCurrentMonotonicTimestamp() + timeout; + + return CHIP_NO_ERROR; +} + +void SynchronizedReportSchedulerImpl::CancelReport() +{ + // We don't need to take action on the handler, since the timer is common here + mTimerDelegate->CancelTimer(this); +} + +/// @brief Checks if the timer is active for the given ReadHandler. Since all read handlers are scheduled on the same timer, we +/// check if the node is in the list and if the timer is active for the ReportScheduler +bool SynchronizedReportSchedulerImpl::IsReportScheduled() +{ + return mTimerDelegate->IsTimerActive(this); +} + +/// @brief Find the smallest maximum interval possible and set it as the common maximum +/// @return NO_ERROR if the smallest maximum interval was found, error otherwise, INVALID LIST LENGTH if the list is empty +CHIP_ERROR SynchronizedReportSchedulerImpl::FindNextMaxInterval() +{ + VerifyOrReturnError(!mReadHandlerList.Empty(), CHIP_ERROR_INVALID_LIST_LENGTH); + System::Clock::Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + System::Clock::Timestamp earliest = now + Seconds16::max(); + + for (auto & iter : mReadHandlerList) + { + if (iter.GetMaxTimestamp() < earliest && iter.GetMaxTimestamp() > now) + { + earliest = iter.GetMaxTimestamp(); + } + } + + mNextMaxTimestamp = earliest; + + return CHIP_NO_ERROR; +} + +/// @brief Find the highest minimum timestamp possible that still respects the lowest max timestamp and sets it as the common +/// minimum. If the max timestamp has not been updated and is in the past, or if no min timestamp is lower than the current max +/// timestamp, this will set now as the common minimum timestamp, thus allowing the report to be sent immediately. +/// @return NO_ERROR if the highest minimum timestamp was found, error otherwise, INVALID LIST LENGTH if the list is empty +CHIP_ERROR SynchronizedReportSchedulerImpl::FindNextMinInterval() +{ + VerifyOrReturnError(!mReadHandlerList.Empty(), CHIP_ERROR_INVALID_LIST_LENGTH); + System::Clock::Timestamp latest = mTimerDelegate->GetCurrentMonotonicTimestamp(); + + for (auto & iter : mReadHandlerList) + { + if (iter.GetMinTimestamp() > latest && IsReadHandlerReportable(iter.GetReadHandler())) + { + // We do not want the new min to be set above the max for any handler + if (iter.GetMinTimestamp() <= mNextMaxTimestamp) + { + latest = iter.GetMinTimestamp(); + } + } + } + + mNextMinTimestamp = latest; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR SynchronizedReportSchedulerImpl::CalculateNextReportTimeout(Timeout & timeout, ReadHandlerNode * aNode) +{ + VerifyOrReturnError(mReadHandlerList.Contains(aNode), CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorOnFailure(FindNextMaxInterval()); + ReturnErrorOnFailure(FindNextMinInterval()); + bool reportableNow = false; + bool reportableAtMin = false; + + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + + for (auto & iter : mReadHandlerList) + { + if (!iter.IsEngineRunScheduled()) + { + if (iter.IsReportableNow()) + { + reportableNow = true; + break; + } + + if (IsReadHandlerReportable(iter.GetReadHandler()) && iter.GetMinTimestamp() <= mNextMaxTimestamp) + { + reportableAtMin = true; + } + } + } + + // Find out if any handler is reportable now + + if (reportableNow) + { + timeout = Milliseconds32(0); + } + else if (reportableAtMin) + { + timeout = mNextMinTimestamp - now; + } + else + { + // Schedule report at next max otherwise + timeout = mNextMaxTimestamp - now; + } + + // Updates the synching time of each handler + for (auto & iter : mReadHandlerList) + { + // Prevent modifying the sync if the handler is currently reportable, sync's purpose is to allow handler to become + // reportable earlier than their max interval + if (!iter.IsReportableNow()) + { + iter.SetSyncTimestamp(Milliseconds64(now + timeout)); + } + } + + return CHIP_NO_ERROR; +} + +/// @brief Callback called when the report timer expires to schedule an engine run regardless of the state of the ReadHandlers, as +/// the engine already verifies that read handlers are reportable before sending a report +void SynchronizedReportSchedulerImpl::ReportTimerCallback() +{ + ReportSchedulerImpl::ReportTimerCallback(); + + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + ChipLogProgress(DataManagement, "Engine run at time: %" PRIu64 " for Handlers:", now.count()); + for (auto & iter : mReadHandlerList) + { + if (iter.IsReportableNow()) + { + iter.SetEngineRunScheduled(true); + ChipLogProgress(DataManagement, "Handler: %p with min: %" PRIu64 " and max: %" PRIu64 " and sync: %" PRIu64, (&iter), + iter.GetMinTimestamp().count(), iter.GetMaxTimestamp().count(), iter.GetSyncTimestamp().count()); + } + } +} + +} // namespace reporting +} // namespace app +} // namespace chip diff --git a/src/app/reporting/SynchronizedReportSchedulerImpl.h b/src/app/reporting/SynchronizedReportSchedulerImpl.h new file mode 100644 index 00000000000000..18ee69520e5651 --- /dev/null +++ b/src/app/reporting/SynchronizedReportSchedulerImpl.h @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace chip { +namespace app { +namespace reporting { + +using Timeout = System::Clock::Timeout; +using Timestamp = System::Clock::Timestamp; +using Milliseconds64 = System::Clock::Milliseconds64; +using ReadHandlerNode = ReportScheduler::ReadHandlerNode; +using TimerDelegate = ReportScheduler::TimerDelegate; + +class SynchronizedReportSchedulerImpl : public ReportSchedulerImpl +{ +public: + void OnReadHandlerDestroyed(ReadHandler * aReadHandler) override; + + SynchronizedReportSchedulerImpl(TimerDelegate * aTimerDelegate) : ReportSchedulerImpl(aTimerDelegate) {} + ~SynchronizedReportSchedulerImpl() {} + + bool IsReportScheduled(); + + void ReportTimerCallback() override; + +protected: + CHIP_ERROR ScheduleReport(System::Clock::Timeout timeout, ReadHandlerNode * node) override; + void CancelReport(); + +private: + friend class chip::app::reporting::TestReportScheduler; + + CHIP_ERROR FindNextMinInterval(); + CHIP_ERROR FindNextMaxInterval(); + CHIP_ERROR CalculateNextReportTimeout(Timeout & timeout, ReadHandlerNode * aReadHandlerNode) override; + + Timestamp mNextMaxTimestamp = Milliseconds64(0); + Timestamp mNextMinTimestamp = Milliseconds64(0); + + // Timestamp of the next report to be scheduled, only used for testing + Timestamp mTestNextReportTimestamp = Milliseconds64(0); +}; + +} // namespace reporting +} // namespace app +} // namespace chip diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index f4267b14bd802e..e5f39d95ab10dd 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -132,7 +132,6 @@ chip_test_suite("tests") { "TestOperationalStateDelegate.cpp", "TestPendingNotificationMap.cpp", "TestReadInteraction.cpp", - "TestReportScheduler.cpp", "TestReportingEngine.cpp", "TestSceneTable.cpp", "TestStatusIB.cpp", @@ -166,6 +165,16 @@ chip_test_suite("tests") { test_sources += [ "TestEventLogging.cpp" ] } + # The platform manager is not properly clearing queues in test teardown, which results in + # DrainIO calls not being able to run in expected time (5seconds) if unprocessed reported engine + # runs are remaining, causing tests to crash in Open IoT SDK and Zephyr tests since they are + # running all tests in one file. We need to figure out how to properly clean the event queues + # before enabling this test for these platforms. + if (chip_device_platform != "nrfconnect" && + chip_device_platform != "openiotsdk") { + test_sources += [ "TestReportScheduler.cpp" ] + } + cflags = [ "-Wconversion" ] public_deps = [ diff --git a/src/app/tests/TestReportScheduler.cpp b/src/app/tests/TestReportScheduler.cpp index a24422010c47c9..c789865bcc50b2 100644 --- a/src/app/tests/TestReportScheduler.cpp +++ b/src/app/tests/TestReportScheduler.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -71,11 +72,10 @@ namespace chip { namespace app { namespace reporting { -using InteractionModelEngine = InteractionModelEngine; -using ReportScheduler = reporting::ReportScheduler; -using ReportSchedulerImpl = reporting::ReportSchedulerImpl; -using ReadHandlerNode = reporting::ReportScheduler::ReadHandlerNode; -using Milliseconds64 = System::Clock::Milliseconds64; +using ReportScheduler = reporting::ReportScheduler; +using ReportSchedulerImpl = reporting::ReportSchedulerImpl; +using ReadHandlerNode = reporting::ReportScheduler::ReadHandlerNode; +using Milliseconds64 = System::Clock::Milliseconds64; static const size_t kNumMaxReadHandlers = 16; @@ -160,7 +160,8 @@ class TestTimerDelegate : public ReportScheduler::TimerDelegate void SetMockSystemTimestamp(System::Clock::Timestamp aMockTimestamp) { mMockSystemTimestamp = aMockTimestamp; } - // Increment the mock timestamp one milisecond at a time for a total of aTime miliseconds. Checks if + // Increment the mock timestamp by aTime and call callbacks for timers that have expired. Checks if the timeout expired after + // incrementing void IncrementMockTimestamp(System::Clock::Milliseconds64 aTime) { mMockSystemTimestamp = mMockSystemTimestamp + aTime; @@ -174,9 +175,78 @@ class TestTimerDelegate : public ReportScheduler::TimerDelegate } }; +/// @brief TestTimerSynchronizedDelegate is a mock of the TimerDelegate interface that allows to control the time without dependency +/// on the system layer. This also simulates the system timer by verifying if the timeout expired when incrementing the mock +/// timestamp. only one timer can be active at a time, which is the one has the earliest timeout. +/// It is used to test the SynchronizedReportSchedulerImpl. +class TestTimerSynchronizedDelegate : public ReportScheduler::TimerDelegate +{ +public: + static void TimerCallbackInterface(System::Layer * aLayer, void * aAppState) + { + SynchronizedReportSchedulerImpl * scheduler = static_cast(aAppState); + scheduler->ReportTimerCallback(); + } + virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) override + { + SynchronizedReportSchedulerImpl * scheduler = static_cast(context); + if (nullptr == scheduler) + { + return CHIP_ERROR_INCORRECT_STATE; + } + + mSyncScheduler = scheduler; + mTimerTimeout = mMockSystemTimestamp + aTimeout; + return CHIP_NO_ERROR; + } + virtual void CancelTimer(void * context) override + { + VerifyOrReturn(nullptr != mSyncScheduler); + mSyncScheduler = nullptr; + mTimerTimeout = System::Clock::Milliseconds64(0x7FFFFFFFFFFFFFFF); + } + virtual bool IsTimerActive(void * context) override + { + return (nullptr != mSyncScheduler) && (mTimerTimeout > mMockSystemTimestamp); + } + + virtual System::Clock::Timestamp GetCurrentMonotonicTimestamp() override { return mMockSystemTimestamp; } + + void SetMockSystemTimestamp(System::Clock::Timestamp aMockTimestamp) { mMockSystemTimestamp = aMockTimestamp; } + + // Increment the mock timestamp one milisecond at a time for a total of aTime miliseconds. Checks if the timeout expired when + // incrementing + void IncrementMockTimestamp(System::Clock::Milliseconds64 aTime) + { + for (System::Clock::Milliseconds64 i = System::Clock::Milliseconds64(0); i < aTime; i++) + { + mMockSystemTimestamp++; + if (mMockSystemTimestamp == mTimerTimeout) + { + TimerCallbackInterface(nullptr, mSyncScheduler); + } + } + + if (aTime == System::Clock::Milliseconds64(0)) + { + if (mMockSystemTimestamp == mTimerTimeout) + { + TimerCallbackInterface(nullptr, mSyncScheduler); + } + } + } + + SynchronizedReportSchedulerImpl * mSyncScheduler = nullptr; + System::Clock::Timeout mTimerTimeout = System::Clock::Milliseconds64(0x7FFFFFFFFFFFFFFF); + System::Clock::Timestamp mMockSystemTimestamp = System::Clock::Milliseconds64(0); +}; + TestTimerDelegate sTestTimerDelegate; ReportSchedulerImpl sScheduler(&sTestTimerDelegate); +TestTimerSynchronizedDelegate sTestTimerSynchronizedDelegate; +SynchronizedReportSchedulerImpl syncScheduler(&sTestTimerSynchronizedDelegate); + class TestReportScheduler { public: @@ -199,7 +269,8 @@ class TestReportScheduler readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); NL_TEST_ASSERT(aSuite, nullptr != readHandler); VerifyOrReturn(nullptr != readHandler); - NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler)); + // Register ReadHandler using callback method + sScheduler.OnReadHandlerCreated(readHandler); NL_TEST_ASSERT(aSuite, nullptr != sScheduler.FindReadHandlerNode(readHandler)); } @@ -209,7 +280,7 @@ class TestReportScheduler // Test unregister first ReadHandler ReadHandler * firstReadHandler = sScheduler.mReadHandlerList.begin()->GetReadHandler(); - sScheduler.UnregisterReadHandler(firstReadHandler); + sScheduler.OnReadHandlerDestroyed(firstReadHandler); NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 1); NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(firstReadHandler)); @@ -220,7 +291,7 @@ class TestReportScheduler iter++; } ReadHandler * middleReadHandler = iter->GetReadHandler(); - sScheduler.UnregisterReadHandler(middleReadHandler); + sScheduler.OnReadHandlerDestroyed(middleReadHandler); NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 2); NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(middleReadHandler)); @@ -228,7 +299,7 @@ class TestReportScheduler iter = sScheduler.mReadHandlerList.end(); iter--; ReadHandler * lastReadHandler = iter->GetReadHandler(); - sScheduler.UnregisterReadHandler(lastReadHandler); + sScheduler.OnReadHandlerDestroyed(lastReadHandler); NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 3); NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(lastReadHandler)); @@ -264,8 +335,9 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2)); NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(1)); // Do those manually to avoid scheduling an engine run - readHandler1->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true); readHandler1->mState = ReadHandler::HandlerState::GeneratingReports; + sScheduler.OnReadHandlerCreated(readHandler1); + readHandler1->ForceDirtyState(); // Clean read handler, will be triggered at max interval ReadHandler * readHandler2 = @@ -274,6 +346,7 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(0)); // Do those manually to avoid scheduling an engine run readHandler2->mState = ReadHandler::HandlerState::GeneratingReports; + sScheduler.OnReadHandlerCreated(readHandler2); // Clean read handler, will be triggered at max interval, but will be cancelled before ReadHandler * readHandler3 = @@ -282,10 +355,7 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMinReportingIntervalForTests(0)); // Do those manually to avoid scheduling an engine run readHandler3->mState = ReadHandler::HandlerState::GeneratingReports; - - NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler1)); - NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler2)); - NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler3)); + sScheduler.OnReadHandlerCreated(readHandler3); // Confirms that none of the ReadHandlers are currently reportable NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler1)); @@ -352,7 +422,7 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, node->GetReadHandler() == readHandler); // Test OnBecameReportable - readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true); + readHandler->ForceDirtyState(); readHandler->mObserver->OnBecameReportable(readHandler); // Should have changed the scheduled timeout to the handler's min interval, to check, we wait for the min interval to // expire @@ -363,7 +433,7 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler)); // Test OnSubscriptionAction - readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, false); + readHandler->ClearForceDirtyFlag(); readHandler->mObserver->OnSubscriptionAction(readHandler); // Should have changed the scheduled timeout to the handlers max interval, to check, we wait for the min interval to // confirm it is not expired yet so the report should still be scheduled @@ -387,7 +457,354 @@ class TestReportScheduler NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0); NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(readHandler)); - sScheduler.UnregisterReadHandler(readHandler); + sScheduler.OnReadHandlerDestroyed(readHandler); + readHandlerPool.ReleaseAll(); + exchangeCtx->Close(); + NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + } + + static void TestSynchronizedScheduler(nlTestSuite * aSuite, void * aContext) + { + TestContext & ctx = *static_cast(aContext); + NullReadHandlerCallback nullCallback; + // exchange context + Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false); + + // First test: ReadHandler 2 merge on ReadHandler 1 max interval + // Read handler pool + ObjectPool readHandlerPool; + + // Initilaize the mock system time + sTestTimerSynchronizedDelegate.SetMockSystemTimestamp(System::Clock::Milliseconds64(0)); + + ReadHandler * readHandler1 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(0)); + readHandler1->MoveToState(ReadHandler::HandlerState::GeneratingReports); + readHandler1->SetObserver(&syncScheduler); + readHandler1->mObserver->OnReadHandlerCreated(readHandler1); + + ReadHandler * readHandler2 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMaxReportingInterval(3)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(1)); + readHandler2->MoveToState(ReadHandler::HandlerState::GeneratingReports); + readHandler2->SetObserver(&syncScheduler); + readHandler2->mObserver->OnReadHandlerCreated(readHandler2); + + // Confirm all handler are currently registered in the scheduler + NL_TEST_ASSERT(aSuite, syncScheduler.GetNumReadHandlers() == 2); + + ReadHandlerNode * node1 = syncScheduler.FindReadHandlerNode(readHandler1); + ReadHandlerNode * node2 = syncScheduler.FindReadHandlerNode(readHandler2); + + // Confirm that a report emission is scheduled + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportScheduled()); + + // Validates that the lowest max is selected as the common max timestamp + NL_TEST_ASSERT(aSuite, syncScheduler.mNextMaxTimestamp == node1->GetMaxTimestamp()); + // Validates that the highest reportable min is selected as the common min interval (0 here) + NL_TEST_ASSERT(aSuite, syncScheduler.mNextMinTimestamp == node1->GetMinTimestamp()); + // Validates that the next report emission is scheduled on the common max timestamp + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == syncScheduler.mNextMaxTimestamp); + + // Simulate waiting for the max interval to expire (2s) + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(2000)); + + // Confirm that both handlers are now reportable since the timer has expired (readHandler1 from its max and readHandler2 + // from its sync) + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + // Confirm timeout has expired and no report is scheduled, an engine run would typically happen here + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler1)); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler2)); + + // Simulate a report emission for readHandler1 + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + // Simulate a report emission for readHandler2 + readHandler2->mObserver->OnSubscriptionAction(readHandler2); + + // Validate that the max timestamp for both readhandlers got updated and that the next report emission is scheduled on + // the new max timestamp for readhandler1 + NL_TEST_ASSERT(aSuite, node1->GetMaxTimestamp() > sTestTimerSynchronizedDelegate.GetCurrentMonotonicTimestamp()); + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + + // Confirm behavior when a read handler becomes dirty + readHandler2->ForceDirtyState(); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + // Simulate wait enough for min timestamp of readHandler2 to be reached (1s) + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + readHandler2->mObserver->OnBecameReportable(readHandler2); + + // confirm report scheduled now + NL_TEST_ASSERT(aSuite, + syncScheduler.mTestNextReportTimestamp == sTestTimerSynchronizedDelegate.GetCurrentMonotonicTimestamp()); + // Increment the timestamp by 0 here to trigger an engine run as the mock timer is only calling the timeout callback if we + // increment the mock timestamp + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(0)); + + // since the min interval on readHandler1 is 0, it should also be reportable now by sync mechanism + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node2->GetMinTimestamp()); + + // Confirm that the next report emission is scheduled on the min timestamp of readHandler2 as it is the highest reportable + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node2->GetMinTimestamp()); + + // Simulate a report emission for readHandler1 + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + + // ReadHandler 2 should still be reportable since it hasn't emitted a report yet + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + readHandler2->ClearForceDirtyFlag(); + readHandler2->mObserver->OnSubscriptionAction(readHandler2); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + + // Validate next report scheduled on the max timestamp of readHandler1 + NL_TEST_ASSERT(aSuite, node1->GetMaxTimestamp() > sTestTimerSynchronizedDelegate.GetCurrentMonotonicTimestamp()); + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + + // Simulate readHandler1 becoming dirty after less than 1 seconds + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(900)); + readHandler1->ForceDirtyState(); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + readHandler1->mObserver->OnBecameReportable(readHandler1); + + // Validate next report scheduled on the min timestamp of readHandler1 (readHandler 2 is not currently reportable) + NL_TEST_ASSERT(aSuite, + syncScheduler.mTestNextReportTimestamp == sTestTimerSynchronizedDelegate.GetCurrentMonotonicTimestamp()); + // Simulate a report emission for readHandler1 + readHandler1->ClearForceDirtyFlag(); + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + + // The next report should be scheduler on the max timestamp of readHandler1 and readHandler2 should be synced + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node1->GetMaxTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(2000)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + readHandler2->mObserver->OnSubscriptionAction(readHandler2); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + + // Simulate a new ReadHandler being added with a min timestamp that will force a conflict + + // Wait for 1 second, nothing should happen here + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + + ReadHandler * readHandler3 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMaxReportingInterval(3)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMinReportingIntervalForTests(2)); + readHandler3->MoveToState(ReadHandler::HandlerState::GeneratingReports); + readHandler3->SetObserver(&syncScheduler); + readHandler3->mObserver->OnReadHandlerCreated(readHandler3); + + // Confirm all handler are currently registered in the scheduler + NL_TEST_ASSERT(aSuite, syncScheduler.GetNumReadHandlers() == 3); + ReadHandlerNode * node3 = syncScheduler.FindReadHandlerNode(readHandler3); + + // Since the min interval on readHandler3 is 2, it should be above the current max timestamp, therefore the next report + // should still happen on the max timestamp of readHandler1 and the sync should be done on future reports + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + // The min timestamp should also not have changed since the min of readhandler3 is higher than the current max + NL_TEST_ASSERT(aSuite, syncScheduler.mNextMinTimestamp == node2->GetMinTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + + // Confirm that readHandler1 and readHandler 2 are now reportable, whilst readHandler3 is not + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler3)); + readHandler1->mObserver->OnBecameReportable(readHandler1); + readHandler2->mObserver->OnBecameReportable(readHandler2); + + // Simulate a report emission for readHandler1 and readHandler2 + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + readHandler1->mObserver->OnSubscriptionAction(readHandler2); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + + // Confirm that next report is scheduled on the max timestamp of readHandler3 and other 2 readHandlers are synced + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node3->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node3->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node3->GetMaxTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(2000)); + + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler3)); + readHandler1->mObserver->OnBecameReportable(readHandler1); + readHandler2->mObserver->OnBecameReportable(readHandler2); + readHandler3->mObserver->OnBecameReportable(readHandler3); + // Engine run should happen here and send all reports + readHandler1->mObserver->OnSubscriptionAction(readHandler1); + readHandler2->mObserver->OnSubscriptionAction(readHandler2); + readHandler3->mObserver->OnSubscriptionAction(readHandler3); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler3)); + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node1->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node1->GetMaxTimestamp()); + + // Now simulate a new readHandler being added with a max forcing a conflict + ReadHandler * readHandler4 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler4->SetMaxReportingInterval(1)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler4->SetMinReportingIntervalForTests(0)); + readHandler4->MoveToState(ReadHandler::HandlerState::GeneratingReports); + readHandler4->SetObserver(&syncScheduler); + readHandler4->mObserver->OnReadHandlerCreated(readHandler4); + + // Confirm all handler are currently registered in the scheduler + NL_TEST_ASSERT(aSuite, syncScheduler.GetNumReadHandlers() == 4); + ReadHandlerNode * node4 = syncScheduler.FindReadHandlerNode(readHandler4); + + // Confirm next report is scheduled on the max timestamp of readHandler4 and other handlers 1 and 2 are synced + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node4->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node4->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node4->GetMaxTimestamp()); + + // Confirm handler 3 is synched on a later timestamp since its min is higher than the max of readHandler4 + NL_TEST_ASSERT(aSuite, node3->GetSyncTimestamp() == node1->GetMaxTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1100)); + + // Confirm readHandler1, 2 and 4 are reportable + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler4)); + + // Confirm readHandler3 is not reportable + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler3)); + readHandler4->mObserver->OnBecameReportable(readHandler1); + readHandler4->mObserver->OnBecameReportable(readHandler2); + readHandler4->mObserver->OnBecameReportable(readHandler4); + readHandler4->mObserver->OnSubscriptionAction(readHandler1); + readHandler4->mObserver->OnSubscriptionAction(readHandler2); + readHandler4->mObserver->OnSubscriptionAction(readHandler4); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler4)); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + + // Confirm readHandler3 is reportable and other handlers are synced + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler3)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler4)); + syncScheduler.OnBecameReportable(readHandler1); + syncScheduler.OnBecameReportable(readHandler2); + syncScheduler.OnBecameReportable(readHandler3); + syncScheduler.OnBecameReportable(readHandler4); + syncScheduler.OnSubscriptionAction(readHandler1); + syncScheduler.OnSubscriptionAction(readHandler2); + syncScheduler.OnSubscriptionAction(readHandler3); + syncScheduler.OnSubscriptionAction(readHandler4); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler3)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler4)); + + // Next emission should be scheduled on the max timestamp of readHandler4 as it is the most restrictive, and handlers 1 and + // 2 should be synced to handler 4 + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node4->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node4->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node4->GetMaxTimestamp()); + // handler 3 should have a sync on a different point as its min is higher, in this case it is the max timestamp of handler 1 + NL_TEST_ASSERT(aSuite, node3->GetSyncTimestamp() == node1->GetMaxTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + + // Confirm readHandler 1-2-4 are reportable + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler4)); + + // Confirm readHandler3 is not reportable because of its min interval + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler3)); + + syncScheduler.OnReadHandlerDestroyed(readHandler1); + syncScheduler.OnReadHandlerDestroyed(readHandler2); + syncScheduler.OnReadHandlerDestroyed(readHandler3); + syncScheduler.OnReadHandlerDestroyed(readHandler4); + + // Reset all handlers + // Test case: Scheduler 1 and 2 are reportable but min2 > max1, they should sync only when possible (min2 = 3, max1 = 2) + NL_TEST_ASSERT(aSuite, syncScheduler.GetNumReadHandlers() == 0); + + readHandler1->MoveToState(ReadHandler::HandlerState::Idle); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(0)); + readHandler1->MoveToState(ReadHandler::HandlerState::GeneratingReports); + syncScheduler.OnReadHandlerCreated(readHandler1); + readHandler1->ForceDirtyState(); + syncScheduler.OnBecameReportable(readHandler1); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + + readHandler2->MoveToState(ReadHandler::HandlerState::Idle); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMaxReportingInterval(4)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(3)); + readHandler2->MoveToState(ReadHandler::HandlerState::GeneratingReports); + syncScheduler.OnReadHandlerCreated(readHandler2); + readHandler2->ForceDirtyState(); + syncScheduler.OnBecameReportable(readHandler2); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + + node1 = syncScheduler.FindReadHandlerNode(readHandler1); + node2 = syncScheduler.FindReadHandlerNode(readHandler2); + + // Verify report is scheduled immediately as readHandler1 is dirty and its min == 0 + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMinTimestamp()); + readHandler1->ClearForceDirtyFlag(); // report got emited so clear dirty flag + syncScheduler.OnSubscriptionAction(readHandler1); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + + // Confirm next report is scheduled on the max timestamp of readHandler1 and readhandler2 is not synced + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + // Node 2's sync timestamp should have remained unaffected since its min is higher + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node2->GetMaxTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(2000)); + // Verify handler 1 became reportable + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + syncScheduler.OnBecameReportable(readHandler1); + + // simulate run with only readhandler1 reportable + syncScheduler.OnSubscriptionAction(readHandler1); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !syncScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node2->GetMinTimestamp()); + NL_TEST_ASSERT(aSuite, node1->GetSyncTimestamp() == node2->GetMinTimestamp()); + + sTestTimerSynchronizedDelegate.IncrementMockTimestamp(System::Clock::Milliseconds64(1000)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, syncScheduler.IsReportableNow(readHandler2)); + + readHandler2->ClearForceDirtyFlag(); + syncScheduler.OnSubscriptionAction(readHandler1); + syncScheduler.OnSubscriptionAction(readHandler2); + + NL_TEST_ASSERT(aSuite, syncScheduler.mTestNextReportTimestamp == node1->GetMaxTimestamp()); + NL_TEST_ASSERT(aSuite, node2->GetSyncTimestamp() == node2->GetMaxTimestamp()); + + syncScheduler.UnregisterAllHandlers(); readHandlerPool.ReleaseAll(); exchangeCtx->Close(); NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); @@ -408,6 +825,7 @@ static nlTest sTests[] = { NL_TEST_DEF("TestReadHandlerList", chip::app::reporting::TestReportScheduler::TestReadHandlerList), NL_TEST_DEF("TestReportTiming", chip::app::reporting::TestReportScheduler::TestReportTiming), NL_TEST_DEF("TestObserverCallbacks", chip::app::reporting::TestReportScheduler::TestObserverCallbacks), + NL_TEST_DEF("TestSynchronizedScheduler", chip::app::reporting::TestReportScheduler::TestSynchronizedScheduler), NL_TEST_SENTINEL(), };