From 119236766438e15f3071c1687f8329adbbc09e71 Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Mon, 30 Jan 2023 08:31:00 -0800 Subject: [PATCH] Add flag to enable/disable dataCache in ClusterStateCache (#24712) --- src/app/ClusterStateCache.cpp | 75 +++-- src/app/ClusterStateCache.h | 15 +- src/controller/tests/BUILD.gn | 1 + .../tests/TestEventNumberCaching.cpp | 281 ++++++++++++++++++ src/controller/tests/data_model/TestRead.cpp | 75 +++++ 5 files changed, 415 insertions(+), 32 deletions(-) create mode 100644 src/controller/tests/TestEventNumberCaching.cpp diff --git a/src/app/ClusterStateCache.cpp b/src/app/ClusterStateCache.cpp index 5497dfd453098c..302f70e44eea00 100644 --- a/src/app/ClusterStateCache.cpp +++ b/src/app/ClusterStateCache.cpp @@ -57,16 +57,19 @@ CHIP_ERROR ClusterStateCache::UpdateCache(const ConcreteDataAttributePath & aPat if (apData) { - size_t elementSize = 0; - ReturnErrorOnFailure(GetElementTLVSize(apData, elementSize)); - Platform::ScopedMemoryBufferWithSize backingBuffer; - backingBuffer.Calloc(elementSize); - VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY); - TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), elementSize); - ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData)); - ReturnErrorOnFailure(writer.Finalize(backingBuffer)); - - state.Set>(std::move(backingBuffer)); + if (mCacheData) + { + size_t elementSize = 0; + ReturnErrorOnFailure(GetElementTLVSize(apData, elementSize)); + Platform::ScopedMemoryBufferWithSize backingBuffer; + backingBuffer.Calloc(elementSize); + VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY); + TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), elementSize); + ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData)); + ReturnErrorOnFailure(writer.Finalize(backingBuffer)); + + state.Set>(std::move(backingBuffer)); + } // // Clear out the committed data version and only set it again once we have received all data for this cluster. // Otherwise, we may have incomplete data that looks like it's complete since it has a valid data version. @@ -99,7 +102,10 @@ CHIP_ERROR ClusterStateCache::UpdateCache(const ConcreteDataAttributePath & aPat } else { - state.Set(aStatus); + if (mCacheData) + { + state.Set(aStatus); + } } // @@ -111,8 +117,12 @@ CHIP_ERROR ClusterStateCache::UpdateCache(const ConcreteDataAttributePath & aPat mAddedEndpoints.push_back(aPath.mEndpointId); } - mCache[aPath.mEndpointId][aPath.mClusterId].mAttributes[aPath.mAttributeId] = std::move(state); - mChangedAttributeSet.insert(aPath); + if (mCacheData) + { + mCache[aPath.mEndpointId][aPath.mClusterId].mAttributes[aPath.mAttributeId] = std::move(state); + mChangedAttributeSet.insert(aPath); + } + return CHIP_NO_ERROR; } @@ -127,32 +137,37 @@ CHIP_ERROR ClusterStateCache::UpdateEventCache(const EventHeader & aEventHeader, { return CHIP_NO_ERROR; } - System::PacketBufferHandle handle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); - VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); - - System::PacketBufferTLVWriter writer; - writer.Init(std::move(handle), false); + if (mCacheData) + { + System::PacketBufferHandle handle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); + VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); - ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData)); - ReturnErrorOnFailure(writer.Finalize(&handle)); + System::PacketBufferTLVWriter writer; + writer.Init(std::move(handle), false); - // - // Compact the buffer down to a more reasonably sized packet buffer - // if we can. - // - handle.RightSize(); + ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData)); + ReturnErrorOnFailure(writer.Finalize(&handle)); - EventData eventData; - eventData.first = aEventHeader; - eventData.second = std::move(handle); + // + // Compact the buffer down to a more reasonably sized packet buffer + // if we can. + // + handle.RightSize(); - mEventDataCache.insert(std::move(eventData)); + EventData eventData; + eventData.first = aEventHeader; + eventData.second = std::move(handle); + mEventDataCache.insert(std::move(eventData)); + } mHighestReceivedEventNumber.SetValue(aEventHeader.mEventNumber); } else if (apStatus) { - mEventStatusCache[aEventHeader.mPath] = *apStatus; + if (mCacheData) + { + mEventStatusCache[aEventHeader.mPath] = *apStatus; + } } return CHIP_NO_ERROR; diff --git a/src/app/ClusterStateCache.h b/src/app/ClusterStateCache.h index efc2cde251d5bf..b0a2d2b540345f 100644 --- a/src/app/ClusterStateCache.h +++ b/src/app/ClusterStateCache.h @@ -86,8 +86,18 @@ class ClusterStateCache : protected ReadClient::Callback virtual void OnEndpointAdded(ClusterStateCache * cache, EndpointId endpointId){}; }; - ClusterStateCache(Callback & callback, Optional highestReceivedEventNumber = Optional::Missing()) : - mCallback(callback), mBufferedReader(*this) + /** + * + * @param [in] callback the derived callback which inherit from ReadClient::Callback + * @param [in] highestReceivedEventNumber optional highest received event number, if cache receive the events with the number + * less than or equal to this value, skip those events + * @param [in] cacheData boolean to decide whether this cache would store attribute/event data/status, + * the default is true. + */ + ClusterStateCache(Callback & callback, Optional highestReceivedEventNumber = Optional::Missing(), + bool cacheData = true) : + mCallback(callback), + mBufferedReader(*this), mCacheData(cacheData) { mHighestReceivedEventNumber = highestReceivedEventNumber; } @@ -622,6 +632,7 @@ class ClusterStateCache : protected ReadClient::Callback std::map mEventStatusCache; BufferedReadCallback mBufferedReader; ConcreteClusterPath mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId); + bool mCacheData = true; }; }; // namespace app diff --git a/src/controller/tests/BUILD.gn b/src/controller/tests/BUILD.gn index 0528c977c95a3b..f3ae7c761cb186 100644 --- a/src/controller/tests/BUILD.gn +++ b/src/controller/tests/BUILD.gn @@ -30,6 +30,7 @@ chip_test_suite("tests") { test_sources += [ "TestEventCaching.cpp" ] test_sources += [ "TestReadChunking.cpp" ] test_sources += [ "TestWriteChunking.cpp" ] + test_sources += [ "TestEventNumberCaching.cpp" ] } cflags = [ "-Wconversion" ] diff --git a/src/controller/tests/TestEventNumberCaching.cpp b/src/controller/tests/TestEventNumberCaching.cpp new file mode 100644 index 00000000000000..aaaa6e039dea7e --- /dev/null +++ b/src/controller/tests/TestEventNumberCaching.cpp @@ -0,0 +1,281 @@ +/* + * + * 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. + */ + +#include "app-common/zap-generated/ids/Attributes.h" +#include "app-common/zap-generated/ids/Clusters.h" +#include "app/ClusterStateCache.h" +#include "app/ConcreteAttributePath.h" +#include "protocols/interaction_model/Constants.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { + +static uint8_t gDebugEventBuffer[4096]; +static uint8_t gInfoEventBuffer[4096]; +static uint8_t gCritEventBuffer[4096]; +static chip::app::CircularEventBuffer gCircularEventBuffer[3]; + +class TestContext : public chip::Test::AppContext +{ +public: + static int Initialize(void * context) + { + if (AppContext::Initialize(context) != SUCCESS) + return FAILURE; + + auto * ctx = static_cast(context); + + if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR) + { + return FAILURE; + } + + chip::app::LogStorageResources logStorageResources[] = { + { &gDebugEventBuffer[0], sizeof(gDebugEventBuffer), chip::app::PriorityLevel::Debug }, + { &gInfoEventBuffer[0], sizeof(gInfoEventBuffer), chip::app::PriorityLevel::Info }, + { &gCritEventBuffer[0], sizeof(gCritEventBuffer), chip::app::PriorityLevel::Critical }, + }; + + chip::app::EventManagement::CreateEventManagement(&ctx->GetExchangeManager(), + sizeof(logStorageResources) / sizeof(logStorageResources[0]), + gCircularEventBuffer, logStorageResources, &ctx->mEventCounter); + + return SUCCESS; + } + + static int Finalize(void * context) + { + chip::app::EventManagement::DestroyEventManagement(); + + if (AppContext::Finalize(context) != SUCCESS) + return FAILURE; + + return SUCCESS; + } + +private: + MonotonicallyIncreasingCounter mEventCounter; +}; + +nlTestSuite * gSuite = nullptr; + +// +// The generated endpoint_config for the controller app has Endpoint 1 +// already used in the fixed endpoint set of size 1. Consequently, let's use the next +// number higher than that for our dynamic test endpoint. +// +constexpr EndpointId kTestEndpointId = 2; + +class TestReadEvents +{ +public: + TestReadEvents() {} + static void TestEventNumberCaching(nlTestSuite * apSuite, void * apContext); + +private: +}; + +//clang-format off +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrs) +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters) +DECLARE_DYNAMIC_CLUSTER(Clusters::UnitTesting::Id, testClusterAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; + +DECLARE_DYNAMIC_ENDPOINT(testEndpoint, testEndpointClusters); + +//clang-format on + +class TestReadCallback : public app::ClusterStateCache::Callback +{ +public: + TestReadCallback() : mClusterCacheAdapter(*this, Optional::Missing(), false /*cacheData*/) {} + void OnDone(app::ReadClient *) {} + + app::ClusterStateCache mClusterCacheAdapter; +}; + +namespace { + +void GenerateEvents(nlTestSuite * apSuite, chip::EventNumber & firstEventNumber, chip::EventNumber & lastEventNumber) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + static uint8_t generationCount = 0; + + Clusters::UnitTesting::Events::TestEvent::Type content; + + for (int i = 0; i < 5; i++) + { + content.arg1 = static_cast(generationCount++); + NL_TEST_ASSERT(apSuite, (err = app::LogEvent(content, kTestEndpointId, lastEventNumber)) == CHIP_NO_ERROR); + if (i == 0) + { + firstEventNumber = lastEventNumber; + } + } +} + +} // namespace + +/* + * This validates event caching by forcing a bunch of events to get generated, then reading them back + * and upon completion of that operation, check the received version from cache, and note that cache would store + * correpsonding attribute data since data cache is disabled. + * + */ +void TestReadEvents::TestEventNumberCaching(nlTestSuite * apSuite, void * apContext) +{ + TestContext & ctx = *static_cast(apContext); + auto sessionHandle = ctx.GetSessionBobToAlice(); + app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); + + // Initialize the ember side server logic + InitDataModelHandler(&ctx.GetExchangeManager()); + + // Register our fake dynamic endpoint. + DataVersion dataVersionStorage[ArraySize(testEndpointClusters)]; + emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span(dataVersionStorage)); + + chip::EventNumber firstEventNumber; + chip::EventNumber lastEventNumber; + + GenerateEvents(apSuite, firstEventNumber, lastEventNumber); + + app::EventPathParams eventPath; + eventPath.mEndpointId = kTestEndpointId; + eventPath.mClusterId = app::Clusters::UnitTesting::Id; + app::ReadPrepareParams readParams(sessionHandle); + + readParams.mpEventPathParamsList = &eventPath; + readParams.mEventPathParamsListSize = 1; + readParams.mEventNumber.SetValue(firstEventNumber); + + TestReadCallback readCallback; + + { + Optional highestEventNumber; + readCallback.mClusterCacheAdapter.GetHighestReceivedEventNumber(highestEventNumber); + NL_TEST_ASSERT(apSuite, !highestEventNumber.HasValue()); + app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mClusterCacheAdapter.GetBufferedCallback(), + app::ReadClient::InteractionType::Read); + + NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + + readCallback.mClusterCacheAdapter.ForEachEventData([&apSuite, &readCallback](const app::EventHeader & header) { + NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == Clusters::UnitTesting::Id); + NL_TEST_ASSERT(apSuite, header.mPath.mEventId == Clusters::UnitTesting::Events::TestEvent::Id); + NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId); + + Clusters::UnitTesting::Events::TestEvent::DecodableType eventData; + NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) != CHIP_NO_ERROR); + return CHIP_NO_ERROR; + }); + + readCallback.mClusterCacheAdapter.GetHighestReceivedEventNumber(highestEventNumber); + NL_TEST_ASSERT(apSuite, highestEventNumber.HasValue() && highestEventNumber.Value() == 4); + } + + // + // Clear out the event cache and set its highest received event number to a non zero value. Validate that + // we don't receive events lower than that value. + // + { + app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mClusterCacheAdapter.GetBufferedCallback(), + app::ReadClient::InteractionType::Read); + + readCallback.mClusterCacheAdapter.ClearEventCache(true); + Optional highestEventNumber; + readCallback.mClusterCacheAdapter.GetHighestReceivedEventNumber(highestEventNumber); + NL_TEST_ASSERT(apSuite, !highestEventNumber.HasValue()); + readCallback.mClusterCacheAdapter.SetHighestReceivedEventNumber(3); + + NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + + readCallback.mClusterCacheAdapter.ForEachEventData([&apSuite, &readCallback](const app::EventHeader & header) { + NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == Clusters::UnitTesting::Id); + NL_TEST_ASSERT(apSuite, header.mPath.mEventId == Clusters::UnitTesting::Events::TestEvent::Id); + NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId); + + Clusters::UnitTesting::Events::TestEvent::DecodableType eventData; + NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) != CHIP_NO_ERROR); + return CHIP_NO_ERROR; + }); + + readCallback.mClusterCacheAdapter.GetHighestReceivedEventNumber(highestEventNumber); + NL_TEST_ASSERT(apSuite, highestEventNumber.HasValue() && highestEventNumber.Value() == 4); + } + NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + + emberAfClearDynamicEndpoint(0); +} + +// clang-format off +const nlTest sTests[] = +{ + NL_TEST_DEF("TestEventNumberCaching", TestReadEvents::TestEventNumberCaching), + NL_TEST_SENTINEL() +}; + +// clang-format on + +// clang-format off +nlTestSuite sSuite = +{ + "TestEventNumberCaching", + &sTests[0], + TestContext::Initialize, + TestContext::Finalize +}; +// clang-format on + +} // namespace + +int TestEventNumberCaching() +{ + gSuite = &sSuite; + return chip::ExecuteTestsWithContext(&sSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestEventNumberCaching) diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp index ddc3b7219b2834..63983a9a5cb722 100644 --- a/src/controller/tests/data_model/TestRead.cpp +++ b/src/controller/tests/data_model/TestRead.cpp @@ -275,6 +275,7 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback static void TestReadHandler_SubscriptionReportingIntervalsTest9(nlTestSuite * apSuite, void * apContext); static void TestReadHandlerResourceExhaustion_MultipleReads(nlTestSuite * apSuite, void * apContext); static void TestReadSubscribeAttributeResponseWithCache(nlTestSuite * apSuite, void * apContext); + static void TestReadSubscribeAttributeResponseWithVersionOnlyCache(nlTestSuite * apSuite, void * apContext); static void TestReadHandler_KillOverQuotaSubscriptions(nlTestSuite * apSuite, void * apContext); static void TestReadHandler_KillOldestSubscriptions(nlTestSuite * apSuite, void * apContext); static void TestReadHandler_ParallelReads(nlTestSuite * apSuite, void * apContext); @@ -1419,6 +1420,79 @@ void TestReadInteraction::TestReadSubscribeAttributeResponseWithCache(nlTestSuit NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); } +void TestReadInteraction::TestReadSubscribeAttributeResponseWithVersionOnlyCache(nlTestSuite * apSuite, void * apContext) +{ + TestContext & ctx = *static_cast(apContext); + CHIP_ERROR err = CHIP_NO_ERROR; + responseDirective = kSendDataResponse; + + MockInteractionModelApp delegate; + chip::app::ClusterStateCache cache(delegate, Optional::Missing(), false /*cachedData*/); + + chip::app::ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice()); + // + // Test the application callback as well to ensure we get the right number of SubscriptionEstablishment/Termination + // callbacks. + // + app::InteractionModelEngine::GetInstance()->RegisterReadHandlerAppCallback(&gTestReadInteraction); + + // read of E2C2A* and E3C2A2. Expect cache E2C2 version + { + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams2[2]; + attributePathParams2[0].mEndpointId = chip::Test::kMockEndpoint2; + attributePathParams2[0].mClusterId = chip::Test::MockClusterId(3); + attributePathParams2[0].mAttributeId = kInvalidAttributeId; + + attributePathParams2[1].mEndpointId = chip::Test::kMockEndpoint3; + attributePathParams2[1].mClusterId = chip::Test::MockClusterId(2); + attributePathParams2[1].mAttributeId = chip::Test::MockAttributeId(2); + readPrepareParams.mpAttributePathParamsList = attributePathParams2; + readPrepareParams.mAttributePathParamsListSize = 2; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + // There are supported 2 global and 3 non-global attributes in E2C2A* and 1 E3C2A2 + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 6); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + app::ConcreteClusterPath clusterPath1(chip::Test::kMockEndpoint2, chip::Test::MockClusterId(3)); + NL_TEST_ASSERT(apSuite, cache.GetVersion(clusterPath1, version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 0)); + Optional version2; + app::ConcreteClusterPath clusterPath2(chip::Test::kMockEndpoint3, chip::Test::MockClusterId(2)); + NL_TEST_ASSERT(apSuite, cache.GetVersion(clusterPath2, version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + + { + app::ConcreteAttributePath attributePath(chip::Test::kMockEndpoint2, chip::Test::MockClusterId(3), + chip::Test::MockAttributeId(2)); + TLV::TLVReader reader; + NL_TEST_ASSERT(apSuite, cache.Get(attributePath, reader) != CHIP_NO_ERROR); + } + + { + app::ConcreteAttributePath attributePath(chip::Test::kMockEndpoint2, chip::Test::MockClusterId(3), + chip::Test::MockAttributeId(3)); + TLV::TLVReader reader; + NL_TEST_ASSERT(apSuite, cache.Get(attributePath, reader) != CHIP_NO_ERROR); + } + + { + app::ConcreteAttributePath attributePath(chip::Test::kMockEndpoint3, chip::Test::MockClusterId(2), + chip::Test::MockAttributeId(2)); + TLV::TLVReader reader; + NL_TEST_ASSERT(apSuite, cache.Get(attributePath, reader) != CHIP_NO_ERROR); + } + delegate.mNumAttributeResponse = 0; + } + + NL_TEST_ASSERT(apSuite, app::InteractionModelEngine::GetInstance()->GetNumActiveReadClients() == 0); + NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); +} + void TestReadInteraction::TestReadEventResponse(nlTestSuite * apSuite, void * apContext) { TestContext & ctx = *static_cast(apContext); @@ -4572,6 +4646,7 @@ const nlTest sTests[] = NL_TEST_DEF("TestReadHandler_SubscriptionReportingIntervalsTest7", TestReadInteraction::TestReadHandler_SubscriptionReportingIntervalsTest7), NL_TEST_DEF("TestReadHandler_SubscriptionReportingIntervalsTest8", TestReadInteraction::TestReadHandler_SubscriptionReportingIntervalsTest8), NL_TEST_DEF("TestReadHandler_SubscriptionReportingIntervalsTest9", TestReadInteraction::TestReadHandler_SubscriptionReportingIntervalsTest9), + NL_TEST_DEF("TestReadSubscribeAttributeResponseWithVersionOnlyCache", TestReadInteraction::TestReadSubscribeAttributeResponseWithVersionOnlyCache), NL_TEST_DEF("TestReadSubscribeAttributeResponseWithCache", TestReadInteraction::TestReadSubscribeAttributeResponseWithCache), NL_TEST_DEF("TestReadHandler_KillOverQuotaSubscriptions", TestReadInteraction::TestReadHandler_KillOverQuotaSubscriptions), NL_TEST_DEF("TestReadHandler_KillOldestSubscriptions", TestReadInteraction::TestReadHandler_KillOldestSubscriptions),