Skip to content

Commit

Permalink
[app] Implement deferred attribute persister (#23366)
Browse files Browse the repository at this point in the history
* [app] Implement deferred attribute persister

Fast-changing attributes with NVM storage, such as
CurrentLevel of the LevelControl cluster, may result in
rapid flash wearout using the default attribute persister
which stores all values immediately.
Implement a helper adapter class for the attribute
persistence interface to defer writes of selected
attributes.

Use the new class in nRF Connect lighting-app for
verification.

* Code review

* Add a comment
  • Loading branch information
Damian-Nordic authored and pull[bot] committed Sep 29, 2023
1 parent 2212809 commit 1300859
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 9 deletions.
13 changes: 13 additions & 0 deletions examples/lighting-app/nrfconnect/main/AppTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <app-common/zap-generated/attribute-type.h>
#include <app-common/zap-generated/attributes/Accessors.h>
#include <app-common/zap-generated/cluster-id.h>
#include <app/DeferredAttributePersistenceProvider.h>
#include <app/clusters/identify-server/identify-server.h>
#include <app/clusters/ota-requestor/OTATestEventTriggerDelegate.h>
#include <app/server/Dnssd.h>
Expand Down Expand Up @@ -89,6 +90,17 @@ bool sHaveBLEConnections = false;

chip::DeviceLayer::DeviceInfoProviderImpl gExampleDeviceInfoProvider;

// Define a custom attribute persister which makes actual write of the CurrentLevel attribute value
// to the non-volatile storage only when it has remained constant for 5 seconds. This is to reduce
// the flash wearout when the attribute changes frequently as a result of MoveToLevel command.
// DeferredAttribute object describes a deferred attribute, but also holds a buffer with a value to
// be written, so it must live so long as the DeferredAttributePersistenceProvider object.
DeferredAttribute gCurrentLevelPersister(ConcreteAttributePath(kLightEndpointId, Clusters::LevelControl::Id,
Clusters::LevelControl::Attributes::CurrentLevel::Id));
DeferredAttributePersistenceProvider gDeferredAttributePersister(Server::GetInstance().GetDefaultAttributePersister(),
Span<DeferredAttribute>(&gCurrentLevelPersister, 1),
System::Clock::Milliseconds32(5000));

} // namespace

AppTask AppTask::sAppTask;
Expand Down Expand Up @@ -195,6 +207,7 @@ CHIP_ERROR AppTask::Init()

gExampleDeviceInfoProvider.SetStorageDelegate(&Server::GetInstance().GetPersistentStorage());
chip::DeviceLayer::SetDeviceInfoProvider(&gExampleDeviceInfoProvider);
app::SetAttributePersistenceProvider(&gDeferredAttributePersister);

ConfigurationMgr().LogDeviceConfig();
PrintOnboardingCodes(chip::RendezvousInformationFlags(chip::RendezvousInformationFlag::kBLE));
Expand Down
4 changes: 1 addition & 3 deletions src/app/AttributePersistenceProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ class AttributePersistenceProvider
* list) to non-volatile memory.
*
* @param [in] aPath the attribute path for the data being written.
* @param [in] aMetadata the attribute metadata, as a convenience.
* @param [in] aValue the data to write. Integers and floats are
* represented in native endianness. Strings are represented
* as Pascal-style strings, as in ZCL, with a length prefix
Expand All @@ -51,8 +50,7 @@ class AttributePersistenceProvider
* of the data in the string (including the length prefix),
* which is no larger than the `size` member of aMetadata.
*/
virtual CHIP_ERROR WriteValue(const ConcreteAttributePath & aPath, const EmberAfAttributeMetadata * aMetadata,
const ByteSpan & aValue) = 0;
virtual CHIP_ERROR WriteValue(const ConcreteAttributePath & aPath, const ByteSpan & aValue) = 0;

