/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include "perfetto/base/build_config.h" #include "perfetto/base/logging.h" #include "perfetto/ext/base/file_utils.h" #include "perfetto/ext/base/optional.h" #include "perfetto/ext/base/pipe.h" #include "perfetto/ext/base/string_utils.h" #include "perfetto/ext/base/subprocess.h" #include "perfetto/ext/tracing/ipc/default_socket.h" #include "perfetto/heap_profile.h" #include "perfetto/trace_processor/trace_processor.h" #include "protos/perfetto/trace/trace.gen.h" #include "protos/perfetto/trace/trace.pbzero.h" #include "src/base/test/test_task_runner.h" #include "src/profiling/memory/heapprofd_producer.h" #include "test/gtest_and_gmock.h" #include "test/test_helper.h" #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) #include #endif #include "protos/perfetto/config/profiling/heapprofd_config.gen.h" #include "protos/perfetto/trace/interned_data/interned_data.gen.h" #include "protos/perfetto/trace/profiling/profile_common.gen.h" #include "protos/perfetto/trace/profiling/profile_packet.gen.h" namespace perfetto { namespace profiling { namespace { constexpr useconds_t kMsToUs = 1000; constexpr auto kTracingDisabledTimeoutMs = 30000; constexpr auto kWaitForReadDataTimeoutMs = 10000; constexpr size_t kStartupAllocSize = 10; constexpr size_t kFirstIterationBytes = 5; constexpr size_t kSecondIterationBytes = 7; enum class TestMode { kCentral, kStatic }; enum class AllocatorMode { kMalloc, kCustom }; using ::testing::AllOf; using ::testing::AnyOf; using ::testing::Bool; using ::testing::Contains; using ::testing::Eq; using ::testing::Field; using ::testing::HasSubstr; using ::testing::Values; constexpr const char* kOnlyFlamegraph = "SELECT id, name, map_name, count, cumulative_count, size, " "cumulative_size, " "alloc_count, cumulative_alloc_count, alloc_size, cumulative_alloc_size, " "parent_id " "FROM experimental_flamegraph WHERE " "(ts, upid) IN (SELECT distinct ts, upid from heap_profile_allocation) AND " "profile_type = 'native' order by abs(cumulative_size) desc;"; struct FlamegraphNode { int64_t id; std::string name; std::string map_name; int64_t count; int64_t cumulative_count; int64_t size; int64_t cumulative_size; int64_t alloc_count; int64_t cumulative_alloc_count; int64_t alloc_size; int64_t cumulative_alloc_size; base::Optional parent_id; }; std::vector GetFlamegraph(trace_processor::TraceProcessor* tp) { std::vector result; auto it = tp->ExecuteQuery(kOnlyFlamegraph); while (it.Next()) { result.push_back({ it.Get(0).AsLong(), it.Get(1).AsString(), it.Get(2).AsString(), it.Get(3).AsLong(), it.Get(4).AsLong(), it.Get(5).AsLong(), it.Get(6).AsLong(), it.Get(7).AsLong(), it.Get(8).AsLong(), it.Get(9).AsLong(), it.Get(10).AsLong(), it.Get(11).is_null() ? base::nullopt : base::Optional(it.Get(11).AsLong()), }); } PERFETTO_CHECK(it.Status().ok()); return result; } std::string AllocatorName(AllocatorMode mode) { switch (mode) { case AllocatorMode::kMalloc: return "libc.malloc"; case AllocatorMode::kCustom: return "test"; } } AllocatorMode AllocatorModeFromNameOrDie(std::string s) { if (s == "libc.malloc") return AllocatorMode::kMalloc; if (s == "test") return AllocatorMode::kCustom; PERFETTO_FATAL("Invalid allocator mode [malloc | test]: %s", s.c_str()); } void ContinuousDump(HeapprofdConfig* cfg) { auto* cont_config = cfg->mutable_continuous_dump_config(); cont_config->set_dump_phase_ms(0); cont_config->set_dump_interval_ms(100); } template TraceConfig MakeTraceConfig(F fn) { TraceConfig trace_config; trace_config.add_buffers()->set_size_kb(10 * 1024); trace_config.set_duration_ms(2000); trace_config.set_data_source_stop_timeout_ms(10000); auto* ds_config = trace_config.add_data_sources()->mutable_config(); ds_config->set_name("android.heapprofd"); ds_config->set_target_buffer(0); protos::gen::HeapprofdConfig heapprofd_config; fn(&heapprofd_config); ds_config->set_heapprofd_config_raw(heapprofd_config.SerializeAsString()); return trace_config; } void CustomAllocateAndFree(size_t bytes) { static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_create("test")); AHeapProfile_reportAllocation(heap_id, 0x1234abc, bytes); AHeapProfile_reportFree(heap_id, 0x1234abc); } void SecondaryAllocAndFree(size_t bytes) { static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_create("secondary")); AHeapProfile_reportAllocation(heap_id, 0x1234abc, bytes); AHeapProfile_reportFree(heap_id, 0x1234abc); } void AllocateAndFree(size_t bytes) { // This volatile is needed to prevent the compiler from trying to be // helpful and compiling a "useless" malloc + free into a noop. volatile char* x = static_cast(malloc(bytes)); if (x) { if (bytes > 0) x[0] = 'x'; free(const_cast(x)); } } void DoAllocation(AllocatorMode mode, size_t bytes) { switch (mode) { case AllocatorMode::kMalloc: AllocateAndFree(bytes); break; case AllocatorMode::kCustom: // We need to run malloc(0) even if we want to test the custom allocator, // as the init mechanism assumes the application uses malloc. AllocateAndFree(1); CustomAllocateAndFree(bytes); break; } } void ContinuousMalloc(AllocatorMode mode, size_t primary_bytes, size_t secondary_bytes, ssize_t max_iter = -1) { for (ssize_t i = 0; max_iter == -1 || i < max_iter; ++i) { DoAllocation(mode, primary_bytes); if (secondary_bytes) SecondaryAllocAndFree(secondary_bytes); usleep(10 * kMsToUs); } } void StartAndWaitForHandshake(base::Subprocess* child) { // We cannot use base::Pipe because that assumes we want CLOEXEC. // We do NOT want CLOEXEC as this gets used by the RunReInit in the child. int ready_pipe[2]; PERFETTO_CHECK(pipe(ready_pipe) == 0); // NOLINT(android-cloexec-pipe) int ready_pipe_rd = ready_pipe[0]; int ready_pipe_wr = ready_pipe[1]; child->args.preserve_fds.push_back(ready_pipe_wr); child->args.env.push_back("HEAPPROFD_TESTING_READY_PIPE=" + std::to_string(ready_pipe_wr)); child->Start(); close(ready_pipe_wr); // Wait for libc to initialize the signal handler. If we signal before the // handler is installed, we can kill the process. char buf[1]; PERFETTO_CHECK(PERFETTO_EINTR(read(ready_pipe_rd, buf, sizeof(buf))) == 0); close(ready_pipe_rd); } void ChildFinishHandshake() { const char* ready_pipe = getenv("HEAPPROFD_TESTING_READY_PIPE"); if (ready_pipe != nullptr) { close(static_cast(base::StringToInt64(ready_pipe).value())); } } base::Subprocess ForkContinuousAlloc(AllocatorMode mode, size_t primary_bytes, size_t secondary_bytes = 0, ssize_t max_iter = -1) { base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + AllocatorName(mode)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(primary_bytes)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::to_string(secondary_bytes)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG3=" + std::to_string(max_iter)); StartAndWaitForHandshake(&child); return child; } void __attribute__((constructor(1024))) RunContinuousMalloc() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG0"); const char* a1 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG1"); const char* a2 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG2"); const char* a3 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG3"); if (a0 == nullptr) return; AllocatorMode arg0 = AllocatorModeFromNameOrDie(a0); uint32_t arg1 = a1 ? base::StringToUInt32(a1).value() : 0; uint32_t arg2 = a2 ? base::StringToUInt32(a2).value() : 0; int32_t arg3 = a3 ? base::StringToInt32(a3).value() : -1; ChildFinishHandshake(); ContinuousMalloc(arg0, arg1, arg2, arg3); exit(0); } void __attribute__((constructor(1024))) RunAccurateMalloc() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC"); if (a0 == nullptr) return; static std::atomic initialized{false}; static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_setEnabledCallback( AHeapInfo_create("test"), [](void*, const AHeapProfileEnableCallbackInfo*) { initialized = true; }, nullptr)); ChildFinishHandshake(); // heapprofd_client needs malloc to see the signal. while (!initialized) AllocateAndFree(1); // We call the callback before setting enabled=true on the heap, so we // wait a bit for the assignment to happen. usleep(100000); if (!AHeapProfile_reportAllocation(heap_id, 0x1, 10u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x1); if (!AHeapProfile_reportAllocation(heap_id, 0x2, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); if (!AHeapProfile_reportAllocation(heap_id, 0x3, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x2); // Wait around so we can verify it did't crash. for (;;) { } } void __attribute__((noreturn)) RunAccurateMallocWithVforkCommon() { static std::atomic initialized{false}; static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_setEnabledCallback( AHeapInfo_create("test"), [](void*, const AHeapProfileEnableCallbackInfo*) { initialized = true; }, nullptr)); ChildFinishHandshake(); // heapprofd_client needs malloc to see the signal. while (!initialized) AllocateAndFree(1); // We call the callback before setting enabled=true on the heap, so we // wait a bit for the assignment to happen. usleep(100000); if (!AHeapProfile_reportAllocation(heap_id, 0x1, 10u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x1); pid_t pid = vfork(); PERFETTO_CHECK(pid != -1); if (pid == 0) { AHeapProfile_reportAllocation(heap_id, 0x2, 15u); AHeapProfile_reportAllocation(heap_id, 0x3, 15u); exit(0); } if (!AHeapProfile_reportAllocation(heap_id, 0x2, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); if (!AHeapProfile_reportAllocation(heap_id, 0x3, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x2); // Wait around so we can verify it did't crash. for (;;) { } } void __attribute__((constructor(1024))) RunAccurateSample() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_SAMPLE"); if (a0 == nullptr) return; static std::atomic initialized{false}; static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_setEnabledCallback( AHeapInfo_create("test"), [](void*, const AHeapProfileEnableCallbackInfo*) { initialized = true; }, nullptr)); ChildFinishHandshake(); // heapprofd_client needs malloc to see the signal. while (!initialized) AllocateAndFree(1); // We call the callback before setting enabled=true on the heap, so we // wait a bit for the assignment to happen. usleep(100000); if (!AHeapProfile_reportSample(heap_id, 0x1, 10u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x1); if (!AHeapProfile_reportSample(heap_id, 0x2, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); if (!AHeapProfile_reportSample(heap_id, 0x3, 15u)) PERFETTO_FATAL("Expected allocation to be sampled."); AHeapProfile_reportFree(heap_id, 0x2); // Wait around so we can verify it did't crash. for (;;) { } } void __attribute__((constructor(1024))) RunAccurateMallocWithVfork() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK"); if (a0 == nullptr) return; RunAccurateMallocWithVforkCommon(); } void __attribute__((constructor(1024))) RunAccurateMallocWithVforkThread() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK_THREAD"); if (a0 == nullptr) return; std::thread th(RunAccurateMallocWithVforkCommon); th.join(); } void __attribute__((constructor(1024))) RunReInit() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_REINIT_ARG0"); if (a0 == nullptr) return; AllocatorMode mode = AllocatorModeFromNameOrDie(a0); const char* a1 = getenv("HEAPPROFD_TESTING_RUN_REINIT_ARG1"); const char* a2 = getenv("HEAPPROFD_TESTING_RUN_REINIT_ARG2"); PERFETTO_CHECK(a1 != nullptr && a2 != nullptr); int signal_pipe_rd = static_cast(base::StringToInt64(a1).value()); int ack_pipe_wr = static_cast(base::StringToInt64(a2).value()); ChildFinishHandshake(); size_t bytes = kFirstIterationBytes; bool signalled = false; for (;;) { DoAllocation(mode, bytes); char buf[1]; if (!signalled && read(signal_pipe_rd, buf, sizeof(buf)) == 1) { signalled = true; close(signal_pipe_rd); // make sure the client has noticed that the session has stopped DoAllocation(mode, bytes); bytes = kSecondIterationBytes; PERFETTO_CHECK(PERFETTO_EINTR(write(ack_pipe_wr, "1", 1)) == 1); close(ack_pipe_wr); } usleep(10 * kMsToUs); } PERFETTO_FATAL("Should be unreachable"); } void __attribute__((constructor(1024))) RunCustomLifetime() { const char* a0 = getenv("HEAPPROFD_TESTING_RUN_LIFETIME_ARG0"); const char* a1 = getenv("HEAPPROFD_TESTING_RUN_LIFETIME_ARG1"); if (a0 == nullptr) return; uint64_t arg0 = a0 ? base::StringToUInt64(a0).value() : 0; uint64_t arg1 = a0 ? base::StringToUInt64(a1).value() : 0; PERFETTO_CHECK(arg1); static std::atomic initialized{false}; static std::atomic disabled{false}; static std::atomic sampling_interval; auto enabled_callback = [](void*, const AHeapProfileEnableCallbackInfo* info) { sampling_interval = AHeapProfileEnableCallbackInfo_getSamplingInterval(info); initialized = true; }; auto disabled_callback = [](void*, const AHeapProfileDisableCallbackInfo*) { disabled = true; }; static uint32_t heap_id = AHeapProfile_registerHeap(AHeapInfo_setDisabledCallback( AHeapInfo_setEnabledCallback(AHeapInfo_create("test"), enabled_callback, nullptr), disabled_callback, nullptr)); ChildFinishHandshake(); // heapprofd_client needs malloc to see the signal. while (!initialized) AllocateAndFree(1); if (sampling_interval.load() != arg0) { PERFETTO_FATAL("%" PRIu64 " != %" PRIu64, sampling_interval.load(), arg0); } while (!disabled) AHeapProfile_reportFree(heap_id, 0x2); char x = 'x'; PERFETTO_CHECK(base::WriteAll(static_cast(arg1), &x, sizeof(x)) == 1); close(static_cast(arg1)); // Wait around so we can verify it didn't crash. for (;;) { } } class TraceProcessorTestHelper : public TestHelper { public: explicit TraceProcessorTestHelper(base::TestTaskRunner* task_runner) : TestHelper(task_runner), tp_(trace_processor::TraceProcessor::CreateInstance({})) {} void ReadTraceData(std::vector packets) override { for (auto& packet : packets) { auto preamble = packet.GetProtoPreamble(); std::string payload = packet.GetRawBytesForTesting(); char* preamble_payload = std::get<0>(preamble); size_t preamble_size = std::get<1>(preamble); size_t buf_size = preamble_size + payload.size(); std::unique_ptr buf = std::unique_ptr(new uint8_t[buf_size]); memcpy(&buf[0], preamble_payload, preamble_size); memcpy(&buf[preamble_size], payload.data(), payload.size()); PERFETTO_CHECK(tp_->Parse(std::move(buf), buf_size).ok()); } TestHelper::ReadTraceData(std::move(packets)); } trace_processor::TraceProcessor& tp() { return *tp_; } private: std::unique_ptr tp_; }; std::unique_ptr GetHelper( base::TestTaskRunner* task_runner) { std::unique_ptr helper( new TraceProcessorTestHelper(task_runner)); helper->StartServiceIfRequired(); helper->ConnectConsumer(); helper->WaitForConsumerConnect(); return helper; } void ReadAndWait(TraceProcessorTestHelper* helper) { helper->WaitForTracingDisabled(kTracingDisabledTimeoutMs); helper->ReadData(); helper->WaitForReadData(0, kWaitForReadDataTimeoutMs); helper->tp().NotifyEndOfFile(); } std::string ToTraceString( const std::vector& packets) { protos::gen::Trace trace; for (const protos::gen::TracePacket& packet : packets) { *trace.add_packet() = packet; } return trace.SerializeAsString(); } #define WRITE_TRACE(trace) \ do { \ WriteTrace(trace, __FILE__, __LINE__); \ } while (0) std::string FormatHistogram(const protos::gen::ProfilePacket_Histogram& hist) { std::string out; std::string prev_upper_limit = "-inf"; for (const auto& bucket : hist.buckets()) { std::string upper_limit; if (bucket.max_bucket()) upper_limit = "inf"; else upper_limit = std::to_string(bucket.upper_limit()); out += "[" + prev_upper_limit + ", " + upper_limit + "]: " + std::to_string(bucket.count()) + "; "; prev_upper_limit = std::move(upper_limit); } return out + "\n"; } std::string FormatStats(const protos::gen::ProfilePacket_ProcessStats& stats) { return std::string("unwinding_errors: ") + std::to_string(stats.unwinding_errors()) + "\n" + "heap_samples: " + std::to_string(stats.heap_samples()) + "\n" + "map_reparses: " + std::to_string(stats.map_reparses()) + "\n" + "unwinding_time_us: " + FormatHistogram(stats.unwinding_time_us()); } std::string Suffix(const std::tuple& param) { TestMode tm = std::get<0>(param); AllocatorMode am = std::get<1>(param); std::string result; switch (tm) { case TestMode::kCentral: result += "CentralMode"; break; case TestMode::kStatic: result += "StaticMode"; break; } switch (am) { case AllocatorMode::kMalloc: result += "Malloc"; break; case AllocatorMode::kCustom: result += "Custom"; break; } return result; } __attribute__((unused)) std::string TestSuffix( const ::testing::TestParamInfo>& info) { return Suffix(info.param); } class HeapprofdEndToEnd : public ::testing::TestWithParam> { protected: base::TestTaskRunner task_runner; TestMode test_mode() { return std::get<0>(GetParam()); } AllocatorMode allocator_mode() { return std::get<1>(GetParam()); } std::string allocator_name() { return AllocatorName(allocator_mode()); } void WriteTrace(const std::vector& packets, const char* filename, uint64_t lineno) { const char* outdir = getenv("HEAPPROFD_TEST_PROFILE_OUT"); if (!outdir) return; const std::string fq_filename = std::string(outdir) + "/" + basename(filename) + ":" + std::to_string(lineno) + "_" + Suffix(GetParam()); base::ScopedFile fd(base::OpenFile(fq_filename, O_WRONLY | O_CREAT, 0666)); PERFETTO_CHECK(*fd); std::string trace_string = ToTraceString(packets); PERFETTO_CHECK( base::WriteAll(*fd, trace_string.data(), trace_string.size()) >= 0); } std::unique_ptr Trace( const TraceConfig& trace_config) { auto helper = GetHelper(&task_runner); helper->StartTracing(trace_config); ReadAndWait(helper.get()); return helper; } std::vector GetUnwindingErrors( TraceProcessorTestHelper* helper) { std::vector out; const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const protos::gen::InternedString& fn : packet.interned_data().function_names()) { if (fn.str().find("ERROR ") == 0) { out.push_back(fn.str()); } } } return out; } void PrintStats(TraceProcessorTestHelper* helper) { const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { // protobuf uint64 does not like the PRIu64 formatter. PERFETTO_LOG("Stats for %s: %s", std::to_string(dump.pid()).c_str(), FormatStats(dump.stats()).c_str()); } } std::vector errors = GetUnwindingErrors(helper); for (const std::string& err : errors) { PERFETTO_LOG("Unwinding error: %s", err.c_str()); } } void ValidateSampleSizes(TraceProcessorTestHelper* helper, uint64_t pid, uint64_t alloc_size, const std::string& heap_name = "") { const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { if (dump.pid() != pid || (!heap_name.empty() && heap_name != dump.heap_name())) { continue; } for (const auto& sample : dump.samples()) { EXPECT_EQ(sample.self_allocated() % alloc_size, 0u); EXPECT_EQ(sample.self_freed() % alloc_size, 0u); EXPECT_THAT(sample.self_allocated() - sample.self_freed(), AnyOf(Eq(0u), Eq(alloc_size))); } } } } void ValidateFromStartup(TraceProcessorTestHelper* helper, uint64_t pid, bool from_startup) { const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { if (dump.pid() != pid) continue; EXPECT_EQ(dump.from_startup(), from_startup); } } } void ValidateRejectedConcurrent(TraceProcessorTestHelper* helper, uint64_t pid, bool rejected_concurrent) { const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { if (dump.pid() != pid) continue; EXPECT_EQ(dump.rejected_concurrent(), rejected_concurrent); } } } void ValidateNoSamples(TraceProcessorTestHelper* helper, uint64_t pid) { const auto& packets = helper->trace(); size_t samples = 0; for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { if (dump.pid() != pid) continue; samples += dump.samples().size(); } } EXPECT_EQ(samples, 0u); } void ValidateHasSamples(TraceProcessorTestHelper* helper, uint64_t pid, const std::string& heap_name, uint64_t sampling_interval) { const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t last_allocated = 0; uint64_t last_freed = 0; for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { if (dump.pid() != pid || dump.heap_name() != heap_name) continue; EXPECT_EQ(dump.sampling_interval_bytes(), sampling_interval); for (const auto& sample : dump.samples()) { last_allocated = sample.self_allocated(); last_freed = sample.self_freed(); samples++; } profile_packets++; } } EXPECT_GT(profile_packets, 0u) << heap_name; EXPECT_GT(samples, 0u) << heap_name; EXPECT_GT(last_allocated, 0u) << heap_name; EXPECT_GT(last_freed, 0u) << heap_name; } void ValidateOnlyPID(TraceProcessorTestHelper* helper, uint64_t pid) { size_t dumps = 0; const auto& packets = helper->trace(); for (const protos::gen::TracePacket& packet : packets) { for (const auto& dump : packet.profile_packet().process_dumps()) { EXPECT_EQ(dump.pid(), pid); dumps++; } } EXPECT_GT(dumps, 0u); } }; // This checks that the child is still running (to ensure it didn't crash // unxpectedly) and then kills it. void KillAssertRunning(base::Subprocess* child) { ASSERT_EQ(child->Poll(), base::Subprocess::kRunning) << "Target process not running. CHECK CRASH LOGS."; PERFETTO_LOG("Shutting down profile target."); child->KillAndWaitForTermination(); } TEST_P(HeapprofdEndToEnd, Disabled) { constexpr size_t kAllocSize = 1024; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps("invalid"); ContinuousDump(cfg); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateNoSamples(helper.get(), pid); } TEST_P(HeapprofdEndToEnd, Smoke) { constexpr size_t kAllocSize = 1024; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); ContinuousDump(cfg); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kAllocSize); } TEST_P(HeapprofdEndToEnd, TwoAllocators) { constexpr size_t kCustomAllocSize = 1024; constexpr size_t kAllocSize = 7; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize, kCustomAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); cfg->add_heaps("secondary"); ContinuousDump(cfg); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateHasSamples(helper.get(), pid, "secondary", kSamplingInterval); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kCustomAllocSize, "secondary"); ValidateSampleSizes(helper.get(), pid, kAllocSize, allocator_name()); } TEST_P(HeapprofdEndToEnd, TwoAllocatorsAll) { constexpr size_t kCustomAllocSize = 1024; constexpr size_t kAllocSize = 7; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize, kCustomAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->set_all_heaps(true); ContinuousDump(cfg); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateHasSamples(helper.get(), pid, "secondary", kSamplingInterval); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kCustomAllocSize, "secondary"); ValidateSampleSizes(helper.get(), pid, kAllocSize, allocator_name()); } TEST_P(HeapprofdEndToEnd, AccurateCustomReportAllocation) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC=1"); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps("test"); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); auto flamegraph = GetFlamegraph(&helper->tp()); EXPECT_THAT(flamegraph, Contains(AllOf( Field(&FlamegraphNode::name, HasSubstr("RunAccurateMalloc")), Field(&FlamegraphNode::cumulative_size, Eq(15)), Field(&FlamegraphNode::cumulative_alloc_size, Eq(40))))); ValidateOnlyPID(helper.get(), pid); size_t total_alloc = 0; size_t total_freed = 0; for (const protos::gen::TracePacket& packet : helper->trace()) { for (const auto& dump : packet.profile_packet().process_dumps()) { for (const auto& sample : dump.samples()) { total_alloc += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(total_alloc, 40u); EXPECT_EQ(total_freed, 25u); } #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) #define MAYBE_AccurateCustomReportAllocationWithVfork \ AccurateCustomReportAllocationWithVfork #define MAYBE_AccurateCustomReportAllocationWithVforkThread \ AccurateCustomReportAllocationWithVforkThread #else #define MAYBE_AccurateCustomReportAllocationWithVfork \ DISABLED_AccurateCustomReportAllocationWithVfork #define MAYBE_AccurateCustomReportAllocationWithVforkThread \ DISABLED_AccurateCustomReportAllocationWithVforkThread #endif TEST_P(HeapprofdEndToEnd, MAYBE_AccurateCustomReportAllocationWithVfork) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back( "HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK=1"); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps("test"); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); auto flamegraph = GetFlamegraph(&helper->tp()); EXPECT_THAT(flamegraph, Contains(AllOf( Field(&FlamegraphNode::name, HasSubstr("RunAccurateMalloc")), Field(&FlamegraphNode::cumulative_size, Eq(15)), Field(&FlamegraphNode::cumulative_alloc_size, Eq(40))))); ValidateOnlyPID(helper.get(), pid); size_t total_alloc = 0; size_t total_freed = 0; for (const protos::gen::TracePacket& packet : helper->trace()) { for (const auto& dump : packet.profile_packet().process_dumps()) { EXPECT_FALSE(dump.disconnected()); for (const auto& sample : dump.samples()) { total_alloc += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(total_alloc, 40u); EXPECT_EQ(total_freed, 25u); } TEST_P(HeapprofdEndToEnd, MAYBE_AccurateCustomReportAllocationWithVforkThread) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back( "HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK_THREAD=1"); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps("test"); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); auto flamegraph = GetFlamegraph(&helper->tp()); EXPECT_THAT(flamegraph, Contains(AllOf( Field(&FlamegraphNode::name, HasSubstr("RunAccurateMalloc")), Field(&FlamegraphNode::cumulative_size, Eq(15)), Field(&FlamegraphNode::cumulative_alloc_size, Eq(40))))); ValidateOnlyPID(helper.get(), pid); size_t total_alloc = 0; size_t total_freed = 0; for (const protos::gen::TracePacket& packet : helper->trace()) { for (const auto& dump : packet.profile_packet().process_dumps()) { EXPECT_FALSE(dump.disconnected()); for (const auto& sample : dump.samples()) { total_alloc += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(total_alloc, 40u); EXPECT_EQ(total_freed, 25u); } TEST_P(HeapprofdEndToEnd, AccurateCustomReportSample) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_ACCURATE_SAMPLE=1"); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1000000); cfg->add_pid(pid); cfg->add_heaps("test"); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateOnlyPID(helper.get(), pid); size_t total_alloc = 0; size_t total_freed = 0; for (const protos::gen::TracePacket& packet : helper->trace()) { for (const auto& dump : packet.profile_packet().process_dumps()) { for (const auto& sample : dump.samples()) { total_alloc += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(total_alloc, 40u); EXPECT_EQ(total_freed, 25u); } TEST_P(HeapprofdEndToEnd, AccurateDumpAtMaxCustom) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC=1"); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps("test"); cfg->set_dump_at_max(true); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateOnlyPID(helper.get(), pid); size_t total_alloc = 0; size_t total_count = 0; for (const protos::gen::TracePacket& packet : helper->trace()) { for (const auto& dump : packet.profile_packet().process_dumps()) { for (const auto& sample : dump.samples()) { total_alloc += sample.self_max(); total_count += sample.self_max_count(); } } } EXPECT_EQ(total_alloc, 30u); EXPECT_EQ(total_count, 2u); } TEST_P(HeapprofdEndToEnd, CustomLifetime) { if (allocator_mode() != AllocatorMode::kCustom) GTEST_SKIP(); int disabled_pipe[2]; PERFETTO_CHECK(pipe(disabled_pipe) == 0); // NOLINT(android-cloexec-pipe) int disabled_pipe_rd = disabled_pipe[0]; int disabled_pipe_wr = disabled_pipe[1]; base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_LIFETIME_ARG0=1000000"); child.args.env.push_back("HEAPPROFD_TESTING_RUN_LIFETIME_ARG1=" + std::to_string(disabled_pipe_wr)); child.args.preserve_fds.push_back(disabled_pipe_wr); StartAndWaitForHandshake(&child); close(disabled_pipe_wr); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1000000); cfg->add_pid(pid); cfg->add_heaps("test"); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); // Give client some time to notice the disconnect. sleep(2); KillAssertRunning(&child); char x; EXPECT_EQ(base::Read(disabled_pipe_rd, &x, sizeof(x)), 1); close(disabled_pipe_rd); } TEST_P(HeapprofdEndToEnd, TwoProcesses) { constexpr size_t kAllocSize = 1024; constexpr size_t kAllocSize2 = 7; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize); base::Subprocess child2 = ForkContinuousAlloc(allocator_mode(), kAllocSize2); const uint64_t pid = static_cast(child.pid()); const auto pid2 = child2.pid(); TraceConfig trace_config = MakeTraceConfig([this, pid, pid2](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_pid(static_cast(pid2)); cfg->add_heaps(allocator_name()); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); KillAssertRunning(&child2); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateSampleSizes(helper.get(), pid, kAllocSize); ValidateHasSamples(helper.get(), static_cast(pid2), allocator_name(), kSamplingInterval); ValidateSampleSizes(helper.get(), static_cast(pid2), kAllocSize2); } TEST_P(HeapprofdEndToEnd, FinalFlush) { constexpr size_t kAllocSize = 1024; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); KillAssertRunning(&child); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kAllocSize); } TEST_P(HeapprofdEndToEnd, NativeStartup) { if (test_mode() == TestMode::kStatic) GTEST_SKIP(); auto helper = GetHelper(&task_runner); TraceConfig trace_config = MakeTraceConfig([this](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_process_cmdline("heapprofd_continuous_malloc"); cfg->add_heaps(allocator_name()); }); trace_config.set_duration_ms(5000); helper->StartTracing(trace_config); // Wait to guarantee that the process forked below is hooked by the profiler // by virtue of the startup check, and not by virtue of being seen as a // running process. This sleep is here to prevent that, accidentally, the // test gets to the fork()+exec() too soon, before the heap profiling daemon // has received the trace config. sleep(1); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(kStartupAllocSize)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::string("0")); StartAndWaitForHandshake(&child); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); KillAssertRunning(&child); const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t total_allocated = 0; uint64_t total_freed = 0; for (const protos::gen::TracePacket& packet : packets) { if (packet.has_profile_packet() && !packet.profile_packet().process_dumps().empty()) { const auto& dumps = packet.profile_packet().process_dumps(); ASSERT_EQ(dumps.size(), 1u); const protos::gen::ProfilePacket_ProcessHeapSamples& dump = dumps[0]; EXPECT_EQ(static_cast(dump.pid()), child.pid()); profile_packets++; for (const auto& sample : dump.samples()) { samples++; total_allocated += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(profile_packets, 1u); EXPECT_GT(samples, 0u); EXPECT_GT(total_allocated, 0u); EXPECT_GT(total_freed, 0u); } TEST_P(HeapprofdEndToEnd, NativeStartupDenormalizedCmdline) { if (test_mode() == TestMode::kStatic) GTEST_SKIP(); auto helper = GetHelper(&task_runner); TraceConfig trace_config = MakeTraceConfig([this](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_process_cmdline("heapprofd_continuous_malloc@1.2.3"); cfg->add_heaps(allocator_name()); }); trace_config.set_duration_ms(5000); helper->StartTracing(trace_config); // Wait to guarantee that the process forked below is hooked by the profiler // by virtue of the startup check, and not by virtue of being seen as a // running process. This sleep is here to prevent that, accidentally, the // test gets to the fork()+exec() too soon, before the heap profiling daemon // has received the trace config. sleep(1); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(kStartupAllocSize)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::string("0")); StartAndWaitForHandshake(&child); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); KillAssertRunning(&child); const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t total_allocated = 0; uint64_t total_freed = 0; for (const protos::gen::TracePacket& packet : packets) { if (packet.has_profile_packet() && !packet.profile_packet().process_dumps().empty()) { const auto& dumps = packet.profile_packet().process_dumps(); ASSERT_EQ(dumps.size(), 1u); const protos::gen::ProfilePacket_ProcessHeapSamples& dump = dumps[0]; EXPECT_EQ(static_cast(dump.pid()), child.pid()); profile_packets++; for (const auto& sample : dump.samples()) { samples++; total_allocated += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(profile_packets, 1u); EXPECT_GT(samples, 0u); EXPECT_GT(total_allocated, 0u); EXPECT_GT(total_freed, 0u); } TEST_P(HeapprofdEndToEnd, DiscoverByName) { auto helper = GetHelper(&task_runner); base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(kStartupAllocSize)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::string("0")); StartAndWaitForHandshake(&child); // Wait to make sure process is fully initialized, so we do not accidentally // match it by the startup logic. sleep(1); TraceConfig trace_config = MakeTraceConfig([this](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_process_cmdline("heapprofd_continuous_malloc"); cfg->add_heaps(allocator_name()); }); trace_config.set_duration_ms(5000); helper->StartTracing(trace_config); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); KillAssertRunning(&child); const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t total_allocated = 0; uint64_t total_freed = 0; for (const protos::gen::TracePacket& packet : packets) { if (packet.has_profile_packet() && !packet.profile_packet().process_dumps().empty()) { const auto& dumps = packet.profile_packet().process_dumps(); ASSERT_EQ(dumps.size(), 1u); const protos::gen::ProfilePacket_ProcessHeapSamples& dump = dumps[0]; EXPECT_EQ(static_cast(dump.pid()), child.pid()); profile_packets++; for (const auto& sample : dump.samples()) { samples++; total_allocated += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(profile_packets, 1u); EXPECT_GT(samples, 0u); EXPECT_GT(total_allocated, 0u); EXPECT_GT(total_freed, 0u); } TEST_P(HeapprofdEndToEnd, DiscoverByNameDenormalizedCmdline) { auto helper = GetHelper(&task_runner); // Make sure the forked process does not get reparented to init. base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(kStartupAllocSize)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::string("0")); StartAndWaitForHandshake(&child); // Wait to make sure process is fully initialized, so we do not accidentally // match it by the startup logic. sleep(1); TraceConfig trace_config = MakeTraceConfig([this](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_process_cmdline("heapprofd_continuous_malloc@1.2.3"); cfg->add_heaps(allocator_name()); }); trace_config.set_duration_ms(5000); helper->StartTracing(trace_config); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); KillAssertRunning(&child); const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t total_allocated = 0; uint64_t total_freed = 0; for (const protos::gen::TracePacket& packet : packets) { if (packet.has_profile_packet() && !packet.profile_packet().process_dumps().empty()) { const auto& dumps = packet.profile_packet().process_dumps(); ASSERT_EQ(dumps.size(), 1u); const protos::gen::ProfilePacket_ProcessHeapSamples& dump = dumps[0]; EXPECT_EQ(static_cast(dump.pid()), child.pid()); profile_packets++; for (const auto& sample : dump.samples()) { samples++; total_allocated += sample.self_allocated(); total_freed += sample.self_freed(); } } } EXPECT_EQ(profile_packets, 1u); EXPECT_GT(samples, 0u); EXPECT_GT(total_allocated, 0u); EXPECT_GT(total_freed, 0u); } TEST_P(HeapprofdEndToEnd, ReInit) { constexpr size_t kSamplingInterval = 1; // We cannot use base::Pipe because that assumes we want CLOEXEC. // We do NOT want CLOEXEC as this gets used by the RunReInit in the child. int signal_pipe[2]; int ack_pipe[2]; PERFETTO_CHECK(pipe(signal_pipe) == 0); // NOLINT(android-cloexec-pipe) PERFETTO_CHECK(pipe(ack_pipe) == 0); // NOLINT(android-cloexec-pipe) int cur_flags = fcntl(signal_pipe[0], F_GETFL, 0); PERFETTO_CHECK(cur_flags >= 0); PERFETTO_CHECK(fcntl(signal_pipe[0], F_SETFL, cur_flags | O_NONBLOCK) == 0); cur_flags = fcntl(signal_pipe[1], F_GETFL, 0); PERFETTO_CHECK(cur_flags >= 0); PERFETTO_CHECK(fcntl(signal_pipe[1], F_SETFL, cur_flags | O_NONBLOCK) == 0); int signal_pipe_rd = signal_pipe[0]; int signal_pipe_wr = signal_pipe[1]; int ack_pipe_rd = ack_pipe[0]; int ack_pipe_wr = ack_pipe[1]; base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.preserve_fds.push_back(signal_pipe_rd); child.args.preserve_fds.push_back(ack_pipe_wr); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG1=" + std::to_string(signal_pipe_rd)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG2=" + std::to_string(ack_pipe_wr)); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); close(signal_pipe_rd); close(ack_pipe_wr); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kFirstIterationBytes); PERFETTO_CHECK(PERFETTO_EINTR(write(signal_pipe_wr, "1", 1)) == 1); close(signal_pipe_wr); char buf[1]; ASSERT_EQ(PERFETTO_EINTR(read(ack_pipe_rd, buf, sizeof(buf))), 1); close(ack_pipe_rd); // A brief sleep to allow the client to notice that the profiling session is // to be torn down (as it rejects concurrent sessions). usleep(500 * kMsToUs); PERFETTO_LOG("HeapprofdEndToEnd::Reinit: Starting second"); // We must keep alive the original helper because it owns the service thread. std::unique_ptr helper2 = std::unique_ptr( new TraceProcessorTestHelper(&task_runner)); helper2->ConnectConsumer(); helper2->WaitForConsumerConnect(); helper2->StartTracing(trace_config); ReadAndWait(helper2.get()); WRITE_TRACE(helper2->trace()); PrintStats(helper2.get()); KillAssertRunning(&child); ValidateHasSamples(helper2.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper2.get(), pid); ValidateSampleSizes(helper2.get(), pid, kSecondIterationBytes); } TEST_P(HeapprofdEndToEnd, ReInitAfterInvalid) { constexpr size_t kSamplingInterval = 1; // We cannot use base::Pipe because that assumes we want CLOEXEC. // We do NOT want CLOEXEC as this gets used by the RunReInit in the child. int signal_pipe[2]; int ack_pipe[2]; PERFETTO_CHECK(pipe(signal_pipe) == 0); // NOLINT(android-cloexec-pipe) PERFETTO_CHECK(pipe(ack_pipe) == 0); // NOLINT(android-cloexec-pipe) int cur_flags = fcntl(signal_pipe[0], F_GETFL, 0); PERFETTO_CHECK(cur_flags >= 0); PERFETTO_CHECK(fcntl(signal_pipe[0], F_SETFL, cur_flags | O_NONBLOCK) == 0); cur_flags = fcntl(signal_pipe[1], F_GETFL, 0); PERFETTO_CHECK(cur_flags >= 0); PERFETTO_CHECK(fcntl(signal_pipe[1], F_SETFL, cur_flags | O_NONBLOCK) == 0); int signal_pipe_rd = signal_pipe[0]; int signal_pipe_wr = signal_pipe[1]; int ack_pipe_rd = ack_pipe[0]; int ack_pipe_wr = ack_pipe[1]; base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.preserve_fds.push_back(signal_pipe_rd); child.args.preserve_fds.push_back(ack_pipe_wr); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG1=" + std::to_string(signal_pipe_rd)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_REINIT_ARG2=" + std::to_string(ack_pipe_wr)); StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); close(signal_pipe_rd); close(ack_pipe_wr); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); }); auto helper = Trace(trace_config); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kFirstIterationBytes); PERFETTO_CHECK(PERFETTO_EINTR(write(signal_pipe_wr, "1", 1)) == 1); close(signal_pipe_wr); char buf[1]; ASSERT_EQ(PERFETTO_EINTR(read(ack_pipe_rd, buf, sizeof(buf))), 1); close(ack_pipe_rd); // A brief sleep to allow the client to notice that the profiling session is // to be torn down (as it rejects concurrent sessions). usleep(500 * kMsToUs); PERFETTO_LOG("HeapprofdEndToEnd::Reinit: Starting second"); // We must keep alive the original helper because it owns the service thread. std::unique_ptr helper2 = std::unique_ptr( new TraceProcessorTestHelper(&task_runner)); helper2->ConnectConsumer(); helper2->WaitForConsumerConnect(); helper2->StartTracing(trace_config); ReadAndWait(helper2.get()); WRITE_TRACE(helper2->trace()); PrintStats(helper2.get()); KillAssertRunning(&child); ValidateHasSamples(helper2.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper2.get(), pid); ValidateSampleSizes(helper2.get(), pid, kSecondIterationBytes); } TEST_P(HeapprofdEndToEnd, ConcurrentSession) { constexpr size_t kAllocSize = 1024; constexpr size_t kSamplingInterval = 1; base::Subprocess child = ForkContinuousAlloc(allocator_mode(), kAllocSize); const uint64_t pid = static_cast(child.pid()); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(kSamplingInterval); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); ContinuousDump(cfg); }); trace_config.set_duration_ms(5000); auto helper = GetHelper(&task_runner); helper->StartTracing(trace_config); sleep(1); PERFETTO_LOG("Starting concurrent."); std::unique_ptr helper_concurrent( new TraceProcessorTestHelper(&task_runner)); helper_concurrent->ConnectConsumer(); helper_concurrent->WaitForConsumerConnect(); helper_concurrent->StartTracing(trace_config); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); PrintStats(helper.get()); ReadAndWait(helper_concurrent.get()); WRITE_TRACE(helper_concurrent->trace()); PrintStats(helper_concurrent.get()); KillAssertRunning(&child); ValidateHasSamples(helper.get(), pid, allocator_name(), kSamplingInterval); ValidateOnlyPID(helper.get(), pid); ValidateSampleSizes(helper.get(), pid, kAllocSize); ValidateRejectedConcurrent(helper.get(), pid, false); ValidateOnlyPID(helper_concurrent.get(), pid); ValidateRejectedConcurrent(helper_concurrent.get(), pid, true); } TEST_P(HeapprofdEndToEnd, NativeProfilingActiveAtProcessExit) { constexpr uint64_t kTestAllocSize = 128; base::Pipe start_pipe = base::Pipe::Create(base::Pipe::kBothBlock); int start_pipe_wr = *start_pipe.wr; base::Subprocess child({"/proc/self/exe"}); child.args.posix_argv0_override_for_testing = "heapprofd_continuous_malloc"; child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG0=" + allocator_name()); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG1=" + std::to_string(kTestAllocSize)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG2=" + std::to_string(0)); child.args.env.push_back("HEAPPROFD_TESTING_RUN_MALLOC_ARG3=" + std::to_string(200)); child.args.preserve_fds.push_back(start_pipe_wr); child.args.posix_entrypoint_for_testing = [start_pipe_wr] { PERFETTO_CHECK(PERFETTO_EINTR(write(start_pipe_wr, "1", 1)) == 1); PERFETTO_CHECK(close(start_pipe_wr) == 0 || errno == EINTR); }; StartAndWaitForHandshake(&child); const uint64_t pid = static_cast(child.pid()); start_pipe.wr.reset(); // Construct tracing config (without starting profiling). auto helper = GetHelper(&task_runner); // Wait for child to have been scheduled at least once. char buf[1] = {}; ASSERT_EQ(PERFETTO_EINTR(read(*start_pipe.rd, buf, sizeof(buf))), 1); start_pipe.rd.reset(); TraceConfig trace_config = MakeTraceConfig([this, pid](HeapprofdConfig* cfg) { cfg->set_sampling_interval_bytes(1); cfg->add_pid(pid); cfg->add_heaps(allocator_name()); }); trace_config.set_duration_ms(5000); // Trace until child exits. helper->StartTracing(trace_config); // Wait for the child and assert that it exited successfully. EXPECT_TRUE(child.Wait(30000)); EXPECT_EQ(child.status(), base::Subprocess::kTerminated); EXPECT_EQ(child.returncode(), 0); // Assert that we did profile the process. helper->FlushAndWait(2000); helper->DisableTracing(); ReadAndWait(helper.get()); WRITE_TRACE(helper->full_trace()); const auto& packets = helper->trace(); ASSERT_GT(packets.size(), 0u); size_t profile_packets = 0; size_t samples = 0; uint64_t total_allocated = 0; for (const protos::gen::TracePacket& packet : packets) { if (packet.has_profile_packet() && !packet.profile_packet().process_dumps().empty()) { const auto& dumps = packet.profile_packet().process_dumps(); ASSERT_EQ(dumps.size(), 1u); const protos::gen::ProfilePacket_ProcessHeapSamples& dump = dumps[0]; EXPECT_EQ(dump.pid(), pid); profile_packets++; for (const auto& sample : dump.samples()) { samples++; total_allocated += sample.self_allocated(); } } } EXPECT_EQ(profile_packets, 1u); EXPECT_GT(samples, 0u); EXPECT_GT(total_allocated, 0u); } // On in-tree Android, we use the system heapprofd in fork or central mode. // For Linux and out-of-tree Android, we statically include a copy of // heapprofd and use that. This one does not support intercepting malloc. #if !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) #if !PERFETTO_BUILDFLAG(PERFETTO_START_DAEMONS) #error "Need to start daemons for Linux test." #endif INSTANTIATE_TEST_CASE_P(DISABLED_Run, HeapprofdEndToEnd, Values(std::make_tuple(TestMode::kStatic, AllocatorMode::kCustom)), TestSuffix); #elif !PERFETTO_BUILDFLAG(PERFETTO_START_DAEMONS) INSTANTIATE_TEST_CASE_P( Run, HeapprofdEndToEnd, Values(std::make_tuple(TestMode::kCentral, AllocatorMode::kMalloc), std::make_tuple(TestMode::kCentral, AllocatorMode::kCustom)), TestSuffix); #endif } // namespace } // namespace profiling } // namespace perfetto