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 kHeaderSize = 16u;
27 
28 constexpr uptr ExpectedProfilingVersion = 0x20180424;
29 
30 struct ExpectedProfilingFileHeader {
31   const u64 MagicBytes = 0x7872617970726f66; // Identifier for XRay profiling
32                                              // files 'xrayprof' in hex.
33   const u64 Version = ExpectedProfilingVersion;
34   u64 Timestamp = 0;
35   u64 PID = 0;
36 };
37 
ValidateFileHeaderBlock(XRayBuffer B)38 void ValidateFileHeaderBlock(XRayBuffer B) {
39   ASSERT_NE(static_cast<const void *>(B.Data), nullptr);
40   ASSERT_EQ(B.Size, sizeof(ExpectedProfilingFileHeader));
41   typename std::aligned_storage<sizeof(ExpectedProfilingFileHeader)>::type
42       FileHeaderStorage;
43   ExpectedProfilingFileHeader ExpectedHeader;
44   std::memcpy(&FileHeaderStorage, B.Data, B.Size);
45   auto &FileHeader =
46       *reinterpret_cast<ExpectedProfilingFileHeader *>(&FileHeaderStorage);
47   ASSERT_EQ(ExpectedHeader.MagicBytes, FileHeader.MagicBytes);
48   ASSERT_EQ(ExpectedHeader.Version, FileHeader.Version);
49 }
50 
ValidateBlock(XRayBuffer B)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(LocalBuffer, B.Data, kHeaderSize);
61   u32 BlockSize = 0;
62   u32 BlockNumber = 0;
63   u64 ThreadId = 0;
64   internal_memcpy(&BlockSize, LocalBuffer, sizeof(u32));
65   internal_memcpy(&BlockNumber, LocalBuffer + sizeof(u32), sizeof(u32));
66   internal_memcpy(&ThreadId, LocalBuffer + (2 * sizeof(u32)), sizeof(u64));
67   ASSERT_NE(BlockSize, 0u);
68   ASSERT_GE(BlockNumber, 0u);
69   ASSERT_NE(ThreadId, 0u);
70 }
71 
ParseBlockHeader(XRayBuffer B)72 std::tuple<u32, u32, u64> ParseBlockHeader(XRayBuffer B) {
73   char LocalBuffer[kHeaderSize] = {};
74   internal_memcpy(LocalBuffer, B.Data, kHeaderSize);
75   u32 BlockSize = 0;
76   u32 BlockNumber = 0;
77   u64 ThreadId = 0;
78   internal_memcpy(&BlockSize, LocalBuffer, sizeof(u32));
79   internal_memcpy(&BlockNumber, LocalBuffer + sizeof(u32), sizeof(u32));
80   internal_memcpy(&ThreadId, LocalBuffer + (2 * sizeof(u32)), sizeof(u64));
81   return std::make_tuple(BlockSize, BlockNumber, ThreadId);
82 }
83 
84 struct Profile {
85   int64_t CallCount;
86   int64_t CumulativeLocalTime;
87   std::vector<int32_t> Path;
88 };
89 
ParseProfile(const char * P)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(&F, P, sizeof(int32_t));
96     P += sizeof(int32_t);
97     Result.Path.push_back(F);
98   } while (F != 0);
99 
100   // Then read the CallCount.
101   internal_memcpy(&Result.CallCount, P, sizeof(int64_t));
102   P += sizeof(int64_t);
103 
104   // Then read the CumulativeLocalTime.
105   internal_memcpy(&Result.CumulativeLocalTime, P, sizeof(int64_t));
106   P += sizeof(int64_t);
107   return std::make_tuple(std::move(Result), P);
108 }
109 
TEST(profileCollectorServiceTest,PostSerializeCollect)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.
threadProcessing()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(B.NodeBuffer);
189     BQ.getBuffer(B.RootsBuffer);
190     BQ.getBuffer(B.ShadowStackBuffer);
191     BQ.getBuffer(B.NodeIdPairBuffer);
192     return B;
193   }();
194 
195   thread_local auto Allocators =
196       FunctionCallTrie::InitAllocatorsFromBuffers(Buffers);
197 
198   FunctionCallTrie T(Allocators);
199 
200   T.enterFunction(1, 1, 0);
201   T.enterFunction(2, 2, 0);
202   T.exitFunction(2, 3, 0);
203   T.exitFunction(1, 4, 0);
204 
205   profileCollectorService::post(&BQ, std::move(T), std::move(Allocators),
206                                 std::move(Buffers), GetTid());
207 }
208 
TEST(profileCollectorServiceTest,PostSerializeCollectMultipleThread)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