Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions protos/perfetto/config/profiling/perf_event_config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ message PerfEventConfig {
// it to the same value. The profiler will choose one bin for all those data
// sources.
optional uint32 process_shard_count = 6;

// If set, a process is also in scope if one of its ancestors is in
// |target_pid|: the workers a server forks, the compilers a build runs.
// Ancestry is read from the parent pid chain when the process is first
// sampled, so a process whose parent exited before then has been
// reparented and is not covered. The names of the descendants do not
// matter, unlike |target_cmdline|.
optional bool target_pid_descendants = 7;
}

// Userspace unwinding mode.
Expand Down
3 changes: 3 additions & 0 deletions src/profiling/perf/event_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,9 @@ std::optional<EventConfig> EventConfig::CreateSampling(
process_sharding)
: ParseTargetFilter(pb_config,
process_sharding); // backwards compatibility
// Only the Scope message has it; the legacy top-level fields do not.
target_filter.pid_descendants =
pb_config.callstack_sampling().scope().target_pid_descendants();

// Kernel callstacks.
kernel_frames = pb_config.callstack_sampling().kernel_frames() ||
Expand Down
2 changes: 2 additions & 0 deletions src/profiling/perf/event_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct TargetFilter {
base::FlatSet<pid_t> exclude_pids;
std::optional<ProcessSharding> process_sharding;
uint32_t additional_cmdline_count = 0;
// Also keep the descendants of |pids|.
bool pid_descendants = false;
};

// Describes a perf event for two purposes:
Expand Down
44 changes: 42 additions & 2 deletions src/profiling/perf/perf_producer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
#include <map>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include <stdlib.h>
#include <unistd.h>

#include <unwindstack/Error.h>
Expand Down Expand Up @@ -254,6 +256,31 @@ void WritePerfEventDefaultsPacket(const EventConfig& event_config,
}
}

// How far up the parent chain a process is looked for in the target's
// descendants. Deeper than any real process tree; stops a loop on a pid
// reused mid-walk.
constexpr int kMaxAncestorDepth = 64;

// The parent pid of |pid|, from /proc/<pid>/stat, whose fourth field it is.
// The second field is the command name in parentheses, which can itself
// hold spaces and parentheses, so the fields after it are found from the
// last ')'.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rsavitski this was the most brute force way I could solve this problem but not at all pretty. Do you have any magic thing you know which could solve the problem I have in this PR without needing this? I know that we could do this if we were to use per-thread per-cpu mode but that seems like a bunch bigger change.

std::optional<pid_t> ReadParentPid(pid_t pid) {
std::string stat;
if (!base::ReadFile("/proc/" + std::to_string(pid) + "/stat", &stat))
return std::nullopt;
size_t end = stat.rfind(')');
if (end == std::string::npos || end + 4 > stat.size())
return std::nullopt;
// ") S 1234 ..."
const char* fields = stat.c_str() + end + 4;
char* parsed_end = nullptr;
long ppid = strtol(fields, &parsed_end, 10);
if (parsed_end == fields)
return std::nullopt;
return static_cast<pid_t>(ppid);
}

