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 | typename std::aligned_storage<sizeof(ExpectedProfilingFileHeader)>::type |
42 | ; |
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 | |