You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AtomCore/Tests/InstanceDatabase.cpp

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