404 lines
13 KiB
C++
404 lines
13 KiB
C++
//===- Profile.cpp - XRay Profile Abstraction -----------------------------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Defines the XRay Profile class representing the latency profile generated by
|
|
// XRay's profiling mode.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
#include "llvm/XRay/Profile.h"
|
|
|
|
#include "llvm/Support/DataExtractor.h"
|
|
#include "llvm/Support/Error.h"
|
|
#include "llvm/Support/FileSystem.h"
|
|
#include "llvm/XRay/Trace.h"
|
|
#include <deque>
|
|
#include <memory>
|
|
|
|
namespace llvm {
|
|
namespace xray {
|
|
|
|
Profile::Profile(const Profile &O) {
|
|
// We need to re-create all the tries from the original (O), into the current
|
|
// Profile being initialized, through the Block instances we see.
|
|
for (const auto &Block : O) {
|
|
Blocks.push_back({Block.Thread, {}});
|
|
auto &B = Blocks.back();
|
|
for (const auto &PathData : Block.PathData)
|
|
B.PathData.push_back({internPath(cantFail(O.expandPath(PathData.first))),
|
|
PathData.second});
|
|
}
|
|
}
|
|
|
|
Profile &Profile::operator=(const Profile &O) {
|
|
Profile P = O;
|
|
*this = std::move(P);
|
|
return *this;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct BlockHeader {
|
|
uint32_t Size;
|
|
uint32_t Number;
|
|
uint64_t Thread;
|
|
};
|
|
|
|
static Expected<BlockHeader> readBlockHeader(DataExtractor &Extractor,
|
|
uint64_t &Offset) {
|
|
BlockHeader H;
|
|
uint64_t CurrentOffset = Offset;
|
|
H.Size = Extractor.getU32(&Offset);
|
|
if (Offset == CurrentOffset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing block header size at offset '") +
|
|
Twine(CurrentOffset) + "'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
CurrentOffset = Offset;
|
|
H.Number = Extractor.getU32(&Offset);
|
|
if (Offset == CurrentOffset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing block header number at offset '") +
|
|
Twine(CurrentOffset) + "'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
CurrentOffset = Offset;
|
|
H.Thread = Extractor.getU64(&Offset);
|
|
if (Offset == CurrentOffset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing block header thread id at offset '") +
|
|
Twine(CurrentOffset) + "'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
return H;
|
|
}
|
|
|
|
static Expected<std::vector<Profile::FuncID>> readPath(DataExtractor &Extractor,
|
|
uint64_t &Offset) {
|
|
// We're reading a sequence of int32_t's until we find a 0.
|
|
std::vector<Profile::FuncID> Path;
|
|
auto CurrentOffset = Offset;
|
|
int32_t FuncId;
|
|
do {
|
|
FuncId = Extractor.getSigned(&Offset, 4);
|
|
if (CurrentOffset == Offset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing path at offset '") + Twine(CurrentOffset) + "'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
CurrentOffset = Offset;
|
|
Path.push_back(FuncId);
|
|
} while (FuncId != 0);
|
|
return std::move(Path);
|
|
}
|
|
|
|
static Expected<Profile::Data> readData(DataExtractor &Extractor,
|
|
uint64_t &Offset) {
|
|
// We expect a certain number of elements for Data:
|
|
// - A 64-bit CallCount
|
|
// - A 64-bit CumulativeLocalTime counter
|
|
Profile::Data D;
|
|
auto CurrentOffset = Offset;
|
|
D.CallCount = Extractor.getU64(&Offset);
|
|
if (CurrentOffset == Offset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing call counts at offset '") + Twine(CurrentOffset) +
|
|
"'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
CurrentOffset = Offset;
|
|
D.CumulativeLocalTime = Extractor.getU64(&Offset);
|
|
if (CurrentOffset == Offset)
|
|
return make_error<StringError>(
|
|
Twine("Error parsing cumulative local time at offset '") +
|
|
Twine(CurrentOffset) + "'",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
return D;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Error Profile::addBlock(Block &&B) {
|
|
if (B.PathData.empty())
|
|
return make_error<StringError>(
|
|
"Block may not have empty path data.",
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
|
|
Blocks.emplace_back(std::move(B));
|
|
return Error::success();
|
|
}
|
|
|
|
Expected<std::vector<Profile::FuncID>> Profile::expandPath(PathID P) const {
|
|
auto It = PathIDMap.find(P);
|
|
if (It == PathIDMap.end())
|
|
return make_error<StringError>(
|
|
Twine("PathID not found: ") + Twine(P),
|
|
std::make_error_code(std::errc::invalid_argument));
|
|
std::vector<Profile::FuncID> Path;
|
|
for (auto Node = It->second; Node; Node = Node->Caller)
|
|
Path.push_back(Node->Func);
|
|
return std::move(Path);
|
|
}
|
|
|
|
Profile::PathID Profile::internPath(ArrayRef<FuncID> P) {
|
|
if (P.empty())
|
|
return 0;
|
|
|
|
auto RootToLeafPath = reverse(P);
|
|
|
|
// Find the root.
|
|
auto It = RootToLeafPath.begin();
|
|
auto PathRoot = *It++;
|
|
auto RootIt =
|
|
find_if(Roots, [PathRoot](TrieNode *N) { return N->Func == PathRoot; });
|
|
|
|
// If we've not seen this root before, remember it.
|
|
TrieNode *Node = nullptr;
|
|
if (RootIt == Roots.end()) {
|
|
NodeStorage.emplace_back();
|
|
Node = &NodeStorage.back();
|
|
Node->Func = PathRoot;
|
|
Roots.push_back(Node);
|
|
} else {
|
|
Node = *RootIt;
|
|
}
|
|
|
|
// Now traverse the path, re-creating if necessary.
|
|
while (It != RootToLeafPath.end()) {
|
|
auto NodeFuncID = *It++;
|
|
auto CalleeIt = find_if(Node->Callees, [NodeFuncID](TrieNode *N) {
|
|
return N->Func == NodeFuncID;
|
|
});
|
|
if (CalleeIt == Node->Callees.end()) {
|
|
NodeStorage.emplace_back();
|
|
auto NewNode = &NodeStorage.back();
|
|
NewNode->Func = NodeFuncID;
|
|
NewNode->Caller = Node;
|
|
Node->Callees.push_back(NewNode);
|
|
Node = NewNode;
|
|
} else {
|
|
Node = *CalleeIt;
|
|
}
|
|
}
|
|
|
|
// At this point, Node *must* be pointing at the leaf.
|
|
assert(Node->Func == P.front());
|
|
if (Node->ID == 0) {
|
|
Node->ID = NextID++;
|
|
PathIDMap.insert({Node->ID, Node});
|
|
}
|
|
return Node->ID;
|
|
}
|
|
|
|
Profile mergeProfilesByThread(const Profile &L, const Profile &R) {
|
|
Profile Merged;
|
|
using PathDataMap = DenseMap<Profile::PathID, Profile::Data>;
|
|
using PathDataMapPtr = std::unique_ptr<PathDataMap>;
|
|
using PathDataVector = decltype(Profile::Block::PathData);
|
|
using ThreadProfileIndexMap = DenseMap<Profile::ThreadID, PathDataMapPtr>;
|
|
ThreadProfileIndexMap ThreadProfileIndex;
|
|
|
|
for (const auto &P : {std::ref(L), std::ref(R)})
|
|
for (const auto &Block : P.get()) {
|
|
ThreadProfileIndexMap::iterator It;
|
|
std::tie(It, std::ignore) = ThreadProfileIndex.insert(
|
|
{Block.Thread, PathDataMapPtr{new PathDataMap()}});
|
|
for (const auto &PathAndData : Block.PathData) {
|
|
auto &PathID = PathAndData.first;
|
|
auto &Data = PathAndData.second;
|
|
auto NewPathID =
|
|
Merged.internPath(cantFail(P.get().expandPath(PathID)));
|
|
PathDataMap::iterator PathDataIt;
|
|
bool Inserted;
|
|
std::tie(PathDataIt, Inserted) = It->second->insert({NewPathID, Data});
|
|
if (!Inserted) {
|
|
auto &ExistingData = PathDataIt->second;
|
|
ExistingData.CallCount += Data.CallCount;
|
|
ExistingData.CumulativeLocalTime += Data.CumulativeLocalTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto &IndexedThreadBlock : ThreadProfileIndex) {
|
|
PathDataVector PathAndData;
|
|
PathAndData.reserve(IndexedThreadBlock.second->size());
|
|
copy(*IndexedThreadBlock.second, std::back_inserter(PathAndData));
|
|
cantFail(
|
|
Merged.addBlock({IndexedThreadBlock.first, std::move(PathAndData)}));
|
|
}
|
|
return Merged;
|
|
}
|
|
|
|
Profile mergeProfilesByStack(const Profile &L, const Profile &R) {
|
|
Profile Merged;
|
|
using PathDataMap = DenseMap<Profile::PathID, Profile::Data>;
|
|
PathDataMap PathData;
|
|
using PathDataVector = decltype(Profile::Block::PathData);
|
|
for (const auto &P : {std::ref(L), std::ref(R)})
|
|
for (const auto &Block : P.get())
|
|
for (const auto &PathAndData : Block.PathData) {
|
|
auto &PathId = PathAndData.first;
|
|
auto &Data = PathAndData.second;
|
|
auto NewPathID =
|
|
Merged.internPath(cantFail(P.get().expandPath(PathId)));
|
|
PathDataMap::iterator PathDataIt;
|
|
bool Inserted;
|
|
std::tie(PathDataIt, Inserted) = PathData.insert({NewPathID, Data});
|
|
if (!Inserted) {
|
|
auto &ExistingData = PathDataIt->second;
|
|
ExistingData.CallCount += Data.CallCount;
|
|
ExistingData.CumulativeLocalTime += Data.CumulativeLocalTime;
|
|
}
|
|
}
|
|
|
|
// In the end there's a single Block, for thread 0.
|
|
PathDataVector Block;
|
|
Block.reserve(PathData.size());
|
|
copy(PathData, std::back_inserter(Block));
|
|
cantFail(Merged.addBlock({0, std::move(Block)}));
|
|
return Merged;
|
|
}
|
|
|
|
Expected<Profile> loadProfile(StringRef Filename) {
|
|
Expected<sys::fs::file_t> FdOrErr = sys::fs::openNativeFileForRead(Filename);
|
|
if (!FdOrErr)
|
|
return FdOrErr.takeError();
|
|
|
|
uint64_t FileSize;
|
|
if (auto EC = sys::fs::file_size(Filename, FileSize))
|
|
return make_error<StringError>(
|
|
Twine("Cannot get filesize of '") + Filename + "'", EC);
|
|
|
|
std::error_code EC;
|
|
sys::fs::mapped_file_region MappedFile(
|
|
*FdOrErr, sys::fs::mapped_file_region::mapmode::readonly, FileSize, 0,
|
|
EC);
|
|
sys::fs::closeFile(*FdOrErr);
|
|
if (EC)
|
|
return make_error<StringError>(
|
|
Twine("Cannot mmap profile '") + Filename + "'", EC);
|
|
StringRef Data(MappedFile.data(), MappedFile.size());
|
|
|
|
Profile P;
|
|
uint64_t Offset = 0;
|
|
DataExtractor Extractor(Data, true, 8);
|
|
|
|
// For each block we get from the file:
|
|
while (Offset != MappedFile.size()) {
|
|
auto HeaderOrError = readBlockHeader(Extractor, Offset);
|
|
if (!HeaderOrError)
|
|
return HeaderOrError.takeError();
|
|
|
|
// TODO: Maybe store this header information for each block, even just for
|
|
// debugging?
|
|
const auto &Header = HeaderOrError.get();
|
|
|
|
// Read in the path data.
|
|
auto PathOrError = readPath(Extractor, Offset);
|
|
if (!PathOrError)
|
|
return PathOrError.takeError();
|
|
const auto &Path = PathOrError.get();
|
|
|
|
// For each path we encounter, we should intern it to get a PathID.
|
|
auto DataOrError = readData(Extractor, Offset);
|
|
if (!DataOrError)
|
|
return DataOrError.takeError();
|
|
auto &Data = DataOrError.get();
|
|
|
|
if (auto E =
|
|
P.addBlock(Profile::Block{Profile::ThreadID{Header.Thread},
|
|
{{P.internPath(Path), std::move(Data)}}}))
|
|
return std::move(E);
|
|
}
|
|
|
|
return P;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct StackEntry {
|
|
uint64_t Timestamp;
|
|
Profile::FuncID FuncId;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
Expected<Profile> profileFromTrace(const Trace &T) {
|
|
Profile P;
|
|
|
|
// The implementation of the algorithm re-creates the execution of
|
|
// the functions based on the trace data. To do this, we set up a number of
|
|
// data structures to track the execution context of every thread in the
|
|
// Trace.
|
|
DenseMap<Profile::ThreadID, std::vector<StackEntry>> ThreadStacks;
|
|
DenseMap<Profile::ThreadID, DenseMap<Profile::PathID, Profile::Data>>
|
|
ThreadPathData;
|
|
|
|
// We then do a pass through the Trace to account data on a per-thread-basis.
|
|
for (const auto &E : T) {
|
|
auto &TSD = ThreadStacks[E.TId];
|
|
switch (E.Type) {
|
|
case RecordTypes::ENTER:
|
|
case RecordTypes::ENTER_ARG:
|
|
|
|
// Push entries into the function call stack.
|
|
TSD.push_back({E.TSC, E.FuncId});
|
|
break;
|
|
|
|
case RecordTypes::EXIT:
|
|
case RecordTypes::TAIL_EXIT:
|
|
|
|
// Exits cause some accounting to happen, based on the state of the stack.
|
|
// For each function we pop off the stack, we take note of the path and
|
|
// record the cumulative state for this path. As we're doing this, we
|
|
// intern the path into the Profile.
|
|
while (!TSD.empty()) {
|
|
auto Top = TSD.back();
|
|
auto FunctionLocalTime = AbsoluteDifference(Top.Timestamp, E.TSC);
|
|
SmallVector<Profile::FuncID, 16> Path;
|
|
transform(reverse(TSD), std::back_inserter(Path),
|
|
std::mem_fn(&StackEntry::FuncId));
|
|
auto InternedPath = P.internPath(Path);
|
|
auto &TPD = ThreadPathData[E.TId][InternedPath];
|
|
++TPD.CallCount;
|
|
TPD.CumulativeLocalTime += FunctionLocalTime;
|
|
TSD.pop_back();
|
|
|
|
// If we've matched the corresponding entry event for this function,
|
|
// then we exit the loop.
|
|
if (Top.FuncId == E.FuncId)
|
|
break;
|
|
|
|
// FIXME: Consider the intermediate times and the cumulative tree time
|
|
// as well.
|
|
}
|
|
|
|
break;
|
|
|
|
case RecordTypes::CUSTOM_EVENT:
|
|
case RecordTypes::TYPED_EVENT:
|
|
// TODO: Support an extension point to allow handling of custom and typed
|
|
// events in profiles.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Once we've gone through the Trace, we now create one Block per thread in
|
|
// the Profile.
|
|
for (const auto &ThreadPaths : ThreadPathData) {
|
|
const auto &TID = ThreadPaths.first;
|
|
const auto &PathsData = ThreadPaths.second;
|
|
if (auto E = P.addBlock({
|
|
TID,
|
|
std::vector<std::pair<Profile::PathID, Profile::Data>>(
|
|
PathsData.begin(), PathsData.end()),
|
|
}))
|
|
return std::move(E);
|
|
}
|
|
|
|
return P;
|
|
}
|
|
|
|
} // namespace xray
|
|
} // namespace llvm
|