[XRay][llvm] Load XRay Profiles

Summary:
This change implements the profile loading functionality in LLVM to
support XRay's profiling mode in compiler-rt.

We introduce a type named `llvm::xray::Profile` which allows building a
profile representation. We can load an XRay profile from a file to build
Profile instances, or do it manually through the Profile type's API.

The intent is to get the `llvm-xray` tool to generate `Profile`
instances and use that as the common abstraction through which all
conversion and analysis can be done. In the future we can generate
`Profile` instances from `Trace` instances as well, through conversion
functions.

Some of the key operations supported by the `Profile` API are:

- Path interning (`Profile::internPath(...)`) which returns a unique path
  identifier.

- Block appending (`Profile::addBlock(...)`) to add thread-associated
  profile information.

- Path ID to Path lookup (`Profile::expandPath(...)`) to look up a
  PathID and return the original interned path.

- Block iteration.

A 'Path' in this context represents the function call stack in
leaf-to-root order. This is represented as a path in an internally
managed prefix tree in the `Profile` instance. Having a handle (PathID)
to identify the unique Paths we encounter for a particular Profile
allows us to reduce the amount of memory required to associate profile
data to a particular Path.

This is the first of a series of patches to migrate the `llvm-stacks`
tool towards using a single profile representation.

Depends on D48653.

Reviewers: kpw, eizan

Reviewed By: kpw

Subscribers: kpw, thakis, mgorny, llvm-commits, hiraditya

Differential Revision: https://reviews.llvm.org/D48370