uint32_t TimeToNextReadTickMs(DataSourceInstanceID ds_id, uint32_t period_ms) {
// Normally, we'd schedule the next tick at the next |period_ms|
// boundary of the boot clock. However, to avoid aligning the read tasks of
Expand Down Expand Up @@ -372,7 +399,8 @@ bool PerfProducer::ShouldRejectDueToFilter(
const TargetFilter& filter,
bool skip_cmdline,
base::FlatSet<std::string>* additional_cmdlines,
std::function<bool(std::string*)> read_proc_pid_cmdline) {
std::function<bool(std::string*)> read_proc_pid_cmdline,
std::function<std::optional<pid_t>(pid_t)> read_ppid) {
PERFETTO_CHECK(additional_cmdlines);

std::string cmdline;
Expand Down Expand Up @@ -414,6 +442,17 @@ bool PerfProducer::ShouldRejectDueToFilter(
if (filter.pids.count(pid)) {
return false;
}
if (filter.pid_descendants && !filter.pids.empty()) {
pid_t p = pid;
for (int depth = 0; depth < kMaxAncestorDepth; depth++) {
std::optional<pid_t> ppid = read_ppid(p);
if (!ppid.has_value() || *ppid <= 1)
break;
if (filter.pids.count(*ppid))
return false;
p = *ppid;
}
}

// Empty allow filter means keep everything that isn't explicitly excluded.
if (filter.cmdlines.empty() && filter.pids.empty() &&
Expand Down Expand Up @@ -925,7 +964,8 @@ bool PerfProducer::ReadAndParsePerCpuBuffer(EventReader* reader,
pid, event_config.filter(), is_kthread, &ds.additional_cmdlines,
[pid](std::string* cmdline) {
return glob_aware::ReadProcCmdlineForPID(pid, cmdline);
})) {
},
ReadParentPid)) {
process_state = ProcessTrackingStatus::kRejected;
EmitSkippedSample(ds_id, std::move(sample.value()),
SampleSkipReason::kRejected);
Expand Down
5 changes: 4 additions & 1 deletion src/profiling/perf/perf_producer.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,15 @@ class PerfProducer : public Producer,
}

// public for testing:
// |read_ppid| returns a process's parent pid, or nullopt if it cannot be
// read (the process is gone).
static bool ShouldRejectDueToFilter(
pid_t pid,
const TargetFilter& filter,
bool skip_cmdline,
base::FlatSet<std::string>* additional_cmdlines,
std::function<bool(std::string*)> read_proc_pid_cmdline);
std::function<bool(std::string*)> read_proc_pid_cmdline,
std::function<std::optional<pid_t>(pid_t)> read_ppid);

private:
// State of the producer's connection to tracing service (traced).
Expand Down
46 changes: 44 additions & 2 deletions src/profiling/perf/perf_producer_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "src/profiling/perf/perf_producer.h"

#include <stdint.h>
#include <map>
#include <optional>

#include "perfetto/base/logging.h"
Expand All @@ -30,11 +31,19 @@ bool ShouldReject(pid_t pid,
std::string cmdline,
const TargetFilter& filter,
bool skip_cmd,
base::FlatSet<std::string>* additional_cmdlines) {
base::FlatSet<std::string>* additional_cmdlines,
std::map<pid_t, pid_t> parents = {}) {
return PerfProducer::ShouldRejectDueToFilter(
pid, filter, skip_cmd, additional_cmdlines, [cmdline](std::string* out) {
pid, filter, skip_cmd, additional_cmdlines,
[cmdline](std::string* out) {
*out = cmdline;
return true;
},
[parents](pid_t p) -> std::optional<pid_t> {
auto it = parents.find(p);
if (it == parents.end())
return std::nullopt;
return it->second;
});
}

Expand Down Expand Up @@ -197,6 +206,39 @@ TEST(TargetFilterTest, AdditionalCmdlines) {
EXPECT_EQ(extra_cmds.count("/bin/top"), 0u);
}

TEST(TargetFilterTest, TargetPidDescendants) {
bool skip_cmd = false;
base::FlatSet<std::string> extra_cmds;
TargetFilter filter;
filter.pids.insert(100);
// 100 forks 200 (a worker that renamed itself), which forks 300; 400 is
// unrelated and 500 was reparented to init.
std::map<pid_t, pid_t> parents = {
{100, 1}, {200, 100}, {300, 200}, {400, 1}, {500, 1}};

// Without the option, only the pid itself.
EXPECT_FALSE(
ShouldReject(100, "nginx", filter, skip_cmd, &extra_cmds, parents));
EXPECT_TRUE(ShouldReject(200, "nginx: worker process", filter, skip_cmd,
&extra_cmds, parents));

filter.pid_descendants = true;
EXPECT_FALSE(ShouldReject(200, "nginx: worker process", filter, skip_cmd,
&extra_cmds, parents));
EXPECT_FALSE(
ShouldReject(300, "cc1", filter, skip_cmd, &extra_cmds, parents));
EXPECT_TRUE(
ShouldReject(400, "bash", filter, skip_cmd, &extra_cmds, parents));
EXPECT_TRUE(ShouldReject(500, "nginx: worker process", filter, skip_cmd,
&extra_cmds, parents));
// A process already gone has no parent to read.
EXPECT_TRUE(
ShouldReject(600, "gone", filter, skip_cmd, &extra_cmds, parents));
// Exclusions still win.
filter.exclude_pids.insert(300);
EXPECT_TRUE(ShouldReject(300, "cc1", filter, skip_cmd, &extra_cmds, parents));
}

} // namespace
} // namespace profiling
} // namespace perfetto
Loading