Sampling profiler regression
It seems like commit 53974e2fd50b5fb97b4a5836361b76e2854b64c8 ("Enable TCMalloc to use reuse size classes by default") causes a regression in the accuracy of the sampling profiler's estimated_bytes field.
The test allocates a number of fixed size arrays, and checks the sum of the estimated bytes fields across all samples. Before that commit, the test below passes reliably. After that commit, it has around 20% error. I see some point fixes after that commit (e.g., https://github.com/google/tcmalloc/commit/68bd09126be397747b88adcaa4b2f77e8ca3c0ea), but the underlying regression still seems to be there.
To test, place the following in the top-level `BUILD`:
```
cc_test(
name = "failing_test",
srcs = ["failing_test.cc"],
copts = TCMALLOC_DEFAULT_COPTS,
linkstatic = 1,
malloc = "//tcmalloc",
deps = [
":malloc_extension",
"//tcmalloc/internal:logging",
"@com_google_absl//absl/log",
"@com_google_absl//absl/debugging:symbolize",
"@com_google_googletest//:gtest_main",
],
)
```
and add this `failing_test.cc`:
```
#include "gtest/gtest.h"
#include "absl/debugging/symbolize.h"
#include "absl/log/log.h"
#include "tcmalloc/malloc_extension.h"
#include "tcmalloc/internal/logging.h"
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <sstream>
#include <unordered_map>
#include <optional>
namespace tcmalloc::tcmalloc_internal {
using namespace std;
struct SampleInfo {
std::optional<int64_t> estimated_bytes;
std::optional<int64_t> estimated_count;
};
using SampleStack = std::string;
typedef std::pair<SampleStack, SampleInfo> Sample;
std::vector<Sample> AggregateAndSortProfile(const Profile& profile) {
LOG(INFO) << "Analyzing TCMalloc sampling profile";
int failed_symbolizations = 0;
std::unordered_map<std::string, SampleInfo> samples_map;
profile.Iterate([&](const Profile::Sample& sample) {
// Deallocation samples are the same as the allocation samples, except with a negative
// sample.count < 0 and the deallocation stack.
if (sample.count <= 0) {
return;
}
std::stringstream sstream;
// 256 is arbitrary. Symbolize will return false if the symbol is longer than that.
char buf[256];
for (int64_t i = 0; i < sample.depth; ++i) {
if (absl::Symbolize(sample.stack[i], buf, sizeof(buf))) {
sstream << buf << std::endl;;
}
}
std::string stack = sstream.str();
// Update the corresponding call stack entry with this sample's information.
auto& entry = samples_map[stack];
entry.estimated_bytes = entry.estimated_bytes.value_or(0) + sample.sum;
entry.estimated_count = entry.estimated_count.value_or(0) + sample.count;
});
std::vector<Sample> samples_vec;
samples_vec.reserve(samples_map.size());
for (auto& entry : samples_map) {
samples_vec.push_back(std::move(entry));
}
return samples_vec;
}
[[nodiscard]] std::unique_ptr<char[]> TestAllocArrayOfSize(int64_t alloc_size) {
std::unique_ptr<char[]> alloc(new char[alloc_size]);
// Clang in release mode can optimize out the above allocation unless
// we do something with the pointer... so we just log it.
VLOG(8) << static_cast<void*>(alloc.get());
return alloc;
}
// Duplicate of TestAllocArrayOfSize which will not be found by GetTestAllocs.
std::unique_ptr<char[]> InternalAllocArrayOfSize(
int64_t alloc_size) {
std::unique_ptr<char[]> alloc(new char[alloc_size]);
VLOG(8) << static_cast<void*>(alloc.get());
return alloc;
}
void SetProfileSamplingRate(int64_t sample_freq_bytes) {
auto old_rate = MallocExtension::GetProfileSamplingRate();
MallocExtension::SetProfileSamplingRate(sample_freq_bytes);
// The probability of sampling an allocation of size X with sampling rate Y is 1 - e^(-X/Y).
// An allocation of size Y * 14 is thus sampled with probability > 99.9999%.
InternalAllocArrayOfSize(old_rate * 14);
}
vector<Sample> GetTestAllocs(const vector<Sample>& samples) {
vector<Sample> test_samples;
for (const auto& sample : samples) {
if (sample.first.find("TestAllocArrayOfSize") != std::string::npos) {
test_samples.push_back(sample);
}
}
return test_samples;
}
TEST(SampledAllocationTest, AnubhavTest) {
const auto kSampleFreqBytes = 10000;
const auto kAllocSize = 10000;
const auto kNumAllocations = 1000;
SetProfileSamplingRate(kSampleFreqBytes);
std::vector<std::unique_ptr<char[]>> v;
for (int i = 0; i < kNumAllocations; ++i) {
v.push_back(TestAllocArrayOfSize(kAllocSize));
}
auto prof = MallocExtension::SnapshotCurrent(tcmalloc::ProfileType::kHeap);
auto samples = AggregateAndSortProfile(prof);
auto test_samples = GetTestAllocs(samples);
ASSERT_EQ(test_samples.size(), 1);
auto estimated_count = *test_samples[0].second.estimated_count;
auto margin = kNumAllocations * 0.2;
ASSERT_NEAR(kNumAllocations, estimated_count, margin);
auto estimated_bytes = *test_samples[0].second.estimated_bytes;
auto actual_bytes = kAllocSize * kNumAllocations;
margin = actual_bytes * 0.2;
ASSERT_NEAR(actual_bytes, estimated_bytes, margin);
}
} // namespace tcmalloc::tcmalloc_internal
```
关闭于 2025-02-12 2 条评论