/**
* Read an attribute value from non-volatile memory.
Expand Down
1 change: 1 addition & 0 deletions src/app/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ static_library("app") {
"CommandResponseHelper.h",
"CommandSender.cpp",
"DefaultAttributePersistenceProvider.cpp",
"DeferredAttributePersistenceProvider.cpp",
"DeviceProxy.cpp",
"DeviceProxy.h",
"EventManagement.cpp",
Expand Down
3 changes: 1 addition & 2 deletions src/app/DefaultAttributePersistenceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
namespace chip {
namespace app {

CHIP_ERROR DefaultAttributePersistenceProvider::WriteValue(const ConcreteAttributePath & aPath,
const EmberAfAttributeMetadata * aMetadata, const ByteSpan & aValue)
CHIP_ERROR DefaultAttributePersistenceProvider::WriteValue(const ConcreteAttributePath & aPath, const ByteSpan & aValue)
{
VerifyOrReturnError(mStorage != nullptr, CHIP_ERROR_INCORRECT_STATE);

Expand Down
3 changes: 1 addition & 2 deletions src/app/DefaultAttributePersistenceProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ class DefaultAttributePersistenceProvider : public AttributePersistenceProvider
void Shutdown() {}

// AttributePersistenceProvider implementation.
CHIP_ERROR WriteValue(const ConcreteAttributePath & aPath, const EmberAfAttributeMetadata * aMetadata,
const ByteSpan & aValue) override;
CHIP_ERROR WriteValue(const ConcreteAttributePath & aPath, const ByteSpan & aValue) override;
CHIP_ERROR ReadValue(const ConcreteAttributePath & aPath, const EmberAfAttributeMetadata * aMetadata,
MutableByteSpan & aValue) override;

Expand Down
98 changes: 98 additions & 0 deletions src/app/DeferredAttributePersistenceProvider.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2022 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 <app/DeferredAttributePersistenceProvider.h>

#include <platform/CHIPDeviceLayer.h>

namespace chip {
namespace app {

CHIP_ERROR DeferredAttribute::PrepareWrite(System::Clock::Timestamp flushTime, const ByteSpan & value)
{
mFlushTime = flushTime;

if (mValue.AllocatedSize() != value.size())
{
mValue.Alloc(value.size());
ReturnErrorCodeIf(!mValue, CHIP_ERROR_NO_MEMORY);
}

memcpy(mValue.Get(), value.data(), value.size());
return CHIP_NO_ERROR;
}

void DeferredAttribute::Flush(AttributePersistenceProvider & persister)
{
VerifyOrReturn(IsArmed());
persister.WriteValue(mPath, ByteSpan(mValue.Get(), mValue.AllocatedSize()));
mValue.Release();
}

CHIP_ERROR DeferredAttributePersistenceProvider::WriteValue(const ConcreteAttributePath & path, const ByteSpan & value)
{
for (DeferredAttribute & da : mDeferredAttributes)
{
if (da.Matches(path))
{
ReturnErrorOnFailure(da.PrepareWrite(System::SystemClock().GetMonotonicTimestamp() + mWriteDelay, value));
FlushAndScheduleNext();
return CHIP_NO_ERROR;
}
}

return mPersister.WriteValue(path, value);
}

CHIP_ERROR DeferredAttributePersistenceProvider::ReadValue(const ConcreteAttributePath & path,
const EmberAfAttributeMetadata * metadata, MutableByteSpan & value)
{
return mPersister.ReadValue(path, metadata, value);
}

void DeferredAttributePersistenceProvider::FlushAndScheduleNext()
{
const System::Clock::Timestamp now = System::SystemClock().GetMonotonicTimestamp();
System::Clock::Timestamp nextFlushTime = System::Clock::Timestamp::max();

for (DeferredAttribute & da : mDeferredAttributes)
{
if (!da.IsArmed())
{
continue;
}

if (da.GetFlushTime() <= now)
{
da.Flush(mPersister);
}
else
{
nextFlushTime = chip::min(nextFlushTime, da.GetFlushTime());
}
}

if (nextFlushTime != System::Clock::Timestamp::max())
{
DeviceLayer::SystemLayer().StartTimer(
nextFlushTime - now,
[](System::Layer *, void * me) { static_cast<DeferredAttributePersistenceProvider *>(me)->FlushAndScheduleNext(); },
this);
}
}

} // namespace app
} // namespace chip
82 changes: 82 additions & 0 deletions src/app/DeferredAttributePersistenceProvider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022 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.
*/
#pragma once

