| 1 | //===-- profile_collector_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 "gtest/gtest.h" |
| 13 | |
| 14 | #include "xray_profile_collector.h" |
| 15 | #include "xray_profiling_flags.h" |
| 16 | #include <cstdint> |
| 17 | #include <cstring> |
| 18 | #include <memory> |
| 19 | #include <thread> |
| 20 | #include <utility> |
| 21 | #include <vector> |
| 22 | |
| 23 | namespace __xray { |
| 24 | namespace { |
| 25 | |
| 26 | static constexpr auto = 16u; |
| 27 | |
| 28 | constexpr uptr ExpectedProfilingVersion = 0x20180424; |
| 29 | |
| 30 | struct { |
| 31 | const u64 = 0x7872617970726f66; // Identifier for XRay profiling |
| 32 | // files 'xrayprof' in hex. |
| 33 | const u64 = ExpectedProfilingVersion; |
| 34 | u64 = 0; |
| 35 | u64 = 0; |
| 36 | }; |
| 37 | |
| 38 | void (XRayBuffer B) { |
| 39 | ASSERT_NE(static_cast<const void *>(B.Data), nullptr); |
| 40 | ASSERT_EQ(B.Size, sizeof(ExpectedProfilingFileHeader)); |
| 41 | alignas(ExpectedProfilingFileHeader) |
| 42 | std::byte [sizeof(ExpectedProfilingFileHeader)]; |
| 43 | ExpectedProfilingFileHeader ; |
| 44 | std::memcpy(dest: &FileHeaderStorage, src: B.Data, n: B.Size); |
| 45 | auto & = |
| 46 | *reinterpret_cast<ExpectedProfilingFileHeader *>(&FileHeaderStorage); |
| 47 | ASSERT_EQ(ExpectedHeader.MagicBytes, FileHeader.MagicBytes); |
| 48 | ASSERT_EQ(ExpectedHeader.Version, FileHeader.Version); |
| 49 | } |
| 50 | |
| 51 | void ValidateBlock(XRayBuffer B) { |
| 52 | profilingFlags()->setDefaults(); |
| 53 | ASSERT_NE(static_cast<const void *>(B.Data), nullptr); |
| 54 | ASSERT_NE(B.Size, 0u); |
| 55 | ASSERT_GE(B.Size, kHeaderSize); |
| 56 | // We look at the block size, the block number, and the thread ID to ensure |
| 57 | // that none of them are zero (or that the header data is laid out as we |
| 58 | // expect). |
| 59 | char LocalBuffer[kHeaderSize] = {}; |
| 60 | internal_memcpy(dest: LocalBuffer, src: B.Data, n: kHeaderSize); |
| 61 | u32 BlockSize = 0; |
| 62 | u32 BlockNumber = 0; |
| 63 | u64 ThreadId = 0; |
| 64 | internal_memcpy(dest: &BlockSize, src: LocalBuffer, n: sizeof(u32)); |
| 65 | internal_memcpy(dest: &BlockNumber, src: LocalBuffer + sizeof(u32), n: sizeof(u32)); |
| 66 | internal_memcpy(dest: &ThreadId, src: LocalBuffer + (2 * sizeof(u32)), n: sizeof(u64)); |
| 67 | ASSERT_NE(BlockSize, 0u); |
| 68 | ASSERT_GE(BlockNumber, 0u); |
| 69 | ASSERT_NE(ThreadId, 0u); |
| 70 | } |
| 71 | |
| 72 | std::tuple<u32, u32, u64> (XRayBuffer B) { |
| 73 | char LocalBuffer[kHeaderSize] = {}; |
| 74 | internal_memcpy(dest: LocalBuffer, src: B.Data, n: kHeaderSize); |
| 75 | u32 BlockSize = 0; |
| 76 | u32 BlockNumber = 0; |
| 77 | u64 ThreadId = 0; |
| 78 | internal_memcpy(dest: &BlockSize, src: LocalBuffer, n: sizeof(u32)); |
| 79 | internal_memcpy(dest: &BlockNumber, src: LocalBuffer + sizeof(u32), n: sizeof(u32)); |
| 80 | internal_memcpy(dest: &ThreadId, src: LocalBuffer + (2 * sizeof(u32)), n: sizeof(u64)); |
| 81 | return std::make_tuple(args&: BlockSize, args&: BlockNumber, args&: ThreadId); |
| 82 | } |
| 83 | |
| 84 | struct Profile { |
| 85 | int64_t CallCount; |
| 86 | int64_t CumulativeLocalTime; |
| 87 | std::vector<int32_t> Path; |
| 88 | }; |
| 89 | |
| 90 | std::tuple<Profile, const char *> ParseProfile(const char *P) { |
| 91 | Profile Result; |
| 92 | // Read the path first, until we find a sentinel 0. |
| 93 | int32_t F; |
| 94 | do { |
| 95 | internal_memcpy(dest: &F, src: P, n: sizeof(int32_t)); |
| 96 | P += sizeof(int32_t); |
| 97 | Result.Path.push_back(x: F); |
| 98 | } while (F != 0); |
| 99 | |
| 100 | // Then read the CallCount. |
| 101 | internal_memcpy(dest: &Result.CallCount, src: P, n: sizeof(int64_t)); |
| 102 | P += sizeof(int64_t); |
| 103 | |
| 104 | // Then read the CumulativeLocalTime. |
| 105 | internal_memcpy(dest: &Result.CumulativeLocalTime, src: P, n: sizeof(int64_t)); |
| 106 | P += sizeof(int64_t); |
| 107 | return std::make_tuple(args: std::move(t&: Result), args&: P); |
| 108 | } |
| 109 | |
| 110 | TEST(profileCollectorServiceTest, PostSerializeCollect) { |
| 111 | profilingFlags()->setDefaults(); |
| 112 | bool Success = false; |
| 113 | BufferQueue BQ(profilingFlags()->per_thread_allocator_max, |
| 114 | profilingFlags()->buffers_max, Success); |
| 115 | ASSERT_EQ(Success, true); |
| 116 | FunctionCallTrie::Allocators::Buffers Buffers; |
| 117 | ASSERT_EQ(BQ.getBuffer(Buffers.NodeBuffer), BufferQueue::ErrorCode::Ok); |
| 118 | ASSERT_EQ(BQ.getBuffer(Buffers.RootsBuffer), BufferQueue::ErrorCode::Ok); |
| 119 | ASSERT_EQ(BQ.getBuffer(Buffers.ShadowStackBuffer), |
| 120 | BufferQueue::ErrorCode::Ok); |
| 121 | ASSERT_EQ(BQ.getBuffer(Buffers.NodeIdPairBuffer), BufferQueue::ErrorCode::Ok); |
| 122 | auto Allocators = FunctionCallTrie::InitAllocatorsFromBuffers(Buffers); |
| 123 | FunctionCallTrie T(Allocators); |
| 124 | |
| 125 | // Populate the trie with some data. |
| 126 | T.enterFunction(1, 1, 0); |
| 127 | T.enterFunction(2, 2, 0); |
| 128 | T.exitFunction(2, 3, 0); |
| 129 | T.exitFunction(1, 4, 0); |
| 130 | |
| 131 | // Reset the collector data structures. |
| 132 | profileCollectorService::reset(); |
| 133 | |
| 134 | // Then we post the data to the global profile collector service. |
| 135 | profileCollectorService::post(&BQ, std::move(T), std::move(Allocators), |
| 136 | std::move(Buffers), 1); |
| 137 | |
| 138 | // Then we serialize the data. |
| 139 | profileCollectorService::serialize(); |
| 140 | |
| 141 | // Then we go through two buffers to see whether we're getting the data we |
| 142 | // expect. The first block must always be as large as a file header, which |
| 143 | // will have a fixed size. |
| 144 | auto B = profileCollectorService::nextBuffer({nullptr, 0}); |
| 145 | ValidateFileHeaderBlock(B); |
| 146 | |
| 147 | B = profileCollectorService::nextBuffer(B); |
| 148 | ValidateBlock(B); |
| 149 | u32 BlockSize; |
| 150 | u32 BlockNum; |
| 151 | u64 ThreadId; |
| 152 | std::tie(BlockSize, BlockNum, ThreadId) = ParseBlockHeader(B); |
| 153 | |
| 154 | // We look at the serialized buffer to see whether the Trie we're expecting |
| 155 | // to see is there. |
| 156 | auto DStart = static_cast<const char *>(B.Data) + kHeaderSize; |
| 157 | std::vector<char> D(DStart, DStart + BlockSize); |
| 158 | B = profileCollectorService::nextBuffer(B); |
| 159 | ASSERT_EQ(B.Data, nullptr); |
| 160 | ASSERT_EQ(B.Size, 0u); |
| 161 | |
| 162 | Profile Profile1, Profile2; |
| 163 | auto P = static_cast<const char *>(D.data()); |
| 164 | std::tie(Profile1, P) = ParseProfile(P); |
| 165 | std::tie(Profile2, P) = ParseProfile(P); |
| 166 | |
| 167 | ASSERT_NE(Profile1.Path.size(), Profile2.Path.size()); |
| 168 | auto &P1 = Profile1.Path.size() < Profile2.Path.size() ? Profile2 : Profile1; |
| 169 | auto &P2 = Profile1.Path.size() < Profile2.Path.size() ? Profile1 : Profile2; |
| 170 | std::vector<int32_t> P1Expected = {2, 1, 0}; |
| 171 | std::vector<int32_t> P2Expected = {1, 0}; |
| 172 | ASSERT_EQ(P1.Path.size(), P1Expected.size()); |
| 173 | ASSERT_EQ(P2.Path.size(), P2Expected.size()); |
| 174 | ASSERT_EQ(P1.Path, P1Expected); |
| 175 | ASSERT_EQ(P2.Path, P2Expected); |
| 176 | } |
| 177 | |
| 178 | // We break out a function that will be run in multiple threads, one that will |
| 179 | // use a thread local allocator, and will post the FunctionCallTrie to the |
| 180 | // profileCollectorService. This simulates what the threads being profiled would |
| 181 | // be doing anyway, but through the XRay logging implementation. |
| 182 | void threadProcessing() { |
| 183 | static bool Success = false; |
| 184 | static BufferQueue BQ(profilingFlags()->per_thread_allocator_max, |
| 185 | profilingFlags()->buffers_max, Success); |
| 186 | thread_local FunctionCallTrie::Allocators::Buffers Buffers = [] { |
| 187 | FunctionCallTrie::Allocators::Buffers B; |
| 188 | BQ.getBuffer(Buf&: B.NodeBuffer); |
| 189 | BQ.getBuffer(Buf&: B.RootsBuffer); |
| 190 | BQ.getBuffer(Buf&: B.ShadowStackBuffer); |
| 191 | BQ.getBuffer(Buf&: B.NodeIdPairBuffer); |
| 192 | return B; |
| 193 | }(); |
| 194 | |
| 195 | thread_local auto Allocators = |
| 196 | FunctionCallTrie::InitAllocatorsFromBuffers(Bufs&: Buffers); |
| 197 | |
| 198 | FunctionCallTrie T(Allocators); |
| 199 | |
| 200 | T.enterFunction(FId: 1, TSC: 1, CPU: 0); |
| 201 | T.enterFunction(FId: 2, TSC: 2, CPU: 0); |
| 202 | T.exitFunction(FId: 2, TSC: 3, CPU: 0); |
| 203 | T.exitFunction(FId: 1, TSC: 4, CPU: 0); |
| 204 | |
| 205 | profileCollectorService::post(Q: &BQ, T: std::move(t&: T), A: std::move(t&: Allocators), |
| 206 | B: std::move(t&: Buffers), TId: GetTid()); |
| 207 | } |
| 208 | |
| 209 | TEST(profileCollectorServiceTest, PostSerializeCollectMultipleThread) { |
| 210 | profilingFlags()->setDefaults(); |
| 211 | |
| 212 | profileCollectorService::reset(); |
| 213 | |
| 214 | std::thread t1(threadProcessing); |
| 215 | std::thread t2(threadProcessing); |
| 216 | |
| 217 | t1.join(); |
| 218 | t2.join(); |
| 219 | |
| 220 | // At this point, t1 and t2 are already done with what they were doing. |
| 221 | profileCollectorService::serialize(); |
| 222 | |
| 223 | // Ensure that we see two buffers. |
| 224 | auto B = profileCollectorService::nextBuffer({nullptr, 0}); |
| 225 | ValidateFileHeaderBlock(B); |
| 226 | |
| 227 | B = profileCollectorService::nextBuffer(B); |
| 228 | ValidateBlock(B); |
| 229 | |
| 230 | B = profileCollectorService::nextBuffer(B); |
| 231 | ValidateBlock(B); |
| 232 | } |
| 233 | |
| 234 | } // namespace |
| 235 | } // namespace __xray |
| 236 | |