llvm-svn: 341012
diff --git a/llvm/unittests/XRay/ProfileTest.cpp b/llvm/unittests/XRay/ProfileTest.cpp
new file mode 100644
index 0000000..8486b7b
--- /dev/null
+++ b/llvm/unittests/XRay/ProfileTest.cpp
@@ -0,0 +1,267 @@
+//===- ProfileTest.cpp - XRay Profile unit tests ----------------*- C++ -*-===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "llvm/XRay/Profile.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include <numeric>
+
+namespace llvm {
+namespace xray {
+namespace {
+
+using ::testing::AllOf;
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::Field;
+using ::testing::Not;
+using ::testing::Pair;
+using ::testing::UnorderedElementsAre;
+
+TEST(ProfileTest, CreateProfile) { Profile P; }
+
+TEST(ProfileTest, InternPath) {
+  Profile P;
+  auto Path0 = P.internPath({3, 2, 1});
+  auto Path1 = P.internPath({3, 2, 1});
+  auto Path2 = P.internPath({2, 1});
+  EXPECT_THAT(Path0, Eq(Path1));
+  EXPECT_THAT(Path0, Not(Eq(Path2)));
+}
+
+TEST(ProfileTest, ExpandPath) {
+  Profile P;
+  auto PathID = P.internPath({3, 2, 1});
+  auto PathOrError = P.expandPath(PathID);
+  if (!PathOrError)
+    FAIL() << "Error: " << PathOrError.takeError();
+  EXPECT_THAT(PathOrError.get(), ElementsAre(3, 2, 1));
+}
+
+TEST(ProfileTest, AddBlocks) {
+  Profile P;
+  // Expect an error on adding empty blocks.
+  EXPECT_TRUE(errorToBool(P.addBlock({})));
+
+  // Thread blocks may not be empty.
+  EXPECT_TRUE(errorToBool(P.addBlock({1, {}})));
+
+  // Thread blocks with data must succeed.
+  EXPECT_FALSE(errorToBool(P.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {
+                         {P.internPath({2, 1}), Profile::Data{1, 1000}},
+                         {P.internPath({3, 2, 1}), Profile::Data{10, 100}},
+                     }})));
+}
+
+TEST(ProfileTest, CopyProfile) {
+  Profile P0, P1;
+  EXPECT_FALSE(errorToBool(P0.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {
+                         {P0.internPath({2, 1}), Profile::Data{1, 1000}},
+                         {P0.internPath({3, 2, 1}), Profile::Data{10, 100}},
+                     }})));
+  P1 = P0;
+  EXPECT_THAT(
+      P1, UnorderedElementsAre(AllOf(
+              Field(&Profile::Block::Thread, Eq(Profile::ThreadID{1})),
+              Field(&Profile::Block::PathData,
+                    UnorderedElementsAre(
+                        Pair(P1.internPath({2, 1}),
+                             AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                   Field(&Profile::Data::CumulativeLocalTime,
+                                         Eq(1000u)))),
+                        Pair(P1.internPath({3, 2, 1}),
+                             AllOf(Field(&Profile::Data::CallCount, Eq(10u)),
+                                   Field(&Profile::Data::CumulativeLocalTime,
+                                         Eq(100u)))))))));
+}
+
+TEST(ProfileTest, MoveProfile) {
+  Profile P0, P1;
+  EXPECT_FALSE(errorToBool(P0.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {
+                         {P0.internPath({2, 1}), Profile::Data{1, 1000}},
+                         {P0.internPath({3, 2, 1}), Profile::Data{10, 100}},
+                     }})));
+  P1 = std::move(P0);
+  EXPECT_THAT(
+      P1, UnorderedElementsAre(AllOf(
+              Field(&Profile::Block::Thread, Eq(Profile::ThreadID{1})),
+              Field(&Profile::Block::PathData,
+                    UnorderedElementsAre(
+                        Pair(P1.internPath({2, 1}),
+                             AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                   Field(&Profile::Data::CumulativeLocalTime,
+                                         Eq(1000u)))),
+                        Pair(P1.internPath({3, 2, 1}),
+                             AllOf(Field(&Profile::Data::CallCount, Eq(10u)),
+                                   Field(&Profile::Data::CumulativeLocalTime,
+                                         Eq(100u)))))))));
+  EXPECT_THAT(P0, UnorderedElementsAre());
+}
+
+TEST(ProfileTest, MergeProfilesByThread) {
+  Profile P0, P1;
+
+  // Set up the blocks for two different threads in P0.
+  EXPECT_FALSE(errorToBool(P0.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {{P0.internPath({2, 1}), Profile::Data{1, 1000}},
+                      {P0.internPath({4, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(P0.addBlock(
+      Profile::Block{Profile::ThreadID{2},
+                     {{P0.internPath({3, 1}), Profile::Data{1, 1000}}}})));
+
+  // Set up the blocks for two different threads in P1.
+  EXPECT_FALSE(errorToBool(P1.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {{P1.internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(P1.addBlock(
+      Profile::Block{Profile::ThreadID{2},
+                     {{P1.internPath({3, 1}), Profile::Data{1, 1000}},
+                      {P1.internPath({4, 1}), Profile::Data{1, 1000}}}})));
+
+  Profile Merged = mergeProfilesByThread(P0, P1);
+  EXPECT_THAT(
+      Merged,
+      UnorderedElementsAre(
+          // We want to see two threads after the merge.
+          AllOf(Field(&Profile::Block::Thread, Eq(Profile::ThreadID{1})),
+                Field(&Profile::Block::PathData,
+                      UnorderedElementsAre(
+                          Pair(Merged.internPath({2, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(2u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(2000u)))),
+                          Pair(Merged.internPath({4, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(1000u))))))),
+          AllOf(Field(&Profile::Block::Thread, Eq(Profile::ThreadID{2})),
+                Field(&Profile::Block::PathData,
+                      UnorderedElementsAre(
+                          Pair(Merged.internPath({3, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(2u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(2000u)))),
+                          Pair(Merged.internPath({4, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(1000u)))))))));
+}
+
+TEST(ProfileTest, MergeProfilesByStack) {
+  Profile P0, P1;
+  EXPECT_FALSE(errorToBool(P0.addBlock(
+      Profile::Block{Profile::ThreadID{1},
+                     {{P0.internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(P1.addBlock(
+      Profile::Block{Profile::ThreadID{2},
+                     {{P1.internPath({2, 1}), Profile::Data{1, 1000}}}})));
+
+  Profile Merged = mergeProfilesByStack(P0, P1);
+  EXPECT_THAT(Merged,
+              ElementsAre(AllOf(
+                  // We expect that we lose the ThreadID dimension in this
+                  // algorithm.
+                  Field(&Profile::Block::Thread, Eq(Profile::ThreadID{0})),
+                  Field(&Profile::Block::PathData,
+                        ElementsAre(Pair(
+                            Merged.internPath({2, 1}),
+                            AllOf(Field(&Profile::Data::CallCount, Eq(2u)),
+                                  Field(&Profile::Data::CumulativeLocalTime,
+                                        Eq(2000u)))))))));
+}
+
+TEST(ProfileTest, MergeProfilesByStackAccumulate) {
+  std::vector<Profile> Profiles(3);
+  EXPECT_FALSE(errorToBool(Profiles[0].addBlock(Profile::Block{
+      Profile::ThreadID{1},
+      {{Profiles[0].internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(Profiles[1].addBlock(Profile::Block{
+      Profile::ThreadID{2},
+      {{Profiles[1].internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(Profiles[2].addBlock(Profile::Block{
+      Profile::ThreadID{3},
+      {{Profiles[2].internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  Profile Merged = std::accumulate(Profiles.begin(), Profiles.end(), Profile(),
+                                   mergeProfilesByStack);
+  EXPECT_THAT(Merged,
+              ElementsAre(AllOf(
+                  // We expect that we lose the ThreadID dimension in this
+                  // algorithm.
+                  Field(&Profile::Block::Thread, Eq(Profile::ThreadID{0})),
+                  Field(&Profile::Block::PathData,
+                        ElementsAre(Pair(
+                            Merged.internPath({2, 1}),
+                            AllOf(Field(&Profile::Data::CallCount, Eq(3u)),
+                                  Field(&Profile::Data::CumulativeLocalTime,
+                                        Eq(3000u)))))))));
+}
+
+TEST(ProfileTest, MergeProfilesByThreadAccumulate) {
+  std::vector<Profile> Profiles(2);
+
+  // Set up the blocks for two different threads in Profiles[0].
+  EXPECT_FALSE(errorToBool(Profiles[0].addBlock(Profile::Block{
+      Profile::ThreadID{1},
+      {{Profiles[0].internPath({2, 1}), Profile::Data{1, 1000}},
+       {Profiles[0].internPath({4, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(Profiles[0].addBlock(Profile::Block{
+      Profile::ThreadID{2},
+      {{Profiles[0].internPath({3, 1}), Profile::Data{1, 1000}}}})));
+
+  // Set up the blocks for two different threads in Profiles[1].
+  EXPECT_FALSE(errorToBool(Profiles[1].addBlock(Profile::Block{
+      Profile::ThreadID{1},
+      {{Profiles[1].internPath({2, 1}), Profile::Data{1, 1000}}}})));
+  EXPECT_FALSE(errorToBool(Profiles[1].addBlock(Profile::Block{
+      Profile::ThreadID{2},
+      {{Profiles[1].internPath({3, 1}), Profile::Data{1, 1000}},
+       {Profiles[1].internPath({4, 1}), Profile::Data{1, 1000}}}})));
+
+  Profile Merged = std::accumulate(Profiles.begin(), Profiles.end(), Profile(),
+                                   mergeProfilesByThread);
+  EXPECT_THAT(
+      Merged,
+      UnorderedElementsAre(
+          // We want to see two threads after the merge.
+          AllOf(Field(&Profile::Block::Thread, Eq(Profile::ThreadID{1})),
+                Field(&Profile::Block::PathData,
+                      UnorderedElementsAre(
+                          Pair(Merged.internPath({2, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(2u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(2000u)))),
+                          Pair(Merged.internPath({4, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(1000u))))))),
+          AllOf(Field(&Profile::Block::Thread, Eq(Profile::ThreadID{2})),
+                Field(&Profile::Block::PathData,
+                      UnorderedElementsAre(
+                          Pair(Merged.internPath({3, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(2u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(2000u)))),
+                          Pair(Merged.internPath({4, 1}),
+                               AllOf(Field(&Profile::Data::CallCount, Eq(1u)),
+                                     Field(&Profile::Data::CumulativeLocalTime,
+                                           Eq(1000u)))))))));
+}
+// FIXME: Add a test creating a Trace and generating a Profile
+// FIXME: Add tests for ranking/sorting profile blocks by dimension
+
+} // namespace
+} // namespace xray
+} // namespace llvm