| 1 | //===-- buffer_queue_test.cpp ---------------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // This file is a part of XRay, a function call tracing system. |
| 10 | // |
| 11 | //===----------------------------------------------------------------------===// |
| 12 | #include "xray_buffer_queue.h" |
| 13 | #include "gmock/gmock.h" |
| 14 | #include "gtest/gtest.h" |
| 15 | |
| 16 | #include <atomic> |
| 17 | #include <future> |
| 18 | #include <thread> |
| 19 | #include <unistd.h> |
| 20 | |
| 21 | namespace __xray { |
| 22 | namespace { |
| 23 | |
| 24 | static constexpr size_t kSize = 4096; |
| 25 | |
| 26 | using ::testing::Eq; |
| 27 | |
| 28 | TEST(BufferQueueTest, API) { |
| 29 | bool Success = false; |
| 30 | BufferQueue Buffers(kSize, 1, Success); |
| 31 | ASSERT_TRUE(Success); |
| 32 | } |
| 33 | |
| 34 | TEST(BufferQueueTest, GetAndRelease) { |
| 35 | bool Success = false; |
| 36 | BufferQueue Buffers(kSize, 1, Success); |
| 37 | ASSERT_TRUE(Success); |
| 38 | BufferQueue::Buffer Buf; |
| 39 | ASSERT_EQ(Buffers.getBuffer(Buf), BufferQueue::ErrorCode::Ok); |
| 40 | ASSERT_NE(nullptr, Buf.Data); |
| 41 | ASSERT_EQ(Buffers.releaseBuffer(Buf), BufferQueue::ErrorCode::Ok); |
| 42 | ASSERT_EQ(nullptr, Buf.Data); |
| 43 | } |
| 44 | |
| 45 | TEST(BufferQueueTest, GetUntilFailed) { |
| 46 | bool Success = false; |
| 47 | BufferQueue Buffers(kSize, 1, Success); |
| 48 | ASSERT_TRUE(Success); |
| 49 | BufferQueue::Buffer Buf0; |
| 50 | EXPECT_EQ(Buffers.getBuffer(Buf&: Buf0), BufferQueue::ErrorCode::Ok); |
| 51 | BufferQueue::Buffer Buf1; |
| 52 | EXPECT_EQ(BufferQueue::ErrorCode::NotEnoughMemory, Buffers.getBuffer(Buf&: Buf1)); |
| 53 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: Buf0), BufferQueue::ErrorCode::Ok); |
| 54 | } |
| 55 | |
| 56 | TEST(BufferQueueTest, ReleaseUnknown) { |
| 57 | bool Success = false; |
| 58 | BufferQueue Buffers(kSize, 1, Success); |
| 59 | ASSERT_TRUE(Success); |
| 60 | BufferQueue::Buffer Buf; |
| 61 | Buf.Data = reinterpret_cast<void *>(0xdeadbeef); |
| 62 | Buf.Size = kSize; |
| 63 | Buf.Generation = Buffers.generation(); |
| 64 | |
| 65 | BufferQueue::Buffer Known; |
| 66 | EXPECT_THAT(Buffers.getBuffer(Buf&: Known), Eq(BufferQueue::ErrorCode::Ok)); |
| 67 | EXPECT_THAT(Buffers.releaseBuffer(Buf), |
| 68 | Eq(BufferQueue::ErrorCode::UnrecognizedBuffer)); |
| 69 | EXPECT_THAT(Buffers.releaseBuffer(Buf&: Known), Eq(BufferQueue::ErrorCode::Ok)); |
| 70 | } |
| 71 | |
| 72 | TEST(BufferQueueTest, ErrorsWhenFinalising) { |
| 73 | bool Success = false; |
| 74 | BufferQueue Buffers(kSize, 2, Success); |
| 75 | ASSERT_TRUE(Success); |
| 76 | BufferQueue::Buffer Buf; |
| 77 | ASSERT_EQ(Buffers.getBuffer(Buf), BufferQueue::ErrorCode::Ok); |
| 78 | ASSERT_NE(nullptr, Buf.Data); |
| 79 | ASSERT_EQ(Buffers.finalize(), BufferQueue::ErrorCode::Ok); |
| 80 | BufferQueue::Buffer OtherBuf; |
| 81 | ASSERT_EQ(BufferQueue::ErrorCode::QueueFinalizing, |
| 82 | Buffers.getBuffer(Buf&: OtherBuf)); |
| 83 | ASSERT_EQ(BufferQueue::ErrorCode::QueueFinalizing, Buffers.finalize()); |
| 84 | ASSERT_EQ(Buffers.releaseBuffer(Buf), BufferQueue::ErrorCode::Ok); |
| 85 | } |
| 86 | |
| 87 | TEST(BufferQueueTest, MultiThreaded) { |
| 88 | bool Success = false; |
| 89 | BufferQueue Buffers(kSize, 100, Success); |
| 90 | ASSERT_TRUE(Success); |
| 91 | auto F = [&] { |
| 92 | BufferQueue::Buffer B; |
| 93 | while (true) { |
| 94 | auto EC = Buffers.getBuffer(Buf&: B); |
| 95 | if (EC != BufferQueue::ErrorCode::Ok) |
| 96 | return; |
| 97 | Buffers.releaseBuffer(Buf&: B); |
| 98 | } |
| 99 | }; |
| 100 | auto T0 = std::async(policy: std::launch::async, fn&: F); |
| 101 | auto T1 = std::async(policy: std::launch::async, fn&: F); |
| 102 | auto T2 = std::async(policy: std::launch::async, fn: [&] { |
| 103 | while (Buffers.finalize() != BufferQueue::ErrorCode::Ok) |
| 104 | ; |
| 105 | }); |
| 106 | F(); |
| 107 | } |
| 108 | |
| 109 | TEST(BufferQueueTest, Apply) { |
| 110 | bool Success = false; |
| 111 | BufferQueue Buffers(kSize, 10, Success); |
| 112 | ASSERT_TRUE(Success); |
| 113 | auto Count = 0; |
| 114 | BufferQueue::Buffer B; |
| 115 | for (int I = 0; I < 10; ++I) { |
| 116 | ASSERT_EQ(Buffers.getBuffer(Buf&: B), BufferQueue::ErrorCode::Ok); |
| 117 | ASSERT_EQ(Buffers.releaseBuffer(Buf&: B), BufferQueue::ErrorCode::Ok); |
| 118 | } |
| 119 | Buffers.apply(Fn: [&](const BufferQueue::Buffer &B) { ++Count; }); |
| 120 | ASSERT_EQ(Count, 10); |
| 121 | } |
| 122 | |
| 123 | TEST(BufferQueueTest, GenerationalSupport) { |
| 124 | bool Success = false; |
| 125 | BufferQueue Buffers(kSize, 10, Success); |
| 126 | ASSERT_TRUE(Success); |
| 127 | BufferQueue::Buffer B0; |
| 128 | ASSERT_EQ(Buffers.getBuffer(Buf&: B0), BufferQueue::ErrorCode::Ok); |
| 129 | ASSERT_EQ(Buffers.finalize(), |
| 130 | BufferQueue::ErrorCode::Ok); // No more new buffers. |
| 131 | |
| 132 | // Re-initialise the queue. |
| 133 | ASSERT_EQ(Buffers.init(BS: kSize, BC: 10), BufferQueue::ErrorCode::Ok); |
| 134 | |
| 135 | BufferQueue::Buffer B1; |
| 136 | ASSERT_EQ(Buffers.getBuffer(Buf&: B1), BufferQueue::ErrorCode::Ok); |
| 137 | |
| 138 | // Validate that the buffers come from different generations. |
| 139 | ASSERT_NE(B0.Generation, B1.Generation); |
| 140 | |
| 141 | // We stash the current generation, for use later. |
| 142 | auto PrevGen = B1.Generation; |
| 143 | |
| 144 | // At this point, we want to ensure that we can return the buffer from the |
| 145 | // first "generation" would still be accepted in the new generation... |
| 146 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: B0), BufferQueue::ErrorCode::Ok); |
| 147 | |
| 148 | // ... and that the new buffer is also accepted. |
| 149 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: B1), BufferQueue::ErrorCode::Ok); |
| 150 | |
| 151 | // A next round will do the same, ensure that we are able to do multiple |
| 152 | // rounds in this case. |
| 153 | ASSERT_EQ(Buffers.finalize(), BufferQueue::ErrorCode::Ok); |
| 154 | ASSERT_EQ(Buffers.init(BS: kSize, BC: 10), BufferQueue::ErrorCode::Ok); |
| 155 | EXPECT_EQ(Buffers.getBuffer(Buf&: B0), BufferQueue::ErrorCode::Ok); |
| 156 | EXPECT_EQ(Buffers.getBuffer(Buf&: B1), BufferQueue::ErrorCode::Ok); |
| 157 | |
| 158 | // Here we ensure that the generation is different from the previous |
| 159 | // generation. |
| 160 | EXPECT_NE(B0.Generation, PrevGen); |
| 161 | EXPECT_EQ(B1.Generation, B1.Generation); |
| 162 | ASSERT_EQ(Buffers.finalize(), BufferQueue::ErrorCode::Ok); |
| 163 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: B0), BufferQueue::ErrorCode::Ok); |
| 164 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: B1), BufferQueue::ErrorCode::Ok); |
| 165 | } |
| 166 | |
| 167 | TEST(BufferQueueTest, GenerationalSupportAcrossThreads) { |
| 168 | bool Success = false; |
| 169 | BufferQueue Buffers(kSize, 10, Success); |
| 170 | ASSERT_TRUE(Success); |
| 171 | |
| 172 | std::atomic<int> Counter{0}; |
| 173 | |
| 174 | // This function allows us to use thread-local storage to isolate the |
| 175 | // instances of the buffers to be used. It also allows us signal the threads |
| 176 | // of a new generation, and allow those to get new buffers. This is |
| 177 | // representative of how we expect the buffer queue to be used by the XRay |
| 178 | // runtime. |
| 179 | auto Process = [&] { |
| 180 | thread_local BufferQueue::Buffer B; |
| 181 | ASSERT_EQ(Buffers.getBuffer(Buf&: B), BufferQueue::ErrorCode::Ok); |
| 182 | auto FirstGen = B.Generation; |
| 183 | |
| 184 | // Signal that we've gotten a buffer in the thread. |
| 185 | Counter.fetch_add(i: 1, m: std::memory_order_acq_rel); |
| 186 | while (!Buffers.finalizing()) { |
| 187 | Buffers.releaseBuffer(Buf&: B); |
| 188 | Buffers.getBuffer(Buf&: B); |
| 189 | } |
| 190 | |
| 191 | // Signal that we've exited the get/release buffer loop. |
| 192 | Counter.fetch_sub(i: 1, m: std::memory_order_acq_rel); |
| 193 | if (B.Data != nullptr) |
| 194 | Buffers.releaseBuffer(Buf&: B); |
| 195 | |
| 196 | // Spin until we find that the Buffer Queue is no longer finalizing. |
| 197 | while (Buffers.getBuffer(Buf&: B) != BufferQueue::ErrorCode::Ok) |
| 198 | ; |
| 199 | |
| 200 | // Signal that we've successfully gotten a buffer in the thread. |
| 201 | Counter.fetch_add(i: 1, m: std::memory_order_acq_rel); |
| 202 | |
| 203 | EXPECT_NE(FirstGen, B.Generation); |
| 204 | EXPECT_EQ(Buffers.releaseBuffer(Buf&: B), BufferQueue::ErrorCode::Ok); |
| 205 | |
| 206 | // Signal that we've successfully exited. |
| 207 | Counter.fetch_sub(i: 1, m: std::memory_order_acq_rel); |
| 208 | }; |
| 209 | |
| 210 | // Spawn two threads running Process. |
| 211 | std::thread T0(Process), T1(Process); |
| 212 | |
| 213 | // Spin until we find the counter is up to 2. |
| 214 | while (Counter.load(m: std::memory_order_acquire) != 2) |
| 215 | ; |
| 216 | |
| 217 | // Then we finalize, then re-initialize immediately. |
| 218 | Buffers.finalize(); |
| 219 | |
| 220 | // Spin until we find the counter is down to 0. |
| 221 | while (Counter.load(m: std::memory_order_acquire) != 0) |
| 222 | ; |
| 223 | |
| 224 | // Then we re-initialize. |
| 225 | EXPECT_EQ(Buffers.init(BS: kSize, BC: 10), BufferQueue::ErrorCode::Ok); |
| 226 | |
| 227 | T0.join(); |
| 228 | T1.join(); |
| 229 | |
| 230 | ASSERT_EQ(Counter.load(m: std::memory_order_acquire), 0); |
| 231 | } |
| 232 | |
| 233 | } // namespace |
| 234 | } // namespace __xray |
| 235 | |