diff --git a/libuavcan/include/uavcan/internal/map.hpp b/libuavcan/include/uavcan/internal/map.hpp new file mode 100644 index 0000000000..1f1544a9ec --- /dev/null +++ b/libuavcan/include/uavcan/internal/map.hpp @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2014 Pavel Kirienko + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace uavcan +{ +/** + * Slow but memory efficient KV container. + * Type requirements: + * Both key and value must be copyable and default constructible. + * Key must implement a comparison operator. + * Key's default constructor must initialize the object into invalid state. + * Size of Key + Value + padding must not exceed MEM_POOL_BLOCK_SIZE. + */ +template +class Map +{ +#pragma pack(push, 1) + struct KVPair + { + Key key; + Value value; + KVPair() { } + KVPair(const Key& key, const Value& value) : key(key), value(value) { } + bool match(const Key& rhs) const { return rhs == key; } + }; + + struct KVGroup : LinkedListNode + { + enum { NUM_KV = (MEM_POOL_BLOCK_SIZE - sizeof(LinkedListNode)) / sizeof(KVPair) }; + KVPair kvs[NUM_KV]; + + KVGroup() + { + StaticAssert<(NUM_KV > 0)>::check(); + IsDynamicallyAllocatable::check(); + } + + KVPair* find(const Key& key) + { + for (int i = 0; i < NUM_KV; i++) + if (kvs[i].match(key)) + return kvs + i; + return NULL; + } + }; +#pragma pack(pop) + + LinkedListRoot list_; + IAllocator* const allocator_; + KVPair static_[NUM_STATIC_ENTRIES]; + + KVPair* find(const Key& key) + { + for (unsigned int i = 0; i < NUM_STATIC_ENTRIES; i++) + if (static_[i].match(key)) + return static_ + i; + + KVGroup* p = list_.get(); + while (p) + { + KVPair* const kv = p->find(key); + if (kv) + return kv; + p = p->getNextListNode(); + } + return NULL; + } + + void optimizeStorage() + { + while (true) + { + // Looking for first EMPTY static entry + KVPair* stat = NULL; + for (unsigned int i = 0; i < NUM_STATIC_ENTRIES; i++) + { + if (static_[i].match(Key())) + { + stat = static_ + i; + break; + } + } + if (stat == NULL) + break; + + // Looking for the first NON-EMPTY dynamic entry, erasing immediately + KVGroup* p = list_.get(); + KVPair dyn; + while (p) + { + bool stop = false; + for (int i = 0; i < KVGroup::NUM_KV; i++) + { + if (!p->kvs[i].match(Key())) // Non empty + { + dyn = p->kvs[i]; // Copy by value + p->kvs[i] = KVPair(); // Erase immediately + stop = true; + break; + } + } + if (stop) + break; + p = p->getNextListNode(); + } + if (dyn.match(Key())) + break; + + // Migrating + *stat = dyn; + } + } + + void compact() + { + KVGroup* p = list_.get(); + while (p) + { + KVGroup* const next = p->getNextListNode(); + bool remove_this = true; + for (int i = 0; i < KVGroup::NUM_KV; i++) + { + if (!p->kvs[i].match(Key())) + { + remove_this = false; + break; + } + } + if (remove_this) + { + list_.remove(p); + p->~KVGroup(); + allocator_->deallocate(p); + } + p = next; + } + } + + struct YesPredicate + { + bool operator()(const Key& k, const Value& v) const { (void)k; (void)v; return true; } + }; + + // This container is not copyable + Map(const Map&); + bool operator=(const Map&); + +public: + Map(IAllocator* allocator) + : allocator_(allocator) + { + assert(allocator); + assert(Key() == Key()); + } + + ~Map() { removeAll(); } + + Value* access(const Key& key) + { + assert(!(key == Key())); + KVPair* const kv = find(key); + return kv ? &kv->value : NULL; + } + + /// If entry with the same key already exists, it will be replaced + bool insert(const Key& key, const Value& value) + { + assert(!(key == Key())); + remove(key); + + KVPair* const kv = find(Key()); + if (kv) + { + *kv = KVPair(key, value); + return true; + } + + void* const praw = allocator_->allocate(sizeof(KVGroup)); + if (praw == NULL) + return false; + + KVGroup* const kvg = new (praw) KVGroup(); + assert(kvg); + kvg->kvs[0] = KVPair(key, value); + list_.insert(kvg); + return true; + } + + void remove(const Key& key) + { + assert(!(key == Key())); + KVPair* const kv = find(key); + if (kv) + { + *kv = KVPair(); + optimizeStorage(); + compact(); + } + } + + /** + * Remove entries where predicate returns true. + * Predicate prototype: + * bool (const Key& key, const Value& value) + */ + template + void removeWhere(Predicate predicate) + { + unsigned int num_removed = 0; + + for (unsigned int i = 0; i < NUM_STATIC_ENTRIES; i++) + { + if (!static_[i].match(Key())) + { + if (predicate(static_[i].key, static_[i].value)) + { + num_removed++; + static_[i] = KVPair(); + } + } + } + + KVGroup* p = list_.get(); + while (p) + { + for (int i = 0; i < KVGroup::NUM_KV; i++) + { + const KVPair* const kv = p->kvs + i; + if (!kv->match(Key())) + { + if (predicate(kv->key, kv->value)) + { + num_removed++; + p->kvs[i] = KVPair(); + } + } + } + p = p->getNextListNode(); + } + + if (num_removed > 0) + { + optimizeStorage(); + compact(); + } + } + + void removeAll() + { + removeWhere(YesPredicate()); + } + + /// For testing + unsigned int getNumStaticPairs() const + { + unsigned int num = 0; + for (unsigned int i = 0; i < NUM_STATIC_ENTRIES; i++) + if (!static_[i].match(Key())) + num++; + return num; + } + + /// For testing + unsigned int getNumDynamicPairs() const + { + unsigned int num = 0; + KVGroup* p = list_.get(); + while (p) + { + for (int i = 0; i < KVGroup::NUM_KV; i++) + { + const KVPair* const kv = p->kvs + i; + if (!kv->match(Key())) + num++; + } + p = p->getNextListNode(); + } + return num; + } +}; + +} diff --git a/libuavcan/test/map.cpp b/libuavcan/test/map.cpp new file mode 100644 index 0000000000..21c47900d5 --- /dev/null +++ b/libuavcan/test/map.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2014 Pavel Kirienko + */ + +#include +#include +#include +#include +#include + + +static std::string toString(long x) +{ + char buf[80]; + std::snprintf(buf, sizeof(buf), "%li", x); + return std::string(buf); +} + +static bool oddValuePredicate(const std::string& key, const std::string& value) +{ + EXPECT_FALSE(key.empty()); + EXPECT_FALSE(value.empty()); + const int num = atoi(value.c_str()); + return num & 1; +} + + +TEST(Map, Basic) +{ + using uavcan::Map; + + static const int POOL_BLOCKS = 3; + uavcan::PoolAllocator pool; + uavcan::PoolManager<2> poolmgr; + poolmgr.addPool(&pool); + + typedef Map MapType; + std::auto_ptr map(new MapType(&poolmgr)); + + // Empty + ASSERT_FALSE(map->access("hi")); + map->remove("foo"); + ASSERT_EQ(0, pool.getNumUsedBlocks()); + + // Static insertion + ASSERT_TRUE(map->insert("1", "a")); + ASSERT_TRUE(map->insert("2", "b")); + ASSERT_EQ(0, pool.getNumUsedBlocks()); + ASSERT_EQ(2, map->getNumStaticPairs()); + ASSERT_EQ(0, map->getNumDynamicPairs()); + + // Dynamic insertion + ASSERT_TRUE(map->insert("3", "c")); + ASSERT_EQ(1, pool.getNumUsedBlocks()); + + ASSERT_TRUE(map->insert("4", "d")); + ASSERT_EQ(1, pool.getNumUsedBlocks()); // Assuming that at least 2 items fit one block + ASSERT_EQ(2, map->getNumStaticPairs()); + ASSERT_EQ(2, map->getNumDynamicPairs()); + + // Making sure everything is here + ASSERT_EQ("a", *map->access("1")); + ASSERT_EQ("b", *map->access("2")); + ASSERT_EQ("c", *map->access("3")); + ASSERT_EQ("d", *map->access("4")); + ASSERT_FALSE(map->access("hi")); + + // Modifying existing entries + *map->access("1") = "A"; + *map->access("2") = "B"; + *map->access("3") = "C"; + *map->access("4") = "D"; + ASSERT_EQ("A", *map->access("1")); + ASSERT_EQ("B", *map->access("2")); + ASSERT_EQ("C", *map->access("3")); + ASSERT_EQ("D", *map->access("4")); + + // Removing one static + map->remove("1"); // One of dynamics now migrates to the static storage + map->remove("foo"); // There's no such thing anyway + ASSERT_EQ(1, pool.getNumUsedBlocks()); + ASSERT_EQ(2, map->getNumStaticPairs()); + ASSERT_EQ(1, map->getNumDynamicPairs()); + + ASSERT_FALSE(map->access("1")); + ASSERT_EQ("B", *map->access("2")); + ASSERT_EQ("C", *map->access("3")); + ASSERT_EQ("D", *map->access("4")); + + // Removing another static + map->remove("2"); + ASSERT_EQ(2, map->getNumStaticPairs()); + ASSERT_EQ(0, map->getNumDynamicPairs()); + ASSERT_EQ(0, pool.getNumUsedBlocks()); // No dynamic entries left + + ASSERT_FALSE(map->access("1")); + ASSERT_FALSE(map->access("2")); + ASSERT_EQ("C", *map->access("3")); + ASSERT_EQ("D", *map->access("4")); + + // Adding some new dynamics + unsigned int max_key_integer = 0; + for (int i = 0; i < 100; i++) + { + const std::string key = toString(i); + const std::string value = toString(i); + const bool res = map->insert(key, value); // Will override some from the above + if (!res) + { + ASSERT_LT(2, i); + break; + } + max_key_integer = i; + } + std::cout << "Max key/value: " << max_key_integer << std::endl; + ASSERT_LT(4, max_key_integer); + + // Making sure there is true OOM + ASSERT_EQ(0, pool.getNumFreeBlocks()); + ASSERT_FALSE(map->insert("nonexistent", "value")); + ASSERT_FALSE(map->access("nonexistent")); + ASSERT_FALSE(map->access("value")); + + // Removing odd values - nearly half of them + ASSERT_EQ(2, map->getNumStaticPairs()); + const unsigned int num_dynamics_old = map->getNumDynamicPairs(); + map->removeWhere(oddValuePredicate); + ASSERT_EQ(2, map->getNumStaticPairs()); + const unsigned int num_dynamics_new = map->getNumDynamicPairs(); + std::cout << "Num of dynamic pairs reduced from " << num_dynamics_old << " to " << num_dynamics_new << std::endl; + ASSERT_LT(num_dynamics_new, num_dynamics_old); + + // Making sure there's no odd values left + for (unsigned int kv_int = 0; kv_int <= max_key_integer; kv_int++) + { + const std::string* val = map->access(toString(kv_int)); + if (val) + { + ASSERT_FALSE(kv_int & 1); + } + else + { + ASSERT_TRUE(kv_int & 1); + } + } + + // Making sure the memory will be released + map.reset(); + ASSERT_EQ(0, pool.getNumUsedBlocks()); +}