Files
o3de/Code/Framework/AtomCore/Tests/InstanceDatabase.cpp
T
Tommy Walton b79c2f385d Adding a temporary Orphan function to the InstanceDatabase (#4297)
* Adding a temporary Orphan function to the InstanceDatabase, which will remove an instance from the database so it will not be found using Find or FindOrCreate. The instance will still persist until its use-count drops to 0, at which point it will be deleted. This is to enable the model asset to remove existing buffer/modellod/model instances and replace them with new instances that have the up to date data. Added unit tests for testing.

Signed-off-by: amzn-tommy <waltont@amazon.com>

* Fix an incorrect ceil and update ParallelInstance test cases for readability

Signed-off-by: amzn-tommy <waltont@amazon.com>
2021-09-29 11:08:05 -07:00

512 lines
19 KiB
C++

/*
* 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 <AtomCore/Instance/InstanceDatabase.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Memory/PoolAllocator.h>
#include <AzCore/std/parallel/conditional_variable.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Debug/Timer.h>
using namespace AZ;
using namespace AZ::Data;
namespace UnitTest
{
static const InstanceId s_instanceId0{ Uuid("{5B29FE2B-6B41-48C9-826A-C723951B0560}") };
static const InstanceId s_instanceId1{ Uuid("{BD354AE5-B5D5-402A-A12E-BE3C96F6522B}") };
static const InstanceId s_instanceId2{ Uuid("{EE99215B-7AB4-4757-B8AF-F78BD4903AC4}") };
static const InstanceId s_instanceId3{ Uuid("{D9CDAB04-D206-431E-BDC0-1DD615D56197}") };
static const AssetId s_assetId0{ Uuid("{5B29FE2B-6B41-48C9-826A-C723951B0560}") };
static const AssetId s_assetId1{ Uuid("{BD354AE5-B5D5-402A-A12E-BE3C96F6522B}") };
static const AssetId s_assetId2{ Uuid("{EE99215B-7AB4-4757-B8AF-F78BD4903AC4}") };
static const AssetId s_assetId3{ Uuid("{D9CDAB04-D206-431E-BDC0-1DD615D56197}") };
// test asset type
class TestAssetType : public AssetData
{
public:
AZ_CLASS_ALLOCATOR(TestAssetType, AZ::SystemAllocator, 0);
AZ_RTTI(TestAssetType, "{73D60606-BDE5-44F9-9420-5649FE7BA5B8}", AssetData);
TestAssetType()
{
m_status = AssetStatus::Ready;
}
};
class TestInstanceA : public InstanceData
{
public:
AZ_INSTANCE_DATA(TestInstanceA, "{65CBF1C8-F65F-4A84-8A11-B510BC435DB0}");
AZ_CLASS_ALLOCATOR(TestInstanceA, AZ::SystemAllocator, 0);
TestInstanceA(TestAssetType* asset)
: m_asset{ asset, AZ::Data::AssetLoadBehavior::Default }
{
}
Asset<TestAssetType> m_asset;
};
class TestInstanceB : public InstanceData
{
public:
AZ_INSTANCE_DATA(TestInstanceB, "{4ED0A8BF-7800-44B2-AC73-2CB759C61C37}");
AZ_CLASS_ALLOCATOR(TestInstanceB, AZ::SystemAllocator, 0);
TestInstanceB(TestAssetType* asset)
: m_asset{ asset, AZ::Data::AssetLoadBehavior::Default }
{
}
~TestInstanceB()
{
if (m_onDeleteCallback)
{
m_onDeleteCallback();
}
}
Asset<TestAssetType> m_asset;
AZStd::function<void()> m_onDeleteCallback;
};
// test asset handler
template<typename AssetDataT>
class MyAssetHandler : public AssetHandler
{
public:
AZ_CLASS_ALLOCATOR(MyAssetHandler, AZ::SystemAllocator, 0);
AssetPtr CreateAsset(const AssetId& id, const AssetType& type) override
{
(void)id;
EXPECT_TRUE(type == AzTypeInfo<AssetDataT>::Uuid());
if (type == AzTypeInfo<AssetDataT>::Uuid())
{
return aznew AssetDataT();
}
return nullptr;
}
LoadResult LoadAssetData(const Asset<AssetData>&, AZStd::shared_ptr<AssetDataStream>, const AZ::Data::AssetFilterCB&) override
{
return LoadResult::Error;
}
void DestroyAsset(AssetPtr ptr) override
{
EXPECT_TRUE(ptr->GetType() == AzTypeInfo<AssetDataT>::Uuid());
delete ptr;
}
void GetHandledAssetTypes(AZStd::vector<AssetType>& assetTypes) override
{
assetTypes.push_back(AzTypeInfo<AssetDataT>::Uuid());
}
};
class InstanceDatabaseTest : public AllocatorsFixture
{
protected:
MyAssetHandler<TestAssetType>* m_assetHandler;
public:
void SetUp() override
{
AllocatorsFixture::SetUp();
AllocatorInstance<PoolAllocator>::Create();
AllocatorInstance<ThreadPoolAllocator>::Create();
// create the asset database
{
AssetManager::Descriptor desc;
AssetManager::Create(desc);
}
// create the instance database
{
InstanceHandler<TestInstanceA> instanceHandler;
instanceHandler.m_createFunction = [](AssetData* assetData)
{
EXPECT_TRUE(azrtti_istypeof<TestAssetType>(assetData));
return aznew TestInstanceA(static_cast<TestAssetType*>(assetData));
};
InstanceDatabase<TestInstanceA>::Create(azrtti_typeid<TestAssetType>(), instanceHandler);
}
// create and register an asset handler
m_assetHandler = aznew MyAssetHandler<TestAssetType>;
AssetManager::Instance().RegisterHandler(m_assetHandler, AzTypeInfo<TestAssetType>::Uuid());
}
void TearDown() override
{
// destroy the database
AssetManager::Destroy();
InstanceDatabase<TestInstanceA>::Destroy();
AllocatorInstance<ThreadPoolAllocator>::Destroy();
AllocatorInstance<PoolAllocator>::Destroy();
AllocatorsFixture::TearDown();
}
};
TEST_F(InstanceDatabaseTest, InstanceCreate)
{
auto& assetManager = AssetManager::Instance();
auto& instanceDatabase = InstanceDatabase<TestInstanceA>::Instance();
Asset<TestAssetType> someAsset = assetManager.CreateAsset<TestAssetType>(s_assetId0, AZ::Data::AssetLoadBehavior::Default);
Instance<TestInstanceA> instance = instanceDatabase.Find(s_instanceId0);
EXPECT_EQ(instance, nullptr);
instance = instanceDatabase.FindOrCreate(s_instanceId0, someAsset);
EXPECT_NE(instance, nullptr);
Instance<TestInstanceA> instance2 = instanceDatabase.FindOrCreate(s_instanceId0, someAsset);
EXPECT_EQ(instance, instance2);
Instance<TestInstanceA> instance3 = instanceDatabase.Find(s_instanceId0);
EXPECT_EQ(instance, instance3);
}
TEST_F(InstanceDatabaseTest, InstanceOrphan)
{
auto& assetManager = AssetManager::Instance();
auto& instanceDatabase = InstanceDatabase<TestInstanceA>::Instance();
Asset<TestAssetType> someAsset = assetManager.CreateAsset<TestAssetType>(s_assetId0, AZ::Data::AssetLoadBehavior::Default);
Instance<TestInstanceA> orphanedInstance = instanceDatabase.FindOrCreate(s_instanceId0, someAsset);
EXPECT_NE(orphanedInstance, nullptr);
instanceDatabase.TEMPOrphan(s_instanceId0);
// After orphan, the instance should not be found in the database, but it should still be valid
EXPECT_EQ(instanceDatabase.Find(s_instanceId0), nullptr);
EXPECT_NE(orphanedInstance, nullptr);
instanceDatabase.TEMPOrphan(s_instanceId0);
// Orphaning twice should be a no-op
EXPECT_EQ(instanceDatabase.Find(s_instanceId0), nullptr);
EXPECT_NE(orphanedInstance, nullptr);
Instance<TestInstanceA> instance2 = instanceDatabase.FindOrCreate(s_instanceId0, someAsset);
// Creating another instance with the same id should return a different instance than the one that was orphaned
EXPECT_NE(orphanedInstance, instance2);
}
enum class ParallelInstanceTestCases
{
Create,
CreateAndDeferRemoval,
CreateAndOrphan,
CreateDeferRemovalAndOrphan
};
enum class ParralleInstanceCurrentAction
{
Create,
DeferredRemoval,
Orphan
};
ParralleInstanceCurrentAction ParallelInstanceGetCurrentAction(ParallelInstanceTestCases testCase)
{
switch (testCase)
{
case ParallelInstanceTestCases::CreateAndDeferRemoval:
switch (rand() % 2)
{
case 0: return ParralleInstanceCurrentAction::Create;
case 1: return ParralleInstanceCurrentAction::DeferredRemoval;
}
case ParallelInstanceTestCases::CreateAndOrphan:
switch (rand() % 2)
{
case 0: return ParralleInstanceCurrentAction::Create;
case 1: return ParralleInstanceCurrentAction::Orphan;
}
case ParallelInstanceTestCases::CreateDeferRemovalAndOrphan:
switch (rand() % 3)
{
case 0: return ParralleInstanceCurrentAction::Create;
case 1: return ParralleInstanceCurrentAction::DeferredRemoval;
case 2: return ParralleInstanceCurrentAction::Orphan;
}
case ParallelInstanceTestCases::Create:
default:
return ParralleInstanceCurrentAction::Create;
}
}
void ParallelInstanceCreateHelper(size_t threadCountMax, size_t assetIdCount, float durationSeconds, ParallelInstanceTestCases testCase)
{
printf("Testing threads=%zu assetIds=%zu ... ", threadCountMax, assetIdCount);
AZ::Debug::Timer timer;
timer.Stamp();
auto& assetManager = AssetManager::Instance();
auto& instanceManager = InstanceDatabase<TestInstanceA>::Instance();
AZStd::vector<Uuid> guids;
AZStd::vector<Data::Instance<Data::InstanceData>> instances;
AZStd::vector<Asset<TestAssetType>> assets;
for (size_t i = 0; i < assetIdCount; ++i)
{
Uuid guid = Uuid::CreateRandom();
guids.emplace_back(guid);
instances.emplace_back(nullptr);
// Pre-create asset so we don't attempt to load it from the catalog.
assets.emplace_back(assetManager.CreateAsset<TestAssetType>(guid, AZ::Data::AssetLoadBehavior::Default));
}
AZStd::vector<AZStd::thread> threads;
AZStd::mutex mutex;
AZStd::mutex referenceTableMutex;
AZStd::atomic<int> threadCount((int)threadCountMax);
AZStd::condition_variable cv;
AZStd::atomic_bool keepDispatching(true);
auto dispatch = [&keepDispatching]()
{
while (keepDispatching)
{
AssetManager::Instance().DispatchEvents();
}
};
srand(0);
AZStd::thread dispatchThread(dispatch);
for (size_t i = 0; i < threadCountMax; ++i)
{
threads.emplace_back(
[&instanceManager, &threadCount, &cv, &guids, &instances, &assets, &durationSeconds, &testCase, &referenceTableMutex]()
{
AZ::Debug::Timer timer;
timer.Stamp();
bool deferRemoval = testCase == ParallelInstanceTestCases::CreateAndDeferRemoval ||
testCase == ParallelInstanceTestCases::CreateDeferRemovalAndOrphan
? true : false;
while (timer.GetDeltaTimeInSeconds() < durationSeconds)
{
const size_t index = rand() % guids.size();
const Uuid uuid = guids[index];
const InstanceId instanceId{ uuid };
const AssetId assetId{ uuid };
ParralleInstanceCurrentAction currentAction = ParallelInstanceGetCurrentAction(testCase);
if (currentAction == ParralleInstanceCurrentAction::Orphan)
{
// Orphan the instance, but don't decrease its refcount
instanceManager.TEMPOrphan(instanceId);
}
else if (currentAction == ParralleInstanceCurrentAction::DeferredRemoval)
{
// Drop the refcount to zero so the instance will be released
referenceTableMutex.lock();
instances[index] = nullptr;
referenceTableMutex.unlock();
}
else
{
// Otherwise, add a new instance
Instance<TestInstanceA> instance = instanceManager.FindOrCreate(instanceId, assets[index]);
EXPECT_NE(instance, nullptr);
EXPECT_EQ(instance->GetId(), instanceId);
EXPECT_EQ(instance->m_asset, assets[index]);
if (deferRemoval)
{
// Keep a reference to the instance alive so it can be removed later
referenceTableMutex.lock();
instances[index] = instance;
referenceTableMutex.unlock();
}
}
}
threadCount--;
cv.notify_one();
});
}
bool timedOut = false;
// Used to detect a deadlock. If we wait for more than 10 seconds, it's likely a deadlock has occurred
while (threadCount > 0 && !timedOut)
{
size_t durationSecondsRoundedUp = static_cast<size_t>(std::ceil(durationSeconds));
AZStd::unique_lock<AZStd::mutex> lock(mutex);
timedOut =
(AZStd::cv_status::timeout ==
cv.wait_until(lock, AZStd::chrono::system_clock::now() + AZStd::chrono::seconds(durationSecondsRoundedUp * 2)));
}
EXPECT_TRUE(threadCount == 0) << "One or more threads appear to be deadlocked at " << timer.GetDeltaTimeInSeconds() << " seconds";
for (auto& thread : threads)
{
thread.join();
}
keepDispatching = false;
dispatchThread.join();
printf("Took %f seconds\n", timer.GetDeltaTimeInSeconds());
}
void ParallelCreateTest(ParallelInstanceTestCases testCase)
{
// This is the original test scenario from when InstanceDatabase was first implemented
// threads, AssetIds, seconds
ParallelInstanceCreateHelper(8, 100, 5, testCase);
// This value is checked in as 1 so this test doesn't take too much time, but can be increased locally to soak the test.
const size_t attempts = 1;
for (size_t i = 0; i < attempts; ++i)
{
printf("Attempt %zu of %zu... \n", i, attempts);
// The idea behind this series of tests is that there are two threads sharing one Instance, and both threads try to
// create or release that instance at the same time.
// At the time, this set of scenarios has something like a 10% failure rate.
const float duration = 2.0f;
// threads, AssetIds, seconds
ParallelInstanceCreateHelper(2, 1, duration, testCase);
ParallelInstanceCreateHelper(4, 1, duration, testCase);
ParallelInstanceCreateHelper(8, 1, duration, testCase);
}
for (size_t i = 0; i < attempts; ++i)
{
printf("Attempt %zu of %zu... \n", i, attempts);
// Here we try a bunch of different threadCount:assetCount ratios to be thorough
const float duration = 2.0f;
// threads, AssetIds, seconds
ParallelInstanceCreateHelper(2, 1, duration, testCase);
ParallelInstanceCreateHelper(4, 1, duration, testCase);
ParallelInstanceCreateHelper(4, 2, duration, testCase);
ParallelInstanceCreateHelper(4, 4, duration, testCase);
ParallelInstanceCreateHelper(8, 1, duration, testCase);
ParallelInstanceCreateHelper(8, 2, duration, testCase);
ParallelInstanceCreateHelper(8, 3, duration, testCase);
ParallelInstanceCreateHelper(8, 4, duration, testCase);
}
}
TEST_F(InstanceDatabaseTest, ParallelInstanceCreate)
{
ParallelCreateTest(ParallelInstanceTestCases::Create);
}
TEST_F(InstanceDatabaseTest, ParallelInstanceCreateAndDeferRemoval)
{
ParallelCreateTest(ParallelInstanceTestCases::CreateAndDeferRemoval);
}
TEST_F(InstanceDatabaseTest, ParallelInstanceCreateAndOrphan)
{
ParallelCreateTest(ParallelInstanceTestCases::CreateAndOrphan);
}
TEST_F(InstanceDatabaseTest, ParallelInstanceCreateDeferRemovalAndOrphan)
{
ParallelCreateTest(ParallelInstanceTestCases::CreateDeferRemovalAndOrphan);
}
TEST_F(InstanceDatabaseTest, InstanceCreateNoDatabase)
{
bool m_deleted = false;
{
Instance<TestInstanceB> instance = aznew TestInstanceB(nullptr);
EXPECT_FALSE(instance->GetId().IsValid());
// Tests whether the deleter actually calls delete properly without
// a parent database.
instance->m_onDeleteCallback = [&m_deleted]()
{
m_deleted = true;
};
}
EXPECT_TRUE(m_deleted);
}
TEST_F(InstanceDatabaseTest, InstanceCreateMultipleDatabases)
{
// create a second instance database.
{
InstanceHandler<TestInstanceB> instanceHandler;
instanceHandler.m_createFunction = [](AssetData* assetData)
{
EXPECT_TRUE(azrtti_istypeof<TestAssetType>(assetData));
return aznew TestInstanceB(static_cast<TestAssetType*>(assetData));
};
InstanceDatabase<TestInstanceB>::Create(azrtti_typeid<TestAssetType>(), instanceHandler);
}
auto& assetManager = AssetManager::Instance();
auto& instanceDatabaseA = InstanceDatabase<TestInstanceA>::Instance();
auto& instanceDatabaseB = InstanceDatabase<TestInstanceB>::Instance();
{
Asset<TestAssetType> someAsset = assetManager.CreateAsset<TestAssetType>(s_assetId0, AZ::Data::AssetLoadBehavior::Default);
// Run the creation tests on 'A' first.
Instance<TestInstanceA> instanceA = instanceDatabaseA.Find(s_instanceId0);
EXPECT_EQ(instanceA, nullptr);
instanceA = instanceDatabaseA.FindOrCreate(s_instanceId0, someAsset);
EXPECT_NE(instanceA, nullptr);
Instance<TestInstanceA> instanceA2 = instanceDatabaseA.FindOrCreate(s_instanceId0, someAsset);
EXPECT_EQ(instanceA, instanceA2);
Instance<TestInstanceA> instanceA3 = instanceDatabaseA.Find(s_instanceId0);
EXPECT_EQ(instanceA, instanceA3);
// Run the same test on 'B' to make sure it works independently.
Instance<TestInstanceB> instanceB = instanceDatabaseB.Find(s_instanceId0);
EXPECT_EQ(instanceB, nullptr);
instanceB = instanceDatabaseB.FindOrCreate(s_instanceId0, someAsset);
EXPECT_NE(instanceB, nullptr);
Instance<TestInstanceB> instanceB2 = instanceDatabaseB.FindOrCreate(s_instanceId0, someAsset);
EXPECT_EQ(instanceB, instanceB2);
Instance<TestInstanceB> instanceB3 = instanceDatabaseB.Find(s_instanceId0);
EXPECT_EQ(instanceB, instanceB3);
}
InstanceDatabase<TestInstanceB>::Destroy();
}
} // namespace UnitTest