Skip to content

Commit d28b526

Browse files
committed
cpu_profiler: add scheduler group
Add scheduler group to each sample. This is recorded on the seastar at the moment of the sample side and we now include it in the output. This is useful for understanding what is running in what scheduler group. Add a new unit test case for this functionality.
1 parent d0aae0c commit d28b526

File tree

6 files changed

+87
-13
lines changed

6 files changed

+87
-13
lines changed

src/v/redpanda/admin/api-doc/debug.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,10 @@
782782
"type": "string",
783783
"description": "user backtrace"
784784
},
785+
"scheduling_group": {
786+
"type": "string",
787+
"description": "The scheduling group that was active when the sample was taken."
788+
},
785789
"occurrences": {
786790
"type": "long",
787791
"description": "number of times this backtrace has occurred"
@@ -1510,4 +1514,4 @@
15101514
}
15111515
}
15121516
}
1513-
}
1517+
}

src/v/redpanda/admin/debug.cc

+2-1
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,9 @@ admin_server::cpu_profile_handler(std::unique_ptr<ss::http::request> req) {
604604
samples.reserve(shard_profile.samples.size());
605605
for (auto& sample : shard_profile.samples) {
606606
ss::httpd::debug_json::cpu_profile_sample json_sample;
607-
json_sample.occurrences = sample.occurrences;
608607
json_sample.user_backtrace = sample.user_backtrace;
608+
json_sample.scheduling_group = sample.sg;
609+
json_sample.occurrences = sample.occurrences;
609610
samples.emplace_back(std::move(json_sample));
610611
}
611612

src/v/resource_mgmt/cpu_profiler.cc

+21-5
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,33 @@ ss::future<std::vector<cpu_profiler::shard_samples>> cpu_profiler::results(
9595
co_return results;
9696
}
9797

98+
// hashable struct holding a single sample
99+
struct single_sample {
100+
ss::simple_backtrace backtrace;
101+
ss::sstring sg;
102+
103+
bool operator==(const single_sample&) const = default;
104+
105+
template<typename H>
106+
friend H AbslHashValue(H h, const single_sample& s) {
107+
return H::combine(std::move(h), s.backtrace, s.sg);
108+
}
109+
};
110+
98111
cpu_profiler::shard_samples cpu_profiler::shard_results(
99112
std::optional<ss::lowres_clock::time_point> filter_before) const {
100113
size_t dropped_samples = 0, total_samples = 0;
101-
chunked_hash_map<ss::simple_backtrace, size_t> backtraces;
114+
chunked_hash_map<single_sample, size_t> backtraces;
102115
for (auto& results_buffer : _results_buffers) {
103116
if (filter_before && results_buffer.polled_time < *filter_before) {
104117
continue;
105118
}
106119

107120
dropped_samples += results_buffer.dropped_samples;
108121
total_samples += results_buffer.samples.size();
122+
109123
for (auto& result : results_buffer.samples) {
110-
backtraces[result.user_backtrace]++;
124+
++backtraces[{result.user_backtrace, result.sg.name()}];
111125
}
112126
}
113127

@@ -116,15 +130,17 @@ cpu_profiler::shard_samples cpu_profiler::shard_results(
116130
total_samples,
117131
backtraces.size());
118132

119-
std::vector<sample> results{};
133+
std::vector<sample> results;
120134
results.reserve(backtraces.size());
121135

122136
for (auto& backtrace : backtraces) {
123137
results.emplace_back(
124-
ssx::sformat("{}", backtrace.first), backtrace.second);
138+
ssx::sformat("{}", backtrace.first.backtrace),
139+
backtrace.first.sg,
140+
backtrace.second);
125141
}
126142

127-
return {ss::this_shard_id(), dropped_samples, results};
143+
return {ss::this_shard_id(), dropped_samples, std::move(results)};
128144
}
129145

