Create a `SpanSearchValue` class that allows tree-searching without extra intermediate `if null/missing` checks. (#36754)
* Start adding the fluent tree object
* Start adding unit tests
* Add test file
* more testing
* update sizes hint and make use of things into CodegenDataModelProvider
* Restyle
* Fix some commments
* More merge fixes
* Remove some odd copy&paste comments
* Update src/lib/support/FluentTreeObject.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/lib/support/FluentTreeObject.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Fix up comments a bit
* Simplify FluentTreeObject a bit
* Extra wrapper not needed
* Cleaned up comments
* Restyled by clang-format
* Sed rename FluentTreeObject to SpanSearchValue
* Also rename files
* Restyle
* Very slight reduction in complexity that reduces extra flash usage by a few bytes
* Another minor source code size decrease
* Even slightly smaller code
* Restyle
* Fix comment typo
* make cc32xx compile optimized for size by default, so we can treak flash usage as such
* Restyled by gn
* Update src/data-model-providers/codegen/CodegenDataModelProvider.cpp
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/lib/support/SpanSearchValue.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/lib/support/SpanSearchValue.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/lib/support/SpanSearchValue.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Rename tree to searchable
* Fix comment
* Restyled by clang-format
---------
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/data-model-providers/codegen/CodegenDataModelProvider.cpp b/src/data-model-providers/codegen/CodegenDataModelProvider.cpp
index 95bac8f..b599ba1 100644
--- a/src/data-model-providers/codegen/CodegenDataModelProvider.cpp
+++ b/src/data-model-providers/codegen/CodegenDataModelProvider.cpp
@@ -35,6 +35,7 @@
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/CodeUtils.h>
+#include <lib/support/SpanSearchValue.h>
#include <optional>
#include <variant>
@@ -106,6 +107,19 @@
namespace {
+/// Search by device type within a span of EmberAfDeviceType (finds the device type that matches the given
+/// DataModel::DeviceTypeEntry)
+struct ByDeviceType
+{
+ using Key = DataModel::DeviceTypeEntry;
+ using Type = const EmberAfDeviceType;
+ static Span<Type> GetSpan(Span<const EmberAfDeviceType> & data) { return data; }
+ static bool HasKey(const Key & id, const Type & instance)
+ {
+ return (instance.deviceId == id.deviceTypeId) && (instance.deviceVersion == id.deviceTypeRevision);
+ }
+};
+
const CommandId * AcceptedCommands(const EmberAfCluster & cluster)
{
return cluster.acceptedCommandList;
@@ -268,51 +282,17 @@
// to a common type is probably better. Need to figure out dependencies since
// this would make ember return datamodel-provider types.
// See: https://github.com/project-chip/connectedhomeip/issues/35889
-DataModel::DeviceTypeEntry DeviceTypeEntryFromEmber(const EmberAfDeviceType & other)
+std::optional<DataModel::DeviceTypeEntry> DeviceTypeEntryFromEmber(const EmberAfDeviceType * other)
{
- DataModel::DeviceTypeEntry entry;
-
- entry.deviceTypeId = other.deviceId;
- entry.deviceTypeRevision = other.deviceVersion;
-
- return entry;
-}
-
-// Explicitly compare for identical entries. note that types are different,
-// so you must do `a == b` and the `b == a` will not work.
-bool operator==(const DataModel::DeviceTypeEntry & a, const EmberAfDeviceType & b)
-{
- return (a.deviceTypeId == b.deviceId) && (a.deviceTypeRevision == b.deviceVersion);
-}
-
-/// Find the `index` where one of the following holds:
-/// - types[index - 1] == previous OR
-/// - index == types.size() // i.e. not found or there is no next
-///
-/// hintWherePreviousMayBe represents a search hint where previous may exist.
-unsigned FindNextDeviceTypeIndex(Span<const EmberAfDeviceType> types, const DataModel::DeviceTypeEntry & previous,
- unsigned hintWherePreviousMayBe)
-{
- if (hintWherePreviousMayBe < types.size())
+ if (other == nullptr)
{
- // this is a valid hint ... see if we are lucky
- if (previous == types[hintWherePreviousMayBe])
- {
- return hintWherePreviousMayBe + 1; // return the next index
- }
+ return std::nullopt;
}
- // hint was not useful. We have to do a full search
- for (unsigned idx = 0; idx < types.size(); idx++)
- {
- if (previous == types[idx])
- {
- return idx + 1;
- }
- }
-
- // cast should be safe as we know we do not have that many types
- return static_cast<unsigned>(types.size());
+ return DataModel::DeviceTypeEntry{
+ .deviceTypeId = other->deviceId,
+ .deviceTypeRevision = other->deviceVersion,
+ };
}
const ConcreteCommandPath kInvalidCommandPath(kInvalidEndpointId, kInvalidClusterId, kInvalidCommandId);
@@ -894,15 +874,9 @@
CHIP_ERROR err = CHIP_NO_ERROR;
Span<const EmberAfDeviceType> deviceTypes = emberAfDeviceTypeListFromEndpointIndex(*endpoint_index, err);
+ SpanSearchValue<chip::Span<const EmberAfDeviceType>> searchable(&deviceTypes);
- if (deviceTypes.empty())
- {
- return std::nullopt;
- }
-
- // we start at the beginning
- mDeviceTypeIterationHint = 0;
- return DeviceTypeEntryFromEmber(deviceTypes[0]);
+ return DeviceTypeEntryFromEmber(searchable.First<ByDeviceType>(mDeviceTypeIterationHint).Value());
}
std::optional<DataModel::DeviceTypeEntry> CodegenDataModelProvider::NextDeviceType(EndpointId endpoint,
@@ -917,18 +891,11 @@
return std::nullopt;
}
- CHIP_ERROR err = CHIP_NO_ERROR;
- Span<const EmberAfDeviceType> deviceTypes = emberAfDeviceTypeListFromEndpointIndex(*endpoint_index, err);
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ chip::Span<const EmberAfDeviceType> deviceTypes = emberAfDeviceTypeListFromEndpointIndex(*endpoint_index, err);
+ SpanSearchValue<chip::Span<const EmberAfDeviceType>> searchable(&deviceTypes);
- unsigned idx = FindNextDeviceTypeIndex(deviceTypes, previous, mDeviceTypeIterationHint);
-
- if (idx >= deviceTypes.size())
- {
- return std::nullopt;
- }
-
- mDeviceTypeIterationHint = idx;
- return DeviceTypeEntryFromEmber(deviceTypes[idx]);
+ return DeviceTypeEntryFromEmber(searchable.Next<ByDeviceType>(previous, mDeviceTypeIterationHint).Value());
}
std::optional<DataModel::Provider::SemanticTag> CodegenDataModelProvider::GetFirstSemanticTag(EndpointId endpoint)
diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn
index a8daba8..29a381d 100644
--- a/src/lib/support/BUILD.gn
+++ b/src/lib/support/BUILD.gn
@@ -236,6 +236,7 @@
"ScopedBuffer.h",
"SetupDiscriminator.h",
"SortUtils.h",
+ "SpanSearchValue.h",
"StateMachine.h",
"StringBuilder.cpp",
"StringBuilder.h",
diff --git a/src/lib/support/SpanSearchValue.h b/src/lib/support/SpanSearchValue.h
new file mode 100644
index 0000000..e11a9e6
--- /dev/null
+++ b/src/lib/support/SpanSearchValue.h
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2024 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 <cstddef>
+#include <lib/support/Span.h>
+
+#include <optional>
+
+namespace chip {
+
+/// represents a wrapper around a type `T` that contains internal
+/// `Span<...>` values of other sub-types. It allows searching within the container sub-spans
+/// to create new containers.
+///
+/// The use case is that we very often search within nested containers, like "find-endpoint" + "find-cluster" + "find-attribute"
+/// and we generally only care about "does the last element exist or not"
+///
+/// A typical example of the way this class is used looks like this:
+///
+/// SpanSearchValue container(somePointer);
+///
+/// const AcceptedCommandData * value =
+/// container
+/// .Find<ByEndpoint>(path.mEndpointId, mEndpointIndexHint)
+/// .Find<ByServerCluster>(path.mClusterId, mServerClusterHint)
+/// .Find<ByAcceptedCommand>(path.mCommandId, mAcceptedCommandHint)
+/// .Value();
+///
+/// Where a `ByFoo` structure looks like:
+///
+/// struct ByFoo {
+/// using Key = int; // the KEY inside a type
+/// using Type = SomeValueType; // The type that is indexed by `Key`
+///
+/// /// Allows getting the "Span of Type" from an underlying structure.
+/// /// A `SpanSearchValue<Foo>` will require a `GetSpan(Foo&)`
+/// static Span<Type> GetSpan(ContainerType & data) { /* return ... */ }
+///
+/// /// Checks that the `Type` value has the given `Key` or not
+/// static bool HasKey(const Key & id, const Type & instance) { /* return "instance has key id" */ }
+/// }
+///
+/// Where we define:
+/// - how to get a "span of sub-elements" for an object (`GetSpan`)
+/// - how to determine if a given sub-element has the "correct key"
+template <typename T>
+class SpanSearchValue
+{
+public:
+ SpanSearchValue() : mValue(nullptr) {}
+ SpanSearchValue(std::nullptr_t) : mValue(nullptr) {}
+ explicit SpanSearchValue(T * value) : mValue(value) {}
+
+ /// Returns nullptr if such an element does not exist or non-null valid value if the element exists
+ T * Value() const { return mValue; }
+
+ /// Gets the first element of `TYPE::Type`
+ template <typename TYPE>
+ SpanSearchValue<typename TYPE::Type> First(unsigned & indexHint)
+ {
+ // if no value, searching more also yields no value
+ VerifyOrReturnValue(mValue != nullptr, nullptr);
+
+ Span<typename TYPE::Type> value_span = TYPE::GetSpan(*mValue);
+ VerifyOrReturnValue(!value_span.empty(), nullptr);
+
+ // found it, save the hint
+ indexHint = 0;
+ return SpanSearchValue<typename TYPE::Type>(&value_span[0]);
+ }
+
+ /// Find the value corresponding to `key`
+ template <typename TYPE>
+ SpanSearchValue<typename TYPE::Type> Find(typename TYPE::Key key, unsigned & indexHint)
+ {
+ VerifyOrReturnValue(mValue != nullptr, nullptr);
+
+ Span<typename TYPE::Type> value_span = TYPE::GetSpan(*mValue);
+
+ if (!FindIndexUsingHint(key, value_span, indexHint, TYPE::HasKey))
+ {
+ return nullptr;
+ }
+
+ return SpanSearchValue<typename TYPE::Type>(&value_span[indexHint]);
+ }
+
+ /// Finds the value that occurs after `key` in the underlying collection.
+ template <typename TYPE>
+ SpanSearchValue<typename TYPE::Type> Next(typename TYPE::Key key, unsigned & indexHint)
+ {
+ VerifyOrReturnValue(mValue != nullptr, nullptr);
+
+ Span<typename TYPE::Type> value_span = TYPE::GetSpan(*mValue);
+
+ if (!FindIndexUsingHint(key, value_span, indexHint, TYPE::HasKey))
+ {
+ return nullptr;
+ }
+
+ VerifyOrReturnValue((indexHint + 1) < value_span.size(), nullptr);
+
+ indexHint++;
+ return SpanSearchValue<typename TYPE::Type>(&value_span[indexHint]);
+ }
+
+private:
+ T * mValue = nullptr; // underlying value, NULL if such a value does not exist
+
+ /// Search for the index where `needle` is located inside `haystack`
+ ///
+ /// using `haystackValueMatchesNeedle` to find if a given haystack value matches the given needle
+ ///
+ /// `in_out_hint` contains a start search point at the start and will contain the found index
+ /// location (if found) at the end.
+ ///
+ /// Returns true on success (index found) false on failure (index not found). If returning
+ /// false, the value of `in_out_hint` is unchanged
+ template <typename N, typename H>
+ static bool FindIndexUsingHint(const N & needle, Span<H> haystack, unsigned & in_out_hint,
+ bool (*haystackValueMatchesNeedle)(const N &, const typename std::remove_const<H>::type &))
+ {
+ // search starts at `hint` rather than 0
+ const unsigned haystackSize = static_cast<unsigned>(haystack.size());
+ unsigned checkIndex = (in_out_hint < haystackSize) ? in_out_hint : 0;
+
+ for (unsigned i = 0; i < haystackSize; i++, checkIndex++)
+ {
+ if (haystackValueMatchesNeedle(needle, haystack[checkIndex % haystackSize]))
+ {
+ in_out_hint = checkIndex % haystackSize;
+ return true;
+ }
+ }
+
+ return false;
+ }
+};
+
+} // namespace chip
diff --git a/src/lib/support/tests/BUILD.gn b/src/lib/support/tests/BUILD.gn
index c6bfddb..d814f70 100644
--- a/src/lib/support/tests/BUILD.gn
+++ b/src/lib/support/tests/BUILD.gn
@@ -56,6 +56,7 @@
"TestScopedBuffer.cpp",
"TestSorting.cpp",
"TestSpan.cpp",
+ "TestSpanSearchValue.cpp",
"TestStateMachine.cpp",
"TestStaticSupportSmartPtr.cpp",
"TestStringBuilder.cpp",
diff --git a/src/lib/support/tests/TestSpanSearchValue.cpp b/src/lib/support/tests/TestSpanSearchValue.cpp
new file mode 100644
index 0000000..469c590
--- /dev/null
+++ b/src/lib/support/tests/TestSpanSearchValue.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2024 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 "pw_unit_test/framework.h"
+#include <pw_unit_test/framework.h>
+
+#include <lib/core/DataModelTypes.h>
+#include <lib/core/StringBuilderAdapters.h>
+#include <lib/support/SpanSearchValue.h>
+
+namespace {
+
+using namespace chip;
+
+struct ClusterData
+{
+ const ClusterId id;
+ const char * name;
+};
+
+struct EndpointData
+{
+ const EndpointId id;
+
+ const char * name;
+ Span<const ClusterData> serverClusters;
+ Span<const ClusterData> clientClusters;
+};
+
+struct EndpointItemsWrapper
+{
+ Span<const EndpointData> data;
+
+ template <size_t N>
+ EndpointItemsWrapper(EndpointData (&d)[N]) : data(d)
+ {}
+};
+
+const ClusterData gClusterList1[] = {
+ {
+ .id = 100,
+ .name = "one hundred",
+ },
+ {
+ .id = 200,
+ .name = "two hundred",
+ },
+};
+
+const ClusterData gClusterList2[] = {
+ {
+ .id = 1,
+ .name = "just one",
+ },
+};
+
+EndpointData gEndpointDataItems[] = {
+ {
+ .id = 123,
+ .name = "foo",
+ .serverClusters = Span<const ClusterData>(gClusterList1),
+ .clientClusters = Span<const ClusterData>(gClusterList2),
+ },
+ {
+ .id = 456,
+ .name = "bar",
+ .serverClusters = Span<const ClusterData>(gClusterList2),
+ .clientClusters = Span<const ClusterData>(),
+ },
+ {
+ .id = 1000,
+ .name = "Empty",
+ .serverClusters = Span<const ClusterData>(),
+ .clientClusters = Span<const ClusterData>(),
+ },
+};
+
+/// search index definitions
+struct ByEndpoint
+{
+ using Key = EndpointId;
+ using Type = const EndpointData;
+ static Span<Type> GetSpan(EndpointItemsWrapper & data) { return data.data; }
+ static bool HasKey(const Key & id, const Type & instance) { return instance.id == id; }
+};
+
+struct ByServerCluster
+{
+ using Key = ClusterId;
+ using Type = const ClusterData;
+ static Span<Type> GetSpan(const EndpointData & data) { return data.serverClusters; }
+ static bool HasKey(const Key & id, const Type & instance) { return instance.id == id; }
+};
+
+struct ByClientCluster
+{
+ using Key = ClusterId;
+ using Type = const ClusterData;
+ static Span<Type> GetSpan(const EndpointData & data) { return data.clientClusters; }
+ static bool HasKey(const Key & id, const Type & instance) { return instance.id == id; }
+};
+
+} // namespace
+
+TEST(TestSpanSearchValue, TestFunctionality)
+{
+ EndpointItemsWrapper wrapper(gEndpointDataItems);
+ SpanSearchValue<EndpointItemsWrapper> tree(&wrapper);
+
+ EXPECT_EQ(tree.Value(), &wrapper); // value getting to start matches
+
+ // search first items
+ {
+ unsigned hint1 = 0;
+ auto ep = tree.First<ByEndpoint>(hint1);
+
+ unsigned hint2 = 0;
+ auto cl = ep.First<ByServerCluster>(hint2);
+
+ ASSERT_NE(cl.Value(), nullptr);
+ EXPECT_EQ(cl.Value()->id, 100u);
+ EXPECT_STREQ(cl.Value()->name, "one hundred");
+ }
+
+ // one level search, with hint
+ {
+ unsigned hint = 0;
+ ASSERT_NE(tree.Find<ByEndpoint>(123, hint).Value(), nullptr);
+ ASSERT_STREQ(tree.Find<ByEndpoint>(123, hint).Value()->name, "foo");
+ EXPECT_EQ(hint, 0u);
+
+ ASSERT_NE(tree.Find<ByEndpoint>(456, hint).Value(), nullptr);
+ EXPECT_EQ(hint, 1u);
+ EXPECT_STREQ(tree.Find<ByEndpoint>(456, hint).Value()->name, "bar");
+ EXPECT_EQ(hint, 1u);
+
+ // hint is ignored here
+ EXPECT_STREQ(tree.Find<ByEndpoint>(123, hint).Value()->name, "foo");
+ EXPECT_EQ(hint, 0u);
+
+ EXPECT_STREQ(tree.Find<ByEndpoint>(1000, hint).Value()->name, "Empty");
+ EXPECT_EQ(hint, 2u);
+
+ // Invalid searches
+ EXPECT_EQ(tree.Find<ByEndpoint>(12345, hint).Value(), nullptr);
+ EXPECT_EQ(tree.Find<ByEndpoint>(0, hint).Value(), nullptr);
+ }
+
+ // searches for "next"
+ {
+ unsigned hint = 0;
+ auto next = tree.Next<ByEndpoint>(123, hint);
+
+ ASSERT_NE(next.Value(), nullptr);
+ EXPECT_EQ(hint, 1u);
+ EXPECT_EQ(next.Value()->id, 456u);
+ EXPECT_STREQ(next.Value()->name, "bar");
+
+ next = tree.Next<ByEndpoint>(456, hint);
+ ASSERT_NE(next.Value(), nullptr);
+ EXPECT_EQ(hint, 2u);
+ EXPECT_EQ(next.Value()->id, 1000u);
+ EXPECT_STREQ(next.Value()->name, "Empty");
+
+ /// search at the end
+ next = tree.Next<ByEndpoint>(1000, hint);
+ EXPECT_EQ(next.Value(), nullptr);
+
+ // null value preserves the failure
+ unsigned clusterHint = 0;
+ auto sub_item = next.Find<ByServerCluster>(123, clusterHint);
+ EXPECT_EQ(sub_item.Value(), nullptr);
+ }
+}
diff --git a/src/platform/cc32xx/args.gni b/src/platform/cc32xx/args.gni
index 288aa5a..d819c03 100755
--- a/src/platform/cc32xx/args.gni
+++ b/src/platform/cc32xx/args.gni
@@ -34,3 +34,6 @@
chip_inet_config_enable_dns_resolver = false
chip_build_tests = false
+
+# small binaries even in CI
+optimize_debug_level = "s"