236 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C++
		
	
	
	
//===-- profile_collector_test.cpp ----------------------------------------===//
 | 
						|
//
 | 
						|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 | 
						|
// See https://llvm.org/LICENSE.txt for license information.
 | 
						|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 | 
						|
//
 | 
						|
//===----------------------------------------------------------------------===//
 | 
						|
//
 | 
						|
// This file is a part of XRay, a function call tracing system.
 | 
						|
//
 | 
						|
//===----------------------------------------------------------------------===//
 | 
						|
#include "gtest/gtest.h"
 | 
						|
 | 
						|
#include "xray_profile_collector.h"
 | 
						|
#include "xray_profiling_flags.h"
 | 
						|
#include <cstdint>
 | 
						|
#include <cstring>
 | 
						|
#include <memory>
 | 
						|
#include <thread>
 | 
						|
#include <utility>
 | 
						|
#include <vector>
 | 
						|
 | 
						|
namespace __xray {
 | 
						|
namespace {
 | 
						|
 | 
						|
static constexpr auto kHeaderSize = 16u;
 | 
						|
 | 
						|
constexpr uptr ExpectedProfilingVersion = 0x20180424;
 | 
						|
 | 
						|
struct ExpectedProfilingFileHeader {
 | 
						|
  const u64 MagicBytes = 0x7872617970726f66; // Identifier for XRay profiling
 | 
						|
                                             // files 'xrayprof' in hex.
 | 
						|
  const u64 Version = ExpectedProfilingVersion;
 | 
						|
  u64 Timestamp = 0;
 | 
						|
  u64 PID = 0;
 | 
						|
};
 | 
						|
 | 
						|
void ValidateFileHeaderBlock(XRayBuffer B) {
 | 
						|
  ASSERT_NE(static_cast<const void *>(B.Data), nullptr);
 | 
						|
  ASSERT_EQ(B.Size, sizeof(ExpectedProfilingFileHeader));
 | 
						|
  typename std::aligned_storage<sizeof(ExpectedProfilingFileHeader)>::type
 | 
						|
      FileHeaderStorage;
 | 
						|
  ExpectedProfilingFileHeader ExpectedHeader;
 | 
						|
  std::memcpy(&FileHeaderStorage, B.Data, B.Size);
 | 
						|
  auto &FileHeader =
 | 
						|
      *reinterpret_cast<ExpectedProfilingFileHeader *>(&FileHeaderStorage);
 | 
						|
  ASSERT_EQ(ExpectedHeader.MagicBytes, FileHeader.MagicBytes);
 | 
						|
  ASSERT_EQ(ExpectedHeader.Version, FileHeader.Version);
 | 
						|
}
 | 
						|
 | 
						|
void ValidateBlock(XRayBuffer B) {
 | 
						|
  profilingFlags()->setDefaults();
 | 
						|
  ASSERT_NE(static_cast<const void *>(B.Data), nullptr);
 | 
						|
  ASSERT_NE(B.Size, 0u);
 | 
						|
  ASSERT_GE(B.Size, kHeaderSize);
 | 
						|
  // We look at the block size, the block number, and the thread ID to ensure
 | 
						|
  // that none of them are zero (or that the header data is laid out as we
 | 
						|
  // expect).
 | 
						|
  char LocalBuffer[kHeaderSize] = {};
 | 
						|
  internal_memcpy(LocalBuffer, B.Data, kHeaderSize);
 | 
						|
  u32 BlockSize = 0;
 | 
						|
  u32 BlockNumber = 0;
 | 
						|
  u64 ThreadId = 0;
 | 
						|
  internal_memcpy(&BlockSize, LocalBuffer, sizeof(u32));
 | 
						|
  internal_memcpy(&BlockNumber, LocalBuffer + sizeof(u32), sizeof(u32));
 | 
						|
  internal_memcpy(&ThreadId, LocalBuffer + (2 * sizeof(u32)), sizeof(u64));
 | 
						|
  ASSERT_NE(BlockSize, 0u);
 | 
						|
  ASSERT_GE(BlockNumber, 0u);
 | 
						|
  ASSERT_NE(ThreadId, 0u);
 | 
						|
}
 | 
						|
 | 
						|
std::tuple<u32, u32, u64> ParseBlockHeader(XRayBuffer B) {
 | 
						|
  char LocalBuffer[kHeaderSize] = {};
 | 
						|
  internal_memcpy(LocalBuffer, B.Data, kHeaderSize);
 | 
						|
  u32 BlockSize = 0;
 | 
						|
  u32 BlockNumber = 0;
 | 
						|
  u64 ThreadId = 0;
 | 
						|
  internal_memcpy(&BlockSize, LocalBuffer, sizeof(u32));
 | 
						|
  internal_memcpy(&BlockNumber, LocalBuffer + sizeof(u32), sizeof(u32));
 | 
						|
  internal_memcpy(&ThreadId, LocalBuffer + (2 * sizeof(u32)), sizeof(u64));
 | 
						|
  return std::make_tuple(BlockSize, BlockNumber, ThreadId);
 | 
						|
}
 | 
						|
 | 
						|
struct Profile {
 | 
						|
  int64_t CallCount;
 | 
						|
  int64_t CumulativeLocalTime;
 | 
						|
  std::vector<int32_t> Path;
 | 
						|
};
 | 
						|
 | 
						|
std::tuple<Profile, const char *> ParseProfile(const char *P) {
 | 
						|
  Profile Result;
 | 
						|
  // Read the path first, until we find a sentinel 0.
 | 
						|
  int32_t F;
 | 
						|
  do {
 | 
						|
    internal_memcpy(&F, P, sizeof(int32_t));
 | 
						|
    P += sizeof(int32_t);
 | 
						|
    Result.Path.push_back(F);
 | 
						|
  } while (F != 0);
 | 
						|
 | 
						|
  // Then read the CallCount.
 | 
						|
  internal_memcpy(&Result.CallCount, P, sizeof(int64_t));
 | 
						|
  P += sizeof(int64_t);
 | 
						|
 | 
						|
  // Then read the CumulativeLocalTime.
 | 
						|
  internal_memcpy(&Result.CumulativeLocalTime, P, sizeof(int64_t));
 | 
						|
  P += sizeof(int64_t);
 | 
						|
  return std::make_tuple(std::move(Result), P);
 | 
						|
}
 | 
						|
 | 
						|
TEST(profileCollectorServiceTest, PostSerializeCollect) {
 | 
						|
  profilingFlags()->setDefaults();
 | 
						|
  bool Success = false;
 | 
						|
  BufferQueue BQ(profilingFlags()->per_thread_allocator_max,
 | 
						|
                 profilingFlags()->buffers_max, Success);
 | 
						|
  ASSERT_EQ(Success, true);
 | 
						|
  FunctionCallTrie::Allocators::Buffers Buffers;
 | 
						|
  ASSERT_EQ(BQ.getBuffer(Buffers.NodeBuffer), BufferQueue::ErrorCode::Ok);
 | 
						|
  ASSERT_EQ(BQ.getBuffer(Buffers.RootsBuffer), BufferQueue::ErrorCode::Ok);
 | 
						|
  ASSERT_EQ(BQ.getBuffer(Buffers.ShadowStackBuffer),
 | 
						|
            BufferQueue::ErrorCode::Ok);
 | 
						|
  ASSERT_EQ(BQ.getBuffer(Buffers.NodeIdPairBuffer), BufferQueue::ErrorCode::Ok);
 | 
						|
  auto Allocators = FunctionCallTrie::InitAllocatorsFromBuffers(Buffers);
 | 
						|
  FunctionCallTrie T(Allocators);
 | 
						|
 | 
						|
  // Populate the trie with some data.
 | 
						|
  T.enterFunction(1, 1, 0);
 | 
						|
  T.enterFunction(2, 2, 0);
 | 
						|
  T.exitFunction(2, 3, 0);
 | 
						|
  T.exitFunction(1, 4, 0);
 | 
						|
 | 
						|
  // Reset the collector data structures.
 | 
						|
  profileCollectorService::reset();
 | 
						|
 | 
						|
  // Then we post the data to the global profile collector service.
 | 
						|
  profileCollectorService::post(&BQ, std::move(T), std::move(Allocators),
 | 
						|
                                std::move(Buffers), 1);
 | 
						|
 | 
						|
  // Then we serialize the data.
 | 
						|
  profileCollectorService::serialize();
 | 
						|
 | 
						|
  // Then we go through two buffers to see whether we're getting the data we
 | 
						|
  // expect. The first block must always be as large as a file header, which
 | 
						|
  // will have a fixed size.
 | 
						|
  auto B = profileCollectorService::nextBuffer({nullptr, 0});
 | 
						|
  ValidateFileHeaderBlock(B);
 | 
						|
 | 
						|
  B = profileCollectorService::nextBuffer(B);
 | 
						|
  ValidateBlock(B);
 | 
						|
  u32 BlockSize;
 | 
						|
  u32 BlockNum;
 | 
						|
  u64 ThreadId;
 | 
						|
  std::tie(BlockSize, BlockNum, ThreadId) = ParseBlockHeader(B);
 | 
						|
 | 
						|
  // We look at the serialized buffer to see whether the Trie we're expecting
 | 
						|
  // to see is there.
 | 
						|
  auto DStart = static_cast<const char *>(B.Data) + kHeaderSize;
 | 
						|
  std::vector<char> D(DStart, DStart + BlockSize);
 | 
						|
  B = profileCollectorService::nextBuffer(B);
 | 
						|
  ASSERT_EQ(B.Data, nullptr);
 | 
						|
  ASSERT_EQ(B.Size, 0u);
 | 
						|
 | 
						|
  Profile Profile1, Profile2;
 | 
						|
  auto P = static_cast<const char *>(D.data());
 | 
						|
  std::tie(Profile1, P) = ParseProfile(P);
 | 
						|
  std::tie(Profile2, P) = ParseProfile(P);
 | 
						|
 | 
						|
  ASSERT_NE(Profile1.Path.size(), Profile2.Path.size());
 | 
						|
  auto &P1 = Profile1.Path.size() < Profile2.Path.size() ? Profile2 : Profile1;
 | 
						|
  auto &P2 = Profile1.Path.size() < Profile2.Path.size() ? Profile1 : Profile2;
 | 
						|
  std::vector<int32_t> P1Expected = {2, 1, 0};
 | 
						|
  std::vector<int32_t> P2Expected = {1, 0};
 | 
						|
  ASSERT_EQ(P1.Path.size(), P1Expected.size());
 | 
						|
  ASSERT_EQ(P2.Path.size(), P2Expected.size());
 | 
						|
  ASSERT_EQ(P1.Path, P1Expected);
 | 
						|
  ASSERT_EQ(P2.Path, P2Expected);
 | 
						|
}
 | 
						|
 | 
						|
// We break out a function that will be run in multiple threads, one that will
 | 
						|
// use a thread local allocator, and will post the FunctionCallTrie to the
 | 
						|
// profileCollectorService. This simulates what the threads being profiled would
 | 
						|
// be doing anyway, but through the XRay logging implementation.
 | 
						|
void threadProcessing() {
 | 
						|
  static bool Success = false;
 | 
						|
  static BufferQueue BQ(profilingFlags()->per_thread_allocator_max,
 | 
						|
                        profilingFlags()->buffers_max, Success);
 | 
						|
  thread_local FunctionCallTrie::Allocators::Buffers Buffers = [] {
 | 
						|
    FunctionCallTrie::Allocators::Buffers B;
 | 
						|
    BQ.getBuffer(B.NodeBuffer);
 | 
						|
    BQ.getBuffer(B.RootsBuffer);
 | 
						|
    BQ.getBuffer(B.ShadowStackBuffer);
 | 
						|
    BQ.getBuffer(B.NodeIdPairBuffer);
 | 
						|
    return B;
 | 
						|
  }();
 | 
						|
 | 
						|
  thread_local auto Allocators =
 | 
						|
      FunctionCallTrie::InitAllocatorsFromBuffers(Buffers);
 | 
						|
 | 
						|
  FunctionCallTrie T(Allocators);
 | 
						|
 | 
						|
  T.enterFunction(1, 1, 0);
 | 
						|
  T.enterFunction(2, 2, 0);
 | 
						|
  T.exitFunction(2, 3, 0);
 | 
						|
  T.exitFunction(1, 4, 0);
 | 
						|
 | 
						|
  profileCollectorService::post(&BQ, std::move(T), std::move(Allocators),
 | 
						|
                                std::move(Buffers), GetTid());
 | 
						|
}
 | 
						|
 | 
						|
TEST(profileCollectorServiceTest, PostSerializeCollectMultipleThread) {
 | 
						|
  profilingFlags()->setDefaults();
 | 
						|
 | 
						|
  profileCollectorService::reset();
 | 
						|
 | 
						|
  std::thread t1(threadProcessing);
 | 
						|
  std::thread t2(threadProcessing);
 | 
						|
 | 
						|
  t1.join();
 | 
						|
  t2.join();
 | 
						|
 | 
						|
  // At this point, t1 and t2 are already done with what they were doing.
 | 
						|
  profileCollectorService::serialize();
 | 
						|
 | 
						|
  // Ensure that we see two buffers.
 | 
						|
  auto B = profileCollectorService::nextBuffer({nullptr, 0});
 | 
						|
  ValidateFileHeaderBlock(B);
 | 
						|
 | 
						|
  B = profileCollectorService::nextBuffer(B);
 | 
						|
  ValidateBlock(B);
 | 
						|
 | 
						|
  B = profileCollectorService::nextBuffer(B);
 | 
						|
  ValidateBlock(B);
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
} // namespace __xray
 |