130146
void cpu_profiler::poll_samples() {

src/v/resource_mgmt/cpu_profiler.h

+2-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ class cpu_profiler : public ss::peering_sharded_service<cpu_profiler> {
5050
public:
5151
struct sample {
5252
ss::sstring user_backtrace;
53-
size_t occurrences;
54-
55-
sample(ss::sstring ub, size_t o)
56-
: user_backtrace(std::move(ub))
57-
, occurrences(o) {}
53+
ss::sstring sg;
54+
size_t occurrences = 0;
5855
};
5956

6057
struct shard_samples {

src/v/resource_mgmt/tests/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ redpanda_cc_btest(
99
deps = [
1010
"//src/v/config",
1111
"//src/v/resource_mgmt:cpu_profiler",
12+
"//src/v/resource_mgmt:cpu_scheduling",
1213
"//src/v/test_utils:seastar_boost",
1314
"@boost//:test",
1415
"@seastar",

src/v/resource_mgmt/tests/cpu_profiler_test.cc

+56-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "config/property.h"
1313
#include "resource_mgmt/cpu_profiler.h"
14+
#include "resource_mgmt/cpu_scheduling.h"
1415

1516
#include <seastar/core/future.hh>
1617
#include <seastar/core/internal/cpu_profiler.hh>
@@ -19,7 +20,9 @@
1920
#include <seastar/core/smp.hh>
2021
#include <seastar/core/timer.hh>
2122
#include <seastar/coroutine/maybe_yield.hh>
23+
#include <seastar/coroutine/switch_to.hh>
2224
#include <seastar/testing/thread_test_case.hh>
25+
#include <seastar/util/defer.hh>
2326

2427
#include <boost/test/unit_test.hpp>
2528

@@ -32,7 +35,13 @@ using shard_samples = resources::cpu_profiler::shard_samples;
3235
using sharded_profiler = ss::sharded<resources::cpu_profiler>;
3336

3437
namespace {
35-
ss::future<> busy_loop(std::chrono::milliseconds duration) {
38+
ss::future<> busy_loop(
39+
std::chrono::milliseconds duration,
40+
std::optional<ss::scheduling_group> sg = {}) {
41+
if (sg) {
42+
co_await ss::coroutine::switch_to(*sg);
43+
}
44+
3645
auto end_time = ss::lowres_clock::now() + duration;
3746
while (ss::lowres_clock::now() < end_time) {
3847
// yield to allow timer to trigger and lowres_clock to update
@@ -69,6 +78,7 @@ SEASTAR_THREAD_TEST_CASE(test_cpu_profiler) {
6978
resources::cpu_profiler cp(
7079
config::mock_binding(true), config::mock_binding(2ms));
7180
cp.start().get();
81+
auto stop = ss::defer([&] { cp.stop().get(); });
7282

7383
// The profiler service will request samples from seastar every
7484
// 256ms since the sample rate is 2ms. So we need to be running
@@ -79,6 +89,51 @@ SEASTAR_THREAD_TEST_CASE(test_cpu_profiler) {
7989
BOOST_TEST(results.samples.size() >= 1);
8090
}
8191

92+
// We should create the sgs only once and not destroy them because sgs may be
93+
// captured by the profiler in one test and leak into the next (e.g., because
94+
// they are still in the seastar-side sample buffer), where accessing them
95+
// would be UB as they are destroyed.
96+
static scheduling_groups get_sgs() {
97+
static scheduling_groups sg = [] {
98+
scheduling_groups sg;
99+
sg.create_groups().get();
100+
return sg;
101+
}();
102+
return sg;
103+
}
104+
105+
SEASTAR_THREAD_TEST_CASE(test_cpu_scheduler_groups) {
106+
scheduling_groups sg = get_sgs();
107+
108+
resources::cpu_profiler cp(
109+
config::mock_binding(true), config::mock_binding(2ms));
110+
cp.start().get();
111+
auto stop = ss::defer([&] { cp.stop().get(); });
112+
113+
busy_loop(256ms + 10ms).get();
114+
115+
auto results = cp.shard_results();
116+
BOOST_TEST(results.samples.size() >= 1);
117+
for (auto& r : results.samples) {
118+
BOOST_REQUIRE_EQUAL(r.sg, "main");
119+
}
120+
121+
busy_loop(256ms + 10ms, sg.kafka_sg()).get();
122+
123+
results = cp.shard_results();
124+
BOOST_TEST(results.samples.size() >= 1);
125+
int found_kafka = 0;
126+
for (auto& r : results.samples) {
127+
// we accept both main and kafka as some internal reactor work
128+
// will be recorded as main group
129+
BOOST_REQUIRE_MESSAGE(
130+
r.sg == "main" || r.sg == "kafka", "unexpected group: " << r.sg);
131+
found_kafka += r.sg == "kafka";
132+
}
133+
// should get at least some kafka!
134+
BOOST_REQUIRE(found_kafka > 0);
135+
}
136+
82137
SEASTAR_THREAD_TEST_CASE(test_cpu_profiler_enable_override) {
83138
// Ensure that overrides to the profiler will enable it and collect samples
84139
// for the specified period of time.

0 commit comments

Comments
 (0)