1 //===-- xray_function_call_trie.h ------------------------------*- C++ -*-===//
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
7 //===----------------------------------------------------------------------===//
9 // This file is a part of XRay, a dynamic runtime instrumentation system.
11 // This file defines the interface for a function call trie.
13 //===----------------------------------------------------------------------===//
14 #ifndef XRAY_FUNCTION_CALL_TRIE_H
15 #define XRAY_FUNCTION_CALL_TRIE_H
17 #include "xray_buffer_queue.h"
18 #include "xray_defs.h"
19 #include "xray_profiling_flags.h"
20 #include "xray_segmented_array.h"
22 #include <memory> // For placement new.
27 /// A FunctionCallTrie represents the stack traces of XRay instrumented
28 /// functions that we've encountered, where a node corresponds to a function and
29 /// the path from the root to the node its stack trace. Each node in the trie
30 /// will contain some useful values, including:
32 /// * The cumulative amount of time spent in this particular node/stack.
33 /// * The number of times this stack has appeared.
34 /// * A histogram of latencies for that particular node.
36 /// Each node in the trie will also contain a list of callees, represented using
37 /// a Array<NodeIdPair> -- each NodeIdPair instance will contain the function
38 /// ID of the callee, and a pointer to the node.
40 /// If we visualise this data structure, we'll find the following potential
43 /// [function id node] -> [callees] [cumulative time]
44 /// [call counter] [latency histogram]
46 /// As an example, when we have a function in this pseudocode:
51 /// for i := 1..N { j() }
54 /// We may end up with a trie of the following form:
56 /// f -> [ g, h, j ] [...] [1] [...]
57 /// g -> [ ... ] [...] [1] [...]
58 /// h -> [ ... ] [...] [1] [...]
59 /// j -> [ ... ] [...] [N] [...]
61 /// If for instance the function g() called j() like so:
64 /// for i := 1..10 { j() }
67 /// We'll find the following updated trie:
69 /// f -> [ g, h, j ] [...] [1] [...]
70 /// g -> [ j' ] [...] [1] [...]
71 /// h -> [ ... ] [...] [1] [...]
72 /// j -> [ ... ] [...] [N] [...]
73 /// j' -> [ ... ] [...] [10] [...]
75 /// Note that we'll have a new node representing the path `f -> g -> j'` with
76 /// isolated data. This isolation gives us a means of representing the stack
77 /// traces as a path, as opposed to a key in a table. The alternative
78 /// implementation here would be to use a separate table for the path, and use
79 /// hashes of the path as an identifier to accumulate the information. We've
80 /// moved away from this approach as it takes a lot of time to compute the hash
81 /// every time we need to update a function's call information as we're handling
82 /// the entry and exit events.
84 /// This approach allows us to maintain a shadow stack, which represents the
85 /// currently executing path, and on function exits quickly compute the amount
86 /// of time elapsed from the entry, then update the counters for the node
87 /// already represented in the trie. This necessitates an efficient
88 /// representation of the various data structures (the list of callees must be
89 /// cache-aware and efficient to look up, and the histogram must be compact and
90 /// quick to update) to enable us to keep the overheads of this implementation
92 class FunctionCallTrie
{
96 // We use a NodeIdPair type instead of a std::pair<...> to not rely on the
97 // standard library types in this header.
103 using NodeIdPairArray
= Array
<NodeIdPair
>;
104 using NodeIdPairAllocatorType
= NodeIdPairArray::AllocatorType
;
106 // A Node in the FunctionCallTrie gives us a list of callees, the cumulative
107 // number of times this node actually appeared, the cumulative amount of time
108 // for this particular node including its children call times, and just the
109 // local time spent on this node. Each Node will have the ID of the XRay
110 // instrumented function that it is associated to.
113 NodeIdPairArray Callees
;
115 uint64_t CumulativeLocalTime
; // Typically in TSC deltas, not wall-time.
118 // TODO: Include the compact histogram.
122 struct ShadowStackEntry
{
128 using NodeArray
= Array
<Node
>;
129 using RootArray
= Array
<Node
*>;
130 using ShadowStackArray
= Array
<ShadowStackEntry
>;
133 // We collate the allocators we need into a single struct, as a convenience to
134 // allow us to initialize these as a group.
136 using NodeAllocatorType
= NodeArray::AllocatorType
;
137 using RootAllocatorType
= RootArray::AllocatorType
;
138 using ShadowStackAllocatorType
= ShadowStackArray::AllocatorType
;
140 // Use hosted aligned storage members to allow for trivial move and init.
141 // This also allows us to sidestep the potential-failing allocation issue.
142 typename
std::aligned_storage
<sizeof(NodeAllocatorType
),
143 alignof(NodeAllocatorType
)>::type
144 NodeAllocatorStorage
;
145 typename
std::aligned_storage
<sizeof(RootAllocatorType
),
146 alignof(RootAllocatorType
)>::type
147 RootAllocatorStorage
;
148 typename
std::aligned_storage
<sizeof(ShadowStackAllocatorType
),
149 alignof(ShadowStackAllocatorType
)>::type
150 ShadowStackAllocatorStorage
;
151 typename
std::aligned_storage
<sizeof(NodeIdPairAllocatorType
),
152 alignof(NodeIdPairAllocatorType
)>::type
153 NodeIdPairAllocatorStorage
;
155 NodeAllocatorType
*NodeAllocator
= nullptr;
156 RootAllocatorType
*RootAllocator
= nullptr;
157 ShadowStackAllocatorType
*ShadowStackAllocator
= nullptr;
158 NodeIdPairAllocatorType
*NodeIdPairAllocator
= nullptr;
160 Allocators() = default;
161 Allocators(const Allocators
&) = delete;
162 Allocators
&operator=(const Allocators
&) = delete;
165 BufferQueue::Buffer NodeBuffer
;
166 BufferQueue::Buffer RootsBuffer
;
167 BufferQueue::Buffer ShadowStackBuffer
;
168 BufferQueue::Buffer NodeIdPairBuffer
;
171 explicit Allocators(Buffers
&B
) XRAY_NEVER_INSTRUMENT
{
172 new (&NodeAllocatorStorage
)
173 NodeAllocatorType(B
.NodeBuffer
.Data
, B
.NodeBuffer
.Size
);
175 reinterpret_cast<NodeAllocatorType
*>(&NodeAllocatorStorage
);
177 new (&RootAllocatorStorage
)
178 RootAllocatorType(B
.RootsBuffer
.Data
, B
.RootsBuffer
.Size
);
180 reinterpret_cast<RootAllocatorType
*>(&RootAllocatorStorage
);
182 new (&ShadowStackAllocatorStorage
) ShadowStackAllocatorType(
183 B
.ShadowStackBuffer
.Data
, B
.ShadowStackBuffer
.Size
);
184 ShadowStackAllocator
= reinterpret_cast<ShadowStackAllocatorType
*>(
185 &ShadowStackAllocatorStorage
);
187 new (&NodeIdPairAllocatorStorage
) NodeIdPairAllocatorType(
188 B
.NodeIdPairBuffer
.Data
, B
.NodeIdPairBuffer
.Size
);
189 NodeIdPairAllocator
= reinterpret_cast<NodeIdPairAllocatorType
*>(
190 &NodeIdPairAllocatorStorage
);
193 explicit Allocators(uptr Max
) XRAY_NEVER_INSTRUMENT
{
194 new (&NodeAllocatorStorage
) NodeAllocatorType(Max
);
196 reinterpret_cast<NodeAllocatorType
*>(&NodeAllocatorStorage
);
198 new (&RootAllocatorStorage
) RootAllocatorType(Max
);
200 reinterpret_cast<RootAllocatorType
*>(&RootAllocatorStorage
);
202 new (&ShadowStackAllocatorStorage
) ShadowStackAllocatorType(Max
);
203 ShadowStackAllocator
= reinterpret_cast<ShadowStackAllocatorType
*>(
204 &ShadowStackAllocatorStorage
);
206 new (&NodeIdPairAllocatorStorage
) NodeIdPairAllocatorType(Max
);
207 NodeIdPairAllocator
= reinterpret_cast<NodeIdPairAllocatorType
*>(
208 &NodeIdPairAllocatorStorage
);
211 Allocators(Allocators
&&O
) XRAY_NEVER_INSTRUMENT
{
212 // Here we rely on the safety of memcpy'ing contents of the storage
213 // members, and then pointing the source pointers to nullptr.
214 internal_memcpy(&NodeAllocatorStorage
, &O
.NodeAllocatorStorage
,
215 sizeof(NodeAllocatorType
));
216 internal_memcpy(&RootAllocatorStorage
, &O
.RootAllocatorStorage
,
217 sizeof(RootAllocatorType
));
218 internal_memcpy(&ShadowStackAllocatorStorage
,
219 &O
.ShadowStackAllocatorStorage
,
220 sizeof(ShadowStackAllocatorType
));
221 internal_memcpy(&NodeIdPairAllocatorStorage
,
222 &O
.NodeIdPairAllocatorStorage
,
223 sizeof(NodeIdPairAllocatorType
));
226 reinterpret_cast<NodeAllocatorType
*>(&NodeAllocatorStorage
);
228 reinterpret_cast<RootAllocatorType
*>(&RootAllocatorStorage
);
229 ShadowStackAllocator
= reinterpret_cast<ShadowStackAllocatorType
*>(
230 &ShadowStackAllocatorStorage
);
231 NodeIdPairAllocator
= reinterpret_cast<NodeIdPairAllocatorType
*>(
232 &NodeIdPairAllocatorStorage
);
234 O
.NodeAllocator
= nullptr;
235 O
.RootAllocator
= nullptr;
236 O
.ShadowStackAllocator
= nullptr;
237 O
.NodeIdPairAllocator
= nullptr;
240 Allocators
&operator=(Allocators
&&O
) XRAY_NEVER_INSTRUMENT
{
241 // When moving into an existing instance, we ensure that we clean up the
242 // current allocators.
244 NodeAllocator
->~NodeAllocatorType();
245 if (O
.NodeAllocator
) {
246 new (&NodeAllocatorStorage
)
247 NodeAllocatorType(std::move(*O
.NodeAllocator
));
249 reinterpret_cast<NodeAllocatorType
*>(&NodeAllocatorStorage
);
250 O
.NodeAllocator
= nullptr;
252 NodeAllocator
= nullptr;
256 RootAllocator
->~RootAllocatorType();
257 if (O
.RootAllocator
) {
258 new (&RootAllocatorStorage
)
259 RootAllocatorType(std::move(*O
.RootAllocator
));
261 reinterpret_cast<RootAllocatorType
*>(&RootAllocatorStorage
);
262 O
.RootAllocator
= nullptr;
264 RootAllocator
= nullptr;
267 if (ShadowStackAllocator
)
268 ShadowStackAllocator
->~ShadowStackAllocatorType();
269 if (O
.ShadowStackAllocator
) {
270 new (&ShadowStackAllocatorStorage
)
271 ShadowStackAllocatorType(std::move(*O
.ShadowStackAllocator
));
272 ShadowStackAllocator
= reinterpret_cast<ShadowStackAllocatorType
*>(
273 &ShadowStackAllocatorStorage
);
274 O
.ShadowStackAllocator
= nullptr;
276 ShadowStackAllocator
= nullptr;
279 if (NodeIdPairAllocator
)
280 NodeIdPairAllocator
->~NodeIdPairAllocatorType();
281 if (O
.NodeIdPairAllocator
) {
282 new (&NodeIdPairAllocatorStorage
)
283 NodeIdPairAllocatorType(std::move(*O
.NodeIdPairAllocator
));
284 NodeIdPairAllocator
= reinterpret_cast<NodeIdPairAllocatorType
*>(
285 &NodeIdPairAllocatorStorage
);
286 O
.NodeIdPairAllocator
= nullptr;
288 NodeIdPairAllocator
= nullptr;
294 ~Allocators() XRAY_NEVER_INSTRUMENT
{
295 if (NodeAllocator
!= nullptr)
296 NodeAllocator
->~NodeAllocatorType();
297 if (RootAllocator
!= nullptr)
298 RootAllocator
->~RootAllocatorType();
299 if (ShadowStackAllocator
!= nullptr)
300 ShadowStackAllocator
->~ShadowStackAllocatorType();
301 if (NodeIdPairAllocator
!= nullptr)
302 NodeIdPairAllocator
->~NodeIdPairAllocatorType();
306 static Allocators
InitAllocators() XRAY_NEVER_INSTRUMENT
{
307 return InitAllocatorsCustom(profilingFlags()->per_thread_allocator_max
);
310 static Allocators
InitAllocatorsCustom(uptr Max
) XRAY_NEVER_INSTRUMENT
{
316 InitAllocatorsFromBuffers(Allocators::Buffers
&Bufs
) XRAY_NEVER_INSTRUMENT
{
324 ShadowStackArray ShadowStack
;
325 NodeIdPairAllocatorType
*NodeIdPairAllocator
;
326 uint32_t OverflowedFunctions
;
329 explicit FunctionCallTrie(const Allocators
&A
) XRAY_NEVER_INSTRUMENT
330 : Nodes(*A
.NodeAllocator
),
331 Roots(*A
.RootAllocator
),
332 ShadowStack(*A
.ShadowStackAllocator
),
333 NodeIdPairAllocator(A
.NodeIdPairAllocator
),
334 OverflowedFunctions(0) {}
336 FunctionCallTrie() = delete;
337 FunctionCallTrie(const FunctionCallTrie
&) = delete;
338 FunctionCallTrie
&operator=(const FunctionCallTrie
&) = delete;
340 FunctionCallTrie(FunctionCallTrie
&&O
) XRAY_NEVER_INSTRUMENT
341 : Nodes(std::move(O
.Nodes
)),
342 Roots(std::move(O
.Roots
)),
343 ShadowStack(std::move(O
.ShadowStack
)),
344 NodeIdPairAllocator(O
.NodeIdPairAllocator
),
345 OverflowedFunctions(O
.OverflowedFunctions
) {}
347 FunctionCallTrie
&operator=(FunctionCallTrie
&&O
) XRAY_NEVER_INSTRUMENT
{
348 Nodes
= std::move(O
.Nodes
);
349 Roots
= std::move(O
.Roots
);
350 ShadowStack
= std::move(O
.ShadowStack
);
351 NodeIdPairAllocator
= O
.NodeIdPairAllocator
;
352 OverflowedFunctions
= O
.OverflowedFunctions
;
356 ~FunctionCallTrie() XRAY_NEVER_INSTRUMENT
{}
358 void enterFunction(const int32_t FId
, uint64_t TSC
,
359 uint16_t CPU
) XRAY_NEVER_INSTRUMENT
{
362 // If we're already overflowed the function call stack, do not bother
363 // attempting to record any more function entries.
364 if (UNLIKELY(OverflowedFunctions
)) {
365 ++OverflowedFunctions
;
369 // If this is the first function we've encountered, we want to set up the
370 // node(s) and treat it as a root.
371 if (UNLIKELY(ShadowStack
.empty())) {
372 auto *NewRoot
= Nodes
.AppendEmplace(
373 nullptr, NodeIdPairArray(*NodeIdPairAllocator
), 0u, 0u, FId
);
374 if (UNLIKELY(NewRoot
== nullptr))
376 if (Roots
.AppendEmplace(NewRoot
) == nullptr) {
380 if (ShadowStack
.AppendEmplace(TSC
, NewRoot
, CPU
) == nullptr) {
383 ++OverflowedFunctions
;
389 // From this point on, we require that the stack is not empty.
390 DCHECK(!ShadowStack
.empty());
391 auto TopNode
= ShadowStack
.back().NodePtr
;
392 DCHECK_NE(TopNode
, nullptr);
394 // If we've seen this callee before, then we access that node and place that
395 // on the top of the stack.
396 auto* Callee
= TopNode
->Callees
.find_element(
397 [FId
](const NodeIdPair
&NR
) { return NR
.FId
== FId
; });
398 if (Callee
!= nullptr) {
399 CHECK_NE(Callee
->NodePtr
, nullptr);
400 if (ShadowStack
.AppendEmplace(TSC
, Callee
->NodePtr
, CPU
) == nullptr)
401 ++OverflowedFunctions
;
405 // This means we've never seen this stack before, create a new node here.
406 auto* NewNode
= Nodes
.AppendEmplace(
407 TopNode
, NodeIdPairArray(*NodeIdPairAllocator
), 0u, 0u, FId
);
408 if (UNLIKELY(NewNode
== nullptr))
410 DCHECK_NE(NewNode
, nullptr);
411 TopNode
->Callees
.AppendEmplace(NewNode
, FId
);
412 if (ShadowStack
.AppendEmplace(TSC
, NewNode
, CPU
) == nullptr)
413 ++OverflowedFunctions
;
417 void exitFunction(int32_t FId
, uint64_t TSC
,
418 uint16_t CPU
) XRAY_NEVER_INSTRUMENT
{
419 // If we're exiting functions that have "overflowed" or don't fit into the
420 // stack due to allocator constraints, we then decrement that count first.
421 if (OverflowedFunctions
) {
422 --OverflowedFunctions
;
426 // When we exit a function, we look up the ShadowStack to see whether we've
427 // entered this function before. We do as little processing here as we can,
428 // since most of the hard work would have already been done at function
430 uint64_t CumulativeTreeTime
= 0;
432 while (!ShadowStack
.empty()) {
433 const auto &Top
= ShadowStack
.back();
434 auto TopNode
= Top
.NodePtr
;
435 DCHECK_NE(TopNode
, nullptr);
437 // We may encounter overflow on the TSC we're provided, which may end up
438 // being less than the TSC when we first entered the function.
440 // To get the accurate measurement of cycles, we need to check whether
441 // we've overflowed (TSC < Top.EntryTSC) and then account the difference
442 // between the entry TSC and the max for the TSC counter (max of uint64_t)
443 // then add the value of TSC. We can prove that the maximum delta we will
444 // get is at most the 64-bit unsigned value, since the difference between
445 // a TSC of 0 and a Top.EntryTSC of 1 is (numeric_limits<uint64_t>::max()
448 // NOTE: This assumes that TSCs are synchronised across CPUs.
449 // TODO: Count the number of times we've seen CPU migrations.
452 ? (std::numeric_limits
<uint64_t>::max() - Top
.EntryTSC
) + TSC
453 : TSC
- Top
.EntryTSC
;
454 TopNode
->CallCount
++;
455 TopNode
->CumulativeLocalTime
+= LocalTime
- CumulativeTreeTime
;
456 CumulativeTreeTime
+= LocalTime
;
459 // TODO: Update the histogram for the node.
460 if (TopNode
->FId
== FId
)
465 const RootArray
&getRoots() const XRAY_NEVER_INSTRUMENT
{ return Roots
; }
467 // The deepCopyInto operation will update the provided FunctionCallTrie by
468 // re-creating the contents of this particular FunctionCallTrie in the other
469 // FunctionCallTrie. It will do this using a Depth First Traversal from the
470 // roots, and while doing so recreating the traversal in the provided
473 // This operation will *not* destroy the state in `O`, and thus may cause some
474 // duplicate entries in `O` if it is not empty.
476 // This function is *not* thread-safe, and may require external
477 // synchronisation of both "this" and |O|.
479 // This function must *not* be called with a non-empty FunctionCallTrie |O|.
480 void deepCopyInto(FunctionCallTrie
&O
) const XRAY_NEVER_INSTRUMENT
{
481 DCHECK(O
.getRoots().empty());
483 // We then push the root into a stack, to use as the parent marker for new
484 // nodes we push in as we're traversing depth-first down the call tree.
485 struct NodeAndParent
{
486 FunctionCallTrie::Node
*Node
;
487 FunctionCallTrie::Node
*NewNode
;
489 using Stack
= Array
<NodeAndParent
>;
491 typename
Stack::AllocatorType
StackAllocator(
492 profilingFlags()->stack_allocator_max
);
493 Stack
DFSStack(StackAllocator
);
495 for (const auto Root
: getRoots()) {
496 // Add a node in O for this root.
497 auto NewRoot
= O
.Nodes
.AppendEmplace(
498 nullptr, NodeIdPairArray(*O
.NodeIdPairAllocator
), Root
->CallCount
,
499 Root
->CumulativeLocalTime
, Root
->FId
);
501 // Because we cannot allocate more memory we should bail out right away.
502 if (UNLIKELY(NewRoot
== nullptr))
505 if (UNLIKELY(O
.Roots
.Append(NewRoot
) == nullptr))
508 // TODO: Figure out what to do if we fail to allocate any more stack
509 // space. Maybe warn or report once?
510 if (DFSStack
.AppendEmplace(Root
, NewRoot
) == nullptr)
512 while (!DFSStack
.empty()) {
513 NodeAndParent NP
= DFSStack
.back();
514 DCHECK_NE(NP
.Node
, nullptr);
515 DCHECK_NE(NP
.NewNode
, nullptr);
517 for (const auto Callee
: NP
.Node
->Callees
) {
518 auto NewNode
= O
.Nodes
.AppendEmplace(
519 NP
.NewNode
, NodeIdPairArray(*O
.NodeIdPairAllocator
),
520 Callee
.NodePtr
->CallCount
, Callee
.NodePtr
->CumulativeLocalTime
,
522 if (UNLIKELY(NewNode
== nullptr))
524 if (UNLIKELY(NP
.NewNode
->Callees
.AppendEmplace(NewNode
, Callee
.FId
) ==
527 if (UNLIKELY(DFSStack
.AppendEmplace(Callee
.NodePtr
, NewNode
) ==
535 // The mergeInto operation will update the provided FunctionCallTrie by
536 // traversing the current trie's roots and updating (i.e. merging) the data in
537 // the nodes with the data in the target's nodes. If the node doesn't exist in
538 // the provided trie, we add a new one in the right position, and inherit the
539 // data from the original (current) trie, along with all its callees.
541 // This function is *not* thread-safe, and may require external
542 // synchronisation of both "this" and |O|.
543 void mergeInto(FunctionCallTrie
&O
) const XRAY_NEVER_INSTRUMENT
{
544 struct NodeAndTarget
{
545 FunctionCallTrie::Node
*OrigNode
;
546 FunctionCallTrie::Node
*TargetNode
;
548 using Stack
= Array
<NodeAndTarget
>;
549 typename
Stack::AllocatorType
StackAllocator(
550 profilingFlags()->stack_allocator_max
);
551 Stack
DFSStack(StackAllocator
);
553 for (const auto Root
: getRoots()) {
554 Node
*TargetRoot
= nullptr;
555 auto R
= O
.Roots
.find_element(
556 [&](const Node
*Node
) { return Node
->FId
== Root
->FId
; });
558 TargetRoot
= O
.Nodes
.AppendEmplace(
559 nullptr, NodeIdPairArray(*O
.NodeIdPairAllocator
), 0u, 0u,
561 if (UNLIKELY(TargetRoot
== nullptr))
564 O
.Roots
.Append(TargetRoot
);
569 DFSStack
.AppendEmplace(Root
, TargetRoot
);
570 while (!DFSStack
.empty()) {
571 NodeAndTarget NT
= DFSStack
.back();
572 DCHECK_NE(NT
.OrigNode
, nullptr);
573 DCHECK_NE(NT
.TargetNode
, nullptr);
575 // TODO: Update the histogram as well when we have it ready.
576 NT
.TargetNode
->CallCount
+= NT
.OrigNode
->CallCount
;
577 NT
.TargetNode
->CumulativeLocalTime
+= NT
.OrigNode
->CumulativeLocalTime
;
578 for (const auto Callee
: NT
.OrigNode
->Callees
) {
579 auto TargetCallee
= NT
.TargetNode
->Callees
.find_element(
580 [&](const FunctionCallTrie::NodeIdPair
&C
) {
581 return C
.FId
== Callee
.FId
;
583 if (TargetCallee
== nullptr) {
584 auto NewTargetNode
= O
.Nodes
.AppendEmplace(
585 NT
.TargetNode
, NodeIdPairArray(*O
.NodeIdPairAllocator
), 0u, 0u,
588 if (UNLIKELY(NewTargetNode
== nullptr))
592 NT
.TargetNode
->Callees
.AppendEmplace(NewTargetNode
, Callee
.FId
);
594 DFSStack
.AppendEmplace(Callee
.NodePtr
, TargetCallee
->NodePtr
);
601 } // namespace __xray
603 #endif // XRAY_FUNCTION_CALL_TRIE_H