ShapeThreadsafeTest.cpp 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzTest/AzTest.h>
  9. #include <AzCore/Component/ComponentApplication.h>
  10. #include <ShapeThreadsafeTest.h>
  11. #include <LmbrCentral/Shape/ShapeComponentBus.h>
  12. namespace UnitTest
  13. {
  14. void ShapeThreadsafeTest::TestShapeGetSetCallsAreThreadsafe(
  15. AZ::Entity& shapeEntity, int numIterations,
  16. AZStd::function<void(AZ::EntityId shapeEntityId, float minDimension, uint32_t dimensionVariance, float height)> shapeSetFn)
  17. {
  18. // This test will run parallel threads that all query "DistanceFromPoint" on the same shape and test point while
  19. // simultaneously running a thread that keeps changing any unimportant dimensions on the shape.
  20. // If the calls are threadsafe between Get/Set and between multiple Get calls themselves, all queries should return
  21. // the same distance because the shape height and point queried are staying invariant.
  22. // If the calls aren't threadsafe, the internal shape data will become inconsistent and we can get arbitrary results.
  23. // The test point that we'll use for getting the distance to the shape.
  24. const AZ::Vector3 TestPoint(0.0f, 0.0f, 20.0f);
  25. // The expected distance from the test point to the shape.
  26. // Since we're setting it above the shape and keeping the height constant, the expected distance will
  27. // always be 10. (The shape extends 10 above and 10 below the origin, so 20 above origin is 10 above the shape)
  28. constexpr float ExpectedDistance = 10.0f;
  29. // Comparing floats needs a tolerance based on whether we are using NEON or not
  30. #if AZ_TRAIT_USE_PLATFORM_SIMD_NEON
  31. constexpr float compareTolerance = 1.0e-4;
  32. #else
  33. constexpr float compareTolerance = 1.0e-6;
  34. #endif // AZ_TRAIT_USE_PLATFORM_SIMD_NEON
  35. AZ::EntityId shapeEntityId = shapeEntity.GetId();
  36. // Pick an arbitrary number of threads and iterations that are large enough to demonstrate thread safety problems.
  37. constexpr int NumQueryThreads = 4;
  38. AZStd::thread queryThreads[NumQueryThreads];
  39. AZStd::semaphore syncThreads;
  40. // Create all of the threads that will query DistanceFromPoint.
  41. for (auto& queryThread : queryThreads)
  42. {
  43. queryThread = AZStd::thread(
  44. [shapeEntityId, TestPoint, ExpectedDistance = ExpectedDistance, numIterations, &syncThreads]()
  45. {
  46. // Block until all the threads are created, so that we can run them 100% in parallel.
  47. syncThreads.acquire();
  48. // Keep querying the same shape and point and verify that we get back the same distance.
  49. // This can fail if the calls aren't threadsafe because the internal shape data will become inconsistent
  50. // and return odd results.
  51. for (int i = 0; i < numIterations; i++)
  52. {
  53. // Pick an impossible value to initialize with so that we can see in the results if we ever fail
  54. // due to a shape not being connected to the EBus.
  55. float distance = -10.0f;
  56. LmbrCentral::ShapeComponentRequestsBus::EventResult(
  57. distance, shapeEntityId, &LmbrCentral::ShapeComponentRequestsBus::Events::DistanceFromPoint, TestPoint);
  58. // This is wrapped in an 'if' statement just to make it easier to debug if anything goes wrong.
  59. // You can put a breakpoint on the ASSERT_EQ line to see the current state of things in the failure case.
  60. if (distance != ExpectedDistance)
  61. {
  62. ASSERT_NEAR(distance, ExpectedDistance, compareTolerance);
  63. }
  64. }
  65. });
  66. }
  67. // Create a single thread that will continuously set shape dimension except height to random values in a tight loop
  68. // until all our query threads have finished their iterations.
  69. AZStd::atomic_bool stopSetThread = false;
  70. AZStd::thread setThread = AZStd::thread(
  71. [shapeEntityId, NumQueryThreads = NumQueryThreads, &shapeSetFn, &syncThreads, &stopSetThread]()
  72. {
  73. // Now that all threads are created, signal everything to start running in parallel.
  74. syncThreads.release(NumQueryThreads);
  75. // Change the dimensions in a tight loop until the query threads are all finished.
  76. while (!stopSetThread)
  77. {
  78. shapeSetFn(shapeEntityId, MinDimension, DimensionVariance, ShapeHeight);
  79. }
  80. });
  81. // Wait for all the query threads to finish.
  82. for (auto& queryThread : queryThreads)
  83. {
  84. queryThread.join();
  85. }
  86. // Signal that the "set" thread should finish and wait for it to end.
  87. stopSetThread = true;
  88. setThread.join();
  89. }
  90. }