#include <app/AttributePersistenceProvider.h>
#include <lib/support/ScopedBuffer.h>
#include <lib/support/Span.h>

namespace chip {
namespace app {

class DeferredAttribute
{
public:
explicit DeferredAttribute(const ConcreteAttributePath & path) : mPath(path) {}

bool Matches(const ConcreteAttributePath & path) const { return mPath == path; }
bool IsArmed() const { return static_cast<bool>(mValue); }
System::Clock::Timestamp GetFlushTime() const { return mFlushTime; }

CHIP_ERROR PrepareWrite(System::Clock::Timestamp flushTime, const ByteSpan & value);
void Flush(AttributePersistenceProvider & persister);

private:
const ConcreteAttributePath mPath;
System::Clock::Timestamp mFlushTime;
Platform::ScopedMemoryBufferWithSize<uint8_t> mValue;
};

/**
* Decorator class for the AttributePersistenceProvider implementation that
* defers writes of selected attributes.
*
* This class is useful to increase the flash lifetime by reducing the number
* of writes of fast-changing attributes, such as CurrentLevel attribute of the
* LevelControl cluster.
*/
class DeferredAttributePersistenceProvider : public AttributePersistenceProvider
{
public:
DeferredAttributePersistenceProvider(AttributePersistenceProvider & persister,
const Span<DeferredAttribute> & deferredAttributes,
System::Clock::Milliseconds32 writeDelay) :
mPersister(persister),
mDeferredAttributes(deferredAttributes), mWriteDelay(writeDelay)
{}

/*
* If the written attribute is one of the deferred attributes specified in the constructor,
* postpone the write operation by the configured delay. If this attribute changes within the
* delay period, further postpone the operation so that the actual write happens once the
* attribute has remained constant for the write delay period.
*
* For other attributes, immediately pass the write operation to the decorated persister.
*/
CHIP_ERROR WriteValue(const ConcreteAttributePath & path, const ByteSpan & value) override;
CHIP_ERROR ReadValue(const ConcreteAttributePath & path, const EmberAfAttributeMetadata * metadata,
MutableByteSpan & value) override;

private:
void FlushAndScheduleNext();

AttributePersistenceProvider & mPersister;
const Span<DeferredAttribute> mDeferredAttributes;
const System::Clock::Milliseconds32 mWriteDelay;
};

} // namespace app
} // namespace chip
2 changes: 2 additions & 0 deletions src/app/server/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ class Server

Credentials::OperationalCertificateStore * GetOpCertStore() { return mOpCertStore; }

app::DefaultAttributePersistenceProvider & GetDefaultAttributePersister() { return mAttributePersister; }

/**
* This function send the ShutDown event before stopping
* the event loop.
Expand Down
3 changes: 1 addition & 2 deletions src/app/util/attribute-storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1300,8 +1300,7 @@ void emAfSaveAttributeToStorageIfNeeded(uint8_t * data, EndpointId endpoint, Clu
auto * attrStorage = app::GetAttributePersistenceProvider();
if (attrStorage)
{
attrStorage->WriteValue(app::ConcreteAttributePath(endpoint, clusterId, metadata->attributeId), metadata,
ByteSpan(data, dataSize));
attrStorage->WriteValue(app::ConcreteAttributePath(endpoint, clusterId, metadata->attributeId), ByteSpan(data, dataSize));
}
else
{
Expand Down

0 comments on commit 1300859

Please sign in to comment.