Add DomPrefixTree, a DOM path => value lookup structure

This is Document Property Editor work I've pulled out as I needed it in a few places, and the API may be generally useful. Real-world performance measurements will need to be done, if we have path lookup based bottlenecks we can consider adopting a contiguous memory approach with a sorted vector.

Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com>
This commit is contained in:
Nicholas Van Sickle
2022-02-10 15:20:04 -08:00
parent 3f32669883
commit 27c6388c3c
10 changed files with 616 additions and 1 deletions
@@ -0,0 +1,104 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Tests/DOM/DomFixtures.h>
#include <AzCore/DOM/DomPrefixTree.h>
#define REGISTER_TREE_BENCHMARK(BaseClass, Method) \
BENCHMARK_REGISTER_F(BaseClass, Method)->Args({10, 1})->Args({1000, 1})->Args({10, 5})->Args({1000, 5})
namespace AZ::Dom::Benchmark
{
class DomPrefixTreeBenchmark : public Tests::DomBenchmarkFixture
{
public:
void SetUpHarness() override
{
Tests::DomBenchmarkFixture::SetUpHarness();
m_registeredPaths = AZStd::make_unique<AZStd::vector<Path>>();
}
void TearDownHarness() override
{
m_registeredPaths.reset();
m_tree.Clear();
Tests::DomBenchmarkFixture::TearDownHarness();
}
void SetupTree(benchmark::State& state)
{
const size_t numPaths = aznumeric_cast<size_t>(state.range(0));
const size_t depth = aznumeric_cast<size_t>(state.range(1));
Path path("/root");
for (size_t i = 0; i < numPaths; ++i)
{
for (size_t c = 0; c < depth; ++c)
{
path.Push(i % 4);
m_tree.SetValue(path, AZStd::string::format("entry%zu", i));
m_registeredPaths->push_back(path);
}
for (size_t c = 0; c < depth; ++c)
{
path.Pop();
}
}
}
DomPrefixTree<AZStd::string> m_tree;
AZStd::unique_ptr<AZStd::vector<Path>> m_registeredPaths;
};
BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_ExactPath)(benchmark::State& state)
{
SetupTree(state);
for (auto _ : state)
{
for (const auto& pathToCheck : *m_registeredPaths)
{
benchmark::DoNotOptimize(m_tree.ValueAtPath(pathToCheck, PrefixTreeMatch::ExactPath));
}
}
state.SetItemsProcessed(m_registeredPaths->size() * state.iterations());
}
REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_ExactPath);
BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_InexactPath)(benchmark::State& state)
{
SetupTree(state);
for (auto _ : state)
{
for (const auto& pathToCheck : *m_registeredPaths)
{
benchmark::DoNotOptimize(m_tree.ValueAtPath(pathToCheck, PrefixTreeMatch::PathAndSubpaths));
}
}
state.SetItemsProcessed(m_registeredPaths->size() * state.iterations());
}
REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_InexactPath);
BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_VisitEntries)(benchmark::State& state)
{
SetupTree(state);
for (auto _ : state)
{
m_tree.VisitPath(Path(), PrefixTreeMatch::PathAndSubpaths, [](const Path& path, const AZStd::string& value)
{
benchmark::DoNotOptimize(path);
benchmark::DoNotOptimize(value);
});
}
state.SetItemsProcessed(m_registeredPaths->size() * state.iterations());
}
REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_VisitEntries);
}
#undef REGISTER_TREE_BENCHMARK
@@ -0,0 +1,170 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/DOM/DomPrefixTree.h>
#include <Tests/DOM/DomFixtures.h>
namespace AZ::Dom::Tests
{
using DomPrefixTreeTests = DomTestFixture;
TEST_F(DomPrefixTreeTests, GetAndSetRoot)
{
DomPrefixTree<AZStd::string> tree;
tree.SetValue(Path(), "root");
EXPECT_EQ(*tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath), "root");
}
TEST_F(DomPrefixTreeTests, GetExactPath)
{
DomPrefixTree<int> tree;
tree.SetValue(Path("/foo/0"), 0);
tree.SetValue(Path("/foo/1"), 42);
tree.SetValue(Path("/foo/foo"), 1);
tree.SetValue(Path("/foo/bar"), 2);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ExactPath), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ExactPath), 42);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/foo"), PrefixTreeMatch::ExactPath), 1);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath), 2);
EXPECT_EQ(tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath), nullptr);
EXPECT_EQ(tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath), nullptr);
EXPECT_EQ(tree.ValueAtPath(Path("/foo/0/subpath"), PrefixTreeMatch::ExactPath), nullptr);
}
TEST_F(DomPrefixTreeTests, GetSubpath)
{
DomPrefixTree<int> tree;
tree.SetValue(Path("/foo/0"), 0);
tree.SetValue(Path("/foo/1"), 42);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::SubpathsOnly), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::SubpathsOnly), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::SubpathsOnly), 42);
EXPECT_EQ(tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::SubpathsOnly), nullptr);
EXPECT_EQ(tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::SubpathsOnly), nullptr);
}
TEST_F(DomPrefixTreeTests, GetPathOrSubpath)
{
DomPrefixTree<int> tree;
tree.SetValue(Path("/foo/0"), 0);
tree.SetValue(Path("/foo/1"), 42);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::PathAndSubpaths), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::PathAndSubpaths), 0);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::PathAndSubpaths), 42);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::PathAndSubpaths), 42);
EXPECT_EQ(tree.ValueAtPath(Path(), PrefixTreeMatch::PathAndSubpaths), nullptr);
EXPECT_EQ(tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), nullptr);
EXPECT_EQ(tree.ValueAtPath(Path("/path/0"), PrefixTreeMatch::PathAndSubpaths), nullptr);
}
TEST_F(DomPrefixTreeTests, RemovePath)
{
DomPrefixTree<int> tree;
tree.SetValue(Path(), 20);
tree.SetValue(Path("/foo"), 40);
tree.SetValue(Path("/foo/0"), 80);
tree.EraseValue(Path("/foo"));
EXPECT_EQ(*tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), 20);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 80);
}
TEST_F(DomPrefixTreeTests, RemovePathAndChildren)
{
DomPrefixTree<int> tree;
tree.SetValue(Path(), 20);
tree.SetValue(Path("/foo"), 40);
tree.SetValue(Path("/foo/0"), 80);
tree.EraseValue(Path("/foo"), true);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), 20);
EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 20);
}
TEST_F(DomPrefixTreeTests, ClearTree)
{
DomPrefixTree<int> tree;
tree.SetValue(Path(), 20);
tree.SetValue(Path("/foo"), 40);
tree.Clear();
EXPECT_EQ(tree.ValueAtPathOrDefault(Path("/foo"), -10, PrefixTreeMatch::PathAndSubpaths), -10);
}
TEST_F(DomPrefixTreeTests, Visit)
{
DomPrefixTree<int> tree;
AZStd::vector<AZStd::pair<Path, int>> results;
auto visitorFn = [&results](const Path& path, int n)
{
results.emplace_back(path, n);
};
auto validateResult = [&results](const Path& path, int n)
{
for (const auto& pair : results)
{
if (pair.first == path)
{
return pair.second == n;
}
}
return false;
};
tree.SetValue(Path("/foo"), 99);
tree.SetValue(Path("/foo/0"), 0);
tree.SetValue(Path("/foo/1"), 42);
tree.SetValue(Path("/bar/bat"), 1);
tree.SetValue(Path("/bar/baz"), 2);
tree.VisitPath(Path("/bar"), PrefixTreeMatch::ExactPath, visitorFn);
EXPECT_EQ(results.size(), 0);
results.clear();
tree.VisitPath(Path("/foo/0"), PrefixTreeMatch::ExactPath, visitorFn);
EXPECT_EQ(results.size(), 1);
EXPECT_TRUE(validateResult(Path("/foo/0"), 0));
results.clear();
tree.VisitPath(Path("/foo/1"), PrefixTreeMatch::ExactPath, visitorFn);
EXPECT_EQ(results.size(), 1);
EXPECT_TRUE(validateResult(Path("/foo/1"), 42));
results.clear();
tree.VisitPath(Path("/foo"), PrefixTreeMatch::SubpathsOnly, visitorFn);
EXPECT_EQ(results.size(), 2);
EXPECT_TRUE(validateResult(Path("/foo/0"), 0));
EXPECT_TRUE(validateResult(Path("/foo/1"), 42));
results.clear();
tree.VisitPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths, visitorFn);
EXPECT_EQ(results.size(), 3);
EXPECT_TRUE(validateResult(Path("/foo"), 99));
EXPECT_TRUE(validateResult(Path("/foo/0"), 0));
EXPECT_TRUE(validateResult(Path("/foo/1"), 42));
results.clear();
}
}
@@ -228,6 +228,8 @@ set(FILES
DOM/DomPatchBenchmarks.cpp
DOM/DomValueTests.cpp
DOM/DomValueBenchmarks.cpp
DOM/DomPrefixTreeTests.cpp
DOM/DomPrefixTreeBenchmarks.cpp
)
# Prevent the following files from being grouped in UNITY builds