Merge pull request #3272 from aws-lumberyard-dev/PIX
Profiler spring cleaning
This commit is contained in:
@@ -1191,154 +1191,6 @@ namespace UnitTest
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
class FrameProfilerComponentTest
|
||||
: public AllocatorsFixture
|
||||
, public FrameProfilerBus::Handler
|
||||
{
|
||||
public:
|
||||
FrameProfilerComponentTest()
|
||||
: AllocatorsFixture()
|
||||
{
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// FrameProfilerDrillerBus
|
||||
void OnFrameProfilerData(const FrameProfiler::ThreadDataArray& data) override
|
||||
{
|
||||
for (size_t iThread = 0; iThread < data.size(); ++iThread)
|
||||
{
|
||||
const FrameProfiler::ThreadData& td = data[iThread];
|
||||
FrameProfiler::ThreadData::RegistersMap::const_iterator regIt = td.m_registers.begin();
|
||||
size_t numRegisters = m_numRegistersReceived;
|
||||
for (; regIt != td.m_registers.end(); ++regIt)
|
||||
{
|
||||
const FrameProfiler::RegisterData& rd = regIt->second;
|
||||
|
||||
AZ_TEST_ASSERT(rd.m_function != NULL);
|
||||
if (strstr(rd.m_function, "ChildFunction") || strstr(rd.m_function, "Profile1")) // filter only the test registers
|
||||
{
|
||||
++m_numRegistersReceived;
|
||||
|
||||
EXPECT_GT(rd.m_line, 0);
|
||||
EXPECT_TRUE(rd.m_name == nullptr || strstr(rd.m_name, "Child1") || strstr(rd.m_name, "Custom name"));
|
||||
AZ::u32 unitTestCrc = AZ_CRC("UnitTest", 0x8089cea8);
|
||||
EXPECT_EQ(unitTestCrc, rd.m_systemId);
|
||||
EXPECT_EQ(ProfilerRegister::PRT_TIME, rd.m_type);
|
||||
|
||||
EXPECT_FALSE(rd.m_frames.empty());
|
||||
const FrameProfiler::FrameData& fd = rd.m_frames.back();
|
||||
EXPECT_GT(fd.m_frameId, 0u);
|
||||
EXPECT_GT(fd.m_timeData.m_time, 0);
|
||||
EXPECT_GT(fd.m_timeData.m_calls, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (numRegisters < m_numRegistersReceived)
|
||||
{
|
||||
// we have received valid test registers for this thread, add it to the list
|
||||
++m_numThreads;
|
||||
}
|
||||
}
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
int ChildFunction(int input)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", nullptr, NamedRegister);
|
||||
int result = 5;
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
result += i % (input + 3);
|
||||
}
|
||||
AZ_PROFILE_TIMER_END(NamedRegister);
|
||||
return result;
|
||||
}
|
||||
|
||||
int ChildFunction1(int input)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", "Child1");
|
||||
int result = 5;
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
result += i % (input + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int Profile1(int numIterations)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", "Custom name");
|
||||
int result = 0;
|
||||
for (int i = 0; i < numIterations; ++i)
|
||||
{
|
||||
result += ChildFunction(i);
|
||||
}
|
||||
|
||||
result += ChildFunction1(numIterations / 3);
|
||||
return result;
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
FrameProfilerBus::Handler::BusConnect();
|
||||
|
||||
ComponentApplication app;
|
||||
ComponentApplication::Descriptor desc;
|
||||
desc.m_useExistingAllocator = true;
|
||||
desc.m_enableDrilling = false; // we already created a memory driller for the test (AllocatorsFixture)
|
||||
ComponentApplication::StartupParameters startupParams;
|
||||
startupParams.m_allocator = &AZ::AllocatorInstance<AZ::SystemAllocator>::Get();
|
||||
Entity* systemEntity = app.Create(desc, startupParams);
|
||||
systemEntity->CreateComponent<FrameProfilerComponent>();
|
||||
|
||||
systemEntity->Init();
|
||||
systemEntity->Activate(); // start frame component
|
||||
|
||||
m_numThreads = 0;
|
||||
m_numRegistersReceived = 0;
|
||||
|
||||
// tick to frame 1 and collect all the samples
|
||||
app.Tick();
|
||||
EXPECT_EQ(0, m_numThreads);
|
||||
EXPECT_EQ(0, m_numRegistersReceived);
|
||||
|
||||
int numIterations = 10000;
|
||||
{
|
||||
AZStd::thread t1(AZStd::bind(&FrameProfilerComponentTest::Profile1, this, numIterations));
|
||||
AZStd::thread t2(AZStd::bind(&FrameProfilerComponentTest::Profile1, this, numIterations));
|
||||
AZStd::thread t3(AZStd::bind(&FrameProfilerComponentTest::Profile1, this, numIterations));
|
||||
AZStd::thread t4(AZStd::bind(&FrameProfilerComponentTest::Profile1, this, numIterations));
|
||||
|
||||
t1.join();
|
||||
t2.join();
|
||||
t3.join();
|
||||
t4.join();
|
||||
}
|
||||
|
||||
// tick to frame 2 and collect all the samples
|
||||
app.Tick();
|
||||
|
||||
EXPECT_EQ(4, m_numThreads);
|
||||
EXPECT_EQ(m_numThreads * 3, m_numRegistersReceived);
|
||||
|
||||
FrameProfilerBus::Handler::BusDisconnect();
|
||||
|
||||
app.Destroy();
|
||||
}
|
||||
|
||||
size_t m_numRegistersReceived;
|
||||
size_t m_numThreads;
|
||||
};
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_FRAMEPROFILER_TEST
|
||||
TEST_F(FrameProfilerComponentTest, DISABLED_Test)
|
||||
#else
|
||||
TEST_F(FrameProfilerComponentTest, Test)
|
||||
#endif
|
||||
{
|
||||
run();
|
||||
}
|
||||
|
||||
class SimpleEntityRefTestComponent
|
||||
: public Component
|
||||
{
|
||||
|
||||
@@ -171,276 +171,6 @@ namespace UnitTest
|
||||
run();
|
||||
}
|
||||
|
||||
class ProfilerTest
|
||||
: public AllocatorsFixture
|
||||
{
|
||||
public:
|
||||
int m_numRegistersReceived;
|
||||
|
||||
bool ReadRegisterCallback(const ProfilerRegister& reg, const AZStd::thread_id& id)
|
||||
{
|
||||
(void)reg;
|
||||
(void)id;
|
||||
switch (reg.m_type)
|
||||
{
|
||||
case ProfilerRegister::PRT_TIME:
|
||||
{
|
||||
AZ_TEST_ASSERT(reg.m_timeData.m_time > 0);
|
||||
AZ_TEST_ASSERT(reg.m_timeData.m_calls > 0);
|
||||
} break;
|
||||
case ProfilerRegister::PRT_VALUE:
|
||||
{
|
||||
AZ_TEST_ASSERT(reg.m_userValues.m_value1 == 1 || reg.m_userValues.m_value1 == 2);
|
||||
AZ_TEST_ASSERT(reg.m_userValues.m_value2 == 0 || reg.m_userValues.m_value2 == 2 || reg.m_userValues.m_value2 == 4);
|
||||
AZ_TEST_ASSERT(reg.m_userValues.m_value3 == 0 || reg.m_userValues.m_value3 == 3 || reg.m_userValues.m_value3 == 6);
|
||||
AZ_TEST_ASSERT(reg.m_userValues.m_value4 == 0 || reg.m_userValues.m_value4 == 4 || reg.m_userValues.m_value4 == 8);
|
||||
AZ_TEST_ASSERT(reg.m_userValues.m_value5 == 0 || reg.m_userValues.m_value5 == 5 || reg.m_userValues.m_value5 == 10);
|
||||
} break;
|
||||
}
|
||||
|
||||
//AZ::u64 threadId = (AZ::u64)id.m_id;
|
||||
//AZ_TracePrintf("Profiler","[%llu] '%s' '%s'(%d) %d Ms (Child calls: %d time: %d Ms) Parent: '%s'!\n",threadId,
|
||||
// reg.m_name,reg.m_function,reg.m_line,reg.m_time.count(),reg.m_childrenCalls,reg.m_childrenTime.count(),reg.m_lastParent ? reg.m_lastParent->m_name : "No");
|
||||
++m_numRegistersReceived;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ChildFunction(int input)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest");
|
||||
|
||||
auto start = AZStd::chrono::system_clock::now();
|
||||
|
||||
int result = 5;
|
||||
for (int i = 0; i < 30000; ++i)
|
||||
{
|
||||
result += i % (input + 3);
|
||||
}
|
||||
|
||||
auto end = AZStd::chrono::system_clock::now();
|
||||
AZ_TEST_ASSERT(end >= start);
|
||||
while (end <= start)
|
||||
{
|
||||
end = AZStd::chrono::system_clock::now();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int ChildFunction1(int input)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", "Child1");
|
||||
|
||||
auto start = AZStd::chrono::system_clock::now();
|
||||
|
||||
int result = 5;
|
||||
for (int i = 0; i < 30000; ++i)
|
||||
{
|
||||
result += i % (input + 1);
|
||||
}
|
||||
|
||||
|
||||
auto end = AZStd::chrono::system_clock::now();
|
||||
AZ_TEST_ASSERT(end >= start);
|
||||
while (end <= start)
|
||||
{
|
||||
end = AZStd::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int Profile1(int numIterations)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", "Custom name");
|
||||
int result = 0;
|
||||
for (int i = 0; i < numIterations; ++i)
|
||||
{
|
||||
result += ChildFunction(i);
|
||||
}
|
||||
|
||||
result += ChildFunction1(numIterations / 3);
|
||||
return result;
|
||||
}
|
||||
|
||||
void UserValuesSet()
|
||||
{
|
||||
AZ_PROFILE_VALUE_SET("UnitTest", "UserValues1", 1);
|
||||
AZ_PROFILE_VALUE_SET("UnitTest", "UserValues2", 1, 2);
|
||||
AZ_PROFILE_VALUE_SET("UnitTest", "UserValues3", 1, 2, 3);
|
||||
AZ::s64 v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5;
|
||||
AZ_PROFILE_VALUE_SET("UnitTest", "UserValues4", v1, v2, v3, v4);
|
||||
AZ_PROFILE_VALUE_SET("UnitTest", "UserValues5", v1, v2, v3, v4, v5);
|
||||
|
||||
// test named register
|
||||
AZ_PROFILE_VALUE_SET_NAMED("UnitTest", "UserValues5", userValues5, v1, v2, v3, v4, v5);
|
||||
#if defined(AZ_PROFILER_MACRO_DISABLE)
|
||||
(void)v1;
|
||||
(void)v2;
|
||||
(void)v3;
|
||||
(void)v4;
|
||||
(void)v5;
|
||||
#else
|
||||
AZ_TEST_ASSERT(userValues5 != nullptr);
|
||||
#endif // !defined(AZ_PROFILER_MACRO_DISABLE)
|
||||
}
|
||||
|
||||
void UserValuesAdd(int numAdditions)
|
||||
{
|
||||
for (int i = 0; i < numAdditions; ++i)
|
||||
{
|
||||
AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues1", 1);
|
||||
AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues2", 1, 2);
|
||||
AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues3", 1, 2, 3);
|
||||
AZ::s64 v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5;
|
||||
AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues4", v1, v2, v3, v4);
|
||||
AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues5", v1, v2, v3, v4, v5);
|
||||
|
||||
// test named register
|
||||
AZ_PROFILE_VALUE_ADD_NAMED("UnitTest", "UserValues5", userValues5, v1, v2, v3, v4, v5);
|
||||
#if defined(AZ_PROFILER_MACRO_DISABLE)
|
||||
(void)v1;
|
||||
(void)v2;
|
||||
(void)v3;
|
||||
(void)v4;
|
||||
(void)v5;
|
||||
#else
|
||||
AZ_TEST_ASSERT(userValues5 != nullptr);
|
||||
#endif // !defined(AZ_PROFILER_MACRO_DISABLE)
|
||||
}
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
AZ_TEST_ASSERT(!Profiler::IsReady());
|
||||
Profiler::Create();
|
||||
AZ_TEST_ASSERT(Profiler::IsReady());
|
||||
Profiler::Destroy();
|
||||
AZ_TEST_ASSERT(!Profiler::IsReady());
|
||||
|
||||
#if !defined(AZ_PROFILER_MACRO_DISABLE)
|
||||
Profiler::Create();
|
||||
|
||||
//Profile1();
|
||||
|
||||
//Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback,this,AZStd::placeholders::_1,AZStd::placeholders::_2));
|
||||
|
||||
//Profiler::Instance().ResetRegisters();
|
||||
|
||||
AZStd::thread_id removeThreadId;
|
||||
AZStd::chrono::microseconds elapsed[2];
|
||||
int numIterations = 10000;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
// for the second run we should not record any data
|
||||
if (i == 1)
|
||||
{
|
||||
Profiler::Instance().DeactivateSystem("UnitTest");
|
||||
}
|
||||
|
||||
AZStd::chrono::system_clock::time_point start = AZStd::chrono::system_clock::now();
|
||||
AZStd::thread t1(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t2(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t3(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t4(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t5(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t6(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t7(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
AZStd::thread t8(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
|
||||
|
||||
removeThreadId = t4.get_id();
|
||||
|
||||
t1.join();
|
||||
t2.join();
|
||||
t3.join();
|
||||
t4.join();
|
||||
t5.join();
|
||||
t6.join();
|
||||
t7.join();
|
||||
t8.join();
|
||||
elapsed[i] = AZStd::chrono::system_clock::now() - start;
|
||||
//AZ_Printf("Profiler","Elapsed time %d\n",elapsed[i].count());
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
// just as test remove all associated data and registers.
|
||||
Profiler::Instance().RemoveThreadData(removeThreadId);
|
||||
}
|
||||
|
||||
m_numRegistersReceived = 0;
|
||||
Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback, this, AZStd::placeholders::_1, AZStd::placeholders::_2));
|
||||
if (i == 0)
|
||||
{
|
||||
AZ_TEST_ASSERT(m_numRegistersReceived == 7 * 3); // 3 registers for each thread (8 threads - 1 we removed the data for 't4')
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_TEST_ASSERT(m_numRegistersReceived == 0);
|
||||
}
|
||||
}
|
||||
Profiler::Destroy();
|
||||
|
||||
// Test user value registers
|
||||
Profiler::Create();
|
||||
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
// for the second run we should not record any data
|
||||
if (i == 1)
|
||||
{
|
||||
Profiler::Instance().DeactivateSystem("UnitTest");
|
||||
}
|
||||
|
||||
AZStd::thread t1(AZStd::bind(&ProfilerTest::UserValuesSet, this));
|
||||
AZStd::thread t2(AZStd::bind(&ProfilerTest::UserValuesSet, this));
|
||||
AZStd::thread t3(AZStd::bind(&ProfilerTest::UserValuesSet, this));
|
||||
AZStd::thread t4(AZStd::bind(&ProfilerTest::UserValuesSet, this));
|
||||
AZStd::thread t5(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
|
||||
AZStd::thread t6(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
|
||||
AZStd::thread t7(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
|
||||
AZStd::thread t8(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
|
||||
|
||||
removeThreadId = t4.get_id();
|
||||
|
||||
t1.join();
|
||||
t2.join();
|
||||
t3.join();
|
||||
t4.join();
|
||||
t5.join();
|
||||
t6.join();
|
||||
t7.join();
|
||||
t8.join();
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
// just as test remove all associated data and registers.
|
||||
Profiler::Instance().RemoveThreadData(removeThreadId);
|
||||
}
|
||||
|
||||
m_numRegistersReceived = 0;
|
||||
Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback, this, AZStd::placeholders::_1, AZStd::placeholders::_2));
|
||||
if (i == 0)
|
||||
{
|
||||
AZ_TEST_ASSERT(m_numRegistersReceived == 7 * 6); // 6 registers for each thread (8 threads - 1 we removed the data for 't4' )
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_TEST_ASSERT(m_numRegistersReceived == 0);
|
||||
}
|
||||
}
|
||||
Profiler::Destroy();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
#if AZ_TRAIT_DISABLE_FAILED_PROFILER_TEST
|
||||
TEST_F(ProfilerTest, DISABLED_Test)
|
||||
#else
|
||||
TEST_F(ProfilerTest, Test)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_PROFILER_TEST
|
||||
|
||||
{
|
||||
run();
|
||||
}
|
||||
|
||||
TEST(Time, Test)
|
||||
{
|
||||
AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond();
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzCore/Math/Obb.h>
|
||||
#include <AzCore/Math/Aabb.h>
|
||||
#include <AzCore/Math/Vector3.h>
|
||||
#include <AzCore/Math/Transform.h>
|
||||
#include <AzCore/UnitTest/TestTypes.h>
|
||||
#include <AZTestShared/Math/MathTestHelpers.h>
|
||||
#include <AzCore/Math/Aabb.h>
|
||||
#include <AzCore/Math/Obb.h>
|
||||
#include <AzCore/Math/Transform.h>
|
||||
#include <AzCore/Math/Vector3.h>
|
||||
#include <AzCore/UnitTest/TestTypes.h>
|
||||
|
||||
using namespace AZ;
|
||||
|
||||
namespace UnitTest
|
||||
namespace UnitTest::ObbTests
|
||||
{
|
||||
const Vector3 position(1.0f, 2.0f, 3.0f);
|
||||
const Quaternion rotation = Quaternion::CreateRotationZ(Constants::QuarterPi);
|
||||
@@ -151,4 +151,4 @@ namespace UnitTest
|
||||
EXPECT_NEAR(obb.GetDistanceSq(Vector3(2.4f, 0.5f, 1.5f)), 0.5532f, 1e-3f);
|
||||
EXPECT_NEAR(obb.GetDistanceSq(Vector3(1.1f, 7.3f, 5.8f)), 1.3612f, 1e-3f);
|
||||
}
|
||||
}
|
||||
} // namespace UnitTest::ObbTests
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace UnitTest
|
||||
|
||||
int ChildFunction0(int numIterations, int sleepTimeMilliseconds)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", CHILD_TIMER_STAT0);
|
||||
AZ_PROFILE_SCOPE(UnitTest, CHILD_TIMER_STAT0);
|
||||
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds));
|
||||
int result = 5;
|
||||
for (int i = 0; i < numIterations; ++i)
|
||||
@@ -95,7 +95,7 @@ namespace UnitTest
|
||||
|
||||
int ChildFunction1(int numIterations, int sleepTimeMilliseconds)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", CHILD_TIMER_STAT1);
|
||||
AZ_PROFILE_SCOPE(UnitTest, CHILD_TIMER_STAT1);
|
||||
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds));
|
||||
int result = 5;
|
||||
for (int i = 0; i < numIterations; ++i)
|
||||
@@ -107,7 +107,7 @@ namespace UnitTest
|
||||
|
||||
int ParentFunction(int numIterations, int sleepTimeMilliseconds)
|
||||
{
|
||||
AZ_PROFILE_TIMER("UnitTest", PARENT_TIMER_STAT);
|
||||
AZ_PROFILE_SCOPE(UnitTest, PARENT_TIMER_STAT);
|
||||
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds));
|
||||
int result = 0;
|
||||
result += ChildFunction0(numIterations, sleepTimeMilliseconds);
|
||||
@@ -198,10 +198,11 @@ namespace UnitTest
|
||||
AZStd::unique_ptr<Statistics::TimeDataStatisticsManager> m_statsManager;
|
||||
};//class TimeDataStatisticsManagerTest
|
||||
|
||||
TEST_F(TimeDataStatisticsManagerTest, Test)
|
||||
{
|
||||
run();
|
||||
}
|
||||
// TODO:BUDGETS disabled until profiler budgets system comes online
|
||||
// TEST_F(TimeDataStatisticsManagerTest, Test)
|
||||
// {
|
||||
// run();
|
||||
// }
|
||||
//End of all Tests of TimeDataStatisticsManagerTest
|
||||
|
||||
}//namespace UnitTest
|
||||
|
||||
@@ -60,7 +60,6 @@ set(FILES
|
||||
SerializeContextFixture.h
|
||||
Slice.cpp
|
||||
State.cpp
|
||||
StatisticalProfiler.cpp
|
||||
Statistics.cpp
|
||||
StreamerTests.cpp
|
||||
StringFunc.cpp
|
||||
|
||||
Reference in New Issue
Block a user