Skip to content

Commit 55861f5

Browse files
excelle08meta-codesync[bot]
authored andcommitted
DriverNodeRank: per-second QPS trace + soft in-flight cap (#733)
Summary: Pull Request resolved: #733 Adds two env-gated knobs to FeedSimDriver. DCPERF_DRIVER_QPS_TRACE=1 spawns a 1Hz trace thread that logs per-thread sent/completed/in_flight counters to /tmp/driver_qps_trace_<pid>.log (or DCPERF_DRIVER_QPS_TRACE_FILE if set). Used to visualize whether the driver is actually pacing smoothly, hitting the connection cap, or sitting on completion gaps. Writes only to the file — no stderr output (stderr would interleave with cout's fully-buffered `final requested_qps=X measured_qps=Y latency=Z` line and break the search_qps.sh regex parser). DCPERF_DRIVER_INFLIGHT_CAP=N applies a per-thread soft cap on (sent - completed). When (in_flight >= cap), nextRequestCb skips the firing and re-arms the evtimer for the same delay. **Default 0 = disabled.** **Important caveat for INFLIGHT_CAP:** empirical testing (2026-05-19 t18 sweep on BGM, 11 iters at qps={60,80,100,120,150}) shows that small cap values like 8 reduce total throughput by ~25% without any latency benefit. QPS-trace inspection shows the cap never actually triggers because the natural per-thread inflight ceiling is `num_conns × depth = 4`, well below cap=8. The throughput penalty appears to come from per-firing atomic-load overhead in nextRequestCb. **Recommendation: leave disabled (cap=0) by default; if used at all, set cap=32+ for explicit latency-shaping experiments only.** Implementation: - DriverStats: new completed_count_ counter, bumped in logResponse; new getSentCount() / getCompletedCount() accessors. Aligned uint64 reads from another thread are atomic on x86-64 / aarch64, so no extra synchronization needed for a 1Hz poll. - FeedSimDriver::Impl: trace_thread + trace_running atomic; joined in shutdown() step 5 after libevent loop break. Reviewed By: YifanYuan3 Differential Revision: D105659811
1 parent 0dcd9df commit 55861f5

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

packages/feedsim/third_party/src/workloads/ranking/FeedSimDriver.cc

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
#include <algorithm>
2424
#include <atomic>
2525
#include <cassert>
26+
#include <chrono>
27+
#include <cstdlib>
2628
#include <cmath>
2729
#include <deque>
30+
#include <fstream>
31+
#include <iomanip>
2832
#include <iostream>
2933
#include <limits>
34+
#include <sstream>
3035
#include <map>
3136
#include <memory>
3237
#include <mutex>
@@ -129,6 +134,7 @@ void DriverStats::logResponse(uint32_t /*type*/, uint64_t latency_ns,
129134
uint32_t packet_size) {
130135
sampler_->sample(static_cast<double>(latency_ns));
131136
rx_bytes_ += packet_size;
137+
completed_count_++;
132138
}
133139

134140
void DriverStats::logFirstStoryLatency(uint64_t latency_ns) {
@@ -145,6 +151,7 @@ void DriverStats::accumulate(const DriverStats& other) {
145151
tx_bytes_ += other.tx_bytes_;
146152
rx_bytes_ += other.rx_bytes_;
147153
query_count_ += other.query_count_;
154+
completed_count_ += other.completed_count_;
148155
session_count_ += other.session_count_;
149156
}
150157

@@ -154,6 +161,7 @@ void DriverStats::reset() {
154161
tx_bytes_ = 0;
155162
rx_bytes_ = 0;
156163
query_count_ = 0;
164+
completed_count_ = 0;
157165
session_count_ = 0;
158166
start_time_ = getTimeNano();
159167
}
@@ -440,8 +448,42 @@ void TestDriver::Impl::eventCb(struct bufferevent* /*bev*/, int16_t events,
440448
}
441449
}
442450

451+
// DCPERF_DRIVER_INFLIGHT_CAP: per-thread soft cap on (sent - completed).
452+
// 0 means disabled (back-compat). When set, nextRequestCb skips firing
453+
// until in-flight drops back below the cap — couples driver firing rate
454+
// to actual server completion rate so requested_qps > server_qps cannot
455+
// pin in-flight at the connection-layer hard cap and drive latency up
456+
// indefinitely. Read once at startup (cached in g_driver_inflight_cap)
457+
// so per-tick checks don't pay env-lookup cost.
458+
static uint64_t g_driver_inflight_cap = []() {
459+
const char* env = std::getenv("DCPERF_DRIVER_INFLIGHT_CAP");
460+
if (env == nullptr || env[0] == '\0') return uint64_t{0};
461+
char* end = nullptr;
462+
unsigned long v = std::strtoul(env, &end, 10);
463+
return static_cast<uint64_t>(v);
464+
}();
465+
443466
void TestDriver::Impl::nextRequestCb(evutil_socket_t, int16_t, void* arg) {
444467
auto* driver = reinterpret_cast<TestDriver*>(arg);
468+
auto& impl = *driver->impl_;
469+
if (g_driver_inflight_cap > 0) {
470+
uint64_t sent = impl.current_stats.getSentCount();
471+
uint64_t done = impl.current_stats.getCompletedCount();
472+
uint64_t in_flight = sent > done ? (sent - done) : 0;
473+
if (in_flight >= g_driver_inflight_cap) {
474+
// Server isn't keeping up. Skip this firing and re-arm the timer
475+
// for the same delay so the next pacing tick is still on schedule
476+
// (we're not trying to "make up" the missed call — that's the
477+
// whole point of the back-off).
478+
if (impl.next_request_delay_us != 0 && impl.next_request_event != nullptr) {
479+
struct timeval tv;
480+
tv.tv_sec = impl.next_request_delay_us / 1000000;
481+
tv.tv_usec = impl.next_request_delay_us % 1000000;
482+
evtimer_add(impl.next_request_event, &tv);
483+
}
484+
return;
485+
}
486+
}
445487
makeRequests(*driver);
446488
}
447489

@@ -452,6 +494,15 @@ void TestDriver::Impl::makeRequests(TestDriver& driver) {
452494
impl.num_backlogged_requests++;
453495
return;
454496
}
497+
if (g_driver_inflight_cap > 0) {
498+
uint64_t sent = impl.current_stats.getSentCount();
499+
uint64_t done = impl.current_stats.getCompletedCount();
500+
if (sent > done && (sent - done) >= g_driver_inflight_cap) {
501+
// Same back-off rule applies inside the spin loop (which only
502+
// spins when next_request_delay_us == 0, i.e. unpaced runs).
503+
return;
504+
}
505+
}
455506
impl.make_request_cb(impl.thread_id, driver);
456507
} while (impl.next_request_delay_us == 0);
457508
}
@@ -679,6 +730,11 @@ struct FeedSimDriver::Impl {
679730

680731
event_base* main_base = nullptr;
681732
std::atomic<bool> running{false};
733+
734+
// DCPERF_DRIVER_QPS_TRACE: per-second telemetry thread. Idle unless
735+
// the env var is set to a non-empty, non-"0" value at run() time.
736+
std::thread trace_thread;
737+
std::atomic<bool> trace_running{false};
682738
};
683739

684740
// ─── FeedSimDriver public methods ───────────────────────────────────────────
@@ -834,6 +890,91 @@ void FeedSimDriver::run(uint32_t num_threads, bool thread_pinning,
834890
pthread_barrier_wait(&init_barrier);
835891
pthread_barrier_destroy(&init_barrier);
836892

893+
// DCPERF_DRIVER_QPS_TRACE: spawn a 1Hz trace thread when the env var
894+
// is set (any non-empty, non-"0" value enables it). Logs to the path
895+
// in DCPERF_DRIVER_QPS_TRACE_FILE, defaulting to
896+
// /tmp/driver_qps_trace_<pid>.log. The default uses pid so that
897+
// multi-instance feedsim runs (each DriverNodeRank is a separate
898+
// process) produce per-instance trace files instead of stomping on
899+
// one shared file. Each line:
900+
// t=<elapsed_s> total{sent_qps=X done_qps=Y inflight=Z} |
901+
// t0{sent=W done=V inflight=U} t1{...} ...
902+
// Aligned uint64 reads of query_count_/completed_count_ are atomic on
903+
// x86-64 and aarch64; no extra synchronization needed for a 1Hz poll.
904+
//
905+
// CRITICAL: do NOT print a startup banner to stderr — search_qps.sh
906+
// reads DriverNodeRank's stdout AND stderr (combined via 2>&1), and a
907+
// stderr write here interleaves with the (fully-buffered) stdout
908+
// "final requested_qps = ..., measured_qps = ..., latency = ..."
909+
// line on the pipe, fragmenting it so the parser captures wrong
910+
// values (regression observed in the t7 sweep). The trace file's
911+
// existence is sufficient evidence the trace was enabled.
912+
{
913+
const char* env = std::getenv("DCPERF_DRIVER_QPS_TRACE");
914+
if (env != nullptr && env[0] != '\0' && std::string(env) != "0") {
915+
const char* path_env = std::getenv("DCPERF_DRIVER_QPS_TRACE_FILE");
916+
std::string trace_path;
917+
if (path_env != nullptr && path_env[0] != '\0') {
918+
trace_path = std::string(path_env);
919+
} else {
920+
std::ostringstream p;
921+
p << "/tmp/driver_qps_trace_" << ::getpid() << ".log";
922+
trace_path = p.str();
923+
}
924+
impl_->trace_running = true;
925+
impl_->trace_thread = std::thread(
926+
[impl_ptr = impl_.get(), trace_path]() {
927+
std::ofstream out(trace_path, std::ios::out | std::ios::app);
928+
if (!out) return; // silent: any cerr write would corrupt search_qps
929+
const auto t0 = std::chrono::steady_clock::now();
930+
std::vector<uint64_t> last_sent(impl_ptr->threads.size(), 0);
931+
std::vector<uint64_t> last_done(impl_ptr->threads.size(), 0);
932+
for (size_t i = 0; i < impl_ptr->threads.size(); i++) {
933+
const auto& s = impl_ptr->threads[i]->driver->impl_->current_stats;
934+
last_sent[i] = s.getSentCount();
935+
last_done[i] = s.getCompletedCount();
936+
}
937+
out << "# DCPERF_DRIVER_QPS_TRACE start, threads="
938+
<< impl_ptr->threads.size() << std::endl;
939+
while (impl_ptr->trace_running.load(std::memory_order_acquire)) {
940+
std::this_thread::sleep_for(std::chrono::seconds(1));
941+
if (!impl_ptr->trace_running.load(std::memory_order_acquire)) {
942+
break;
943+
}
944+
auto now = std::chrono::steady_clock::now();
945+
double elapsed_s = std::chrono::duration<double>(now - t0).count();
946+
uint64_t total_sent_qps = 0;
947+
uint64_t total_done_qps = 0;
948+
uint64_t total_in_flight = 0;
949+
std::ostringstream per_thread;
950+
for (size_t i = 0; i < impl_ptr->threads.size(); i++) {
951+
const auto& s =
952+
impl_ptr->threads[i]->driver->impl_->current_stats;
953+
uint64_t sent = s.getSentCount();
954+
uint64_t done = s.getCompletedCount();
955+
uint64_t dsent = sent - last_sent[i];
956+
uint64_t ddone = done - last_done[i];
957+
uint64_t in_flight = sent > done ? (sent - done) : 0;
958+
last_sent[i] = sent;
959+
last_done[i] = done;
960+
total_sent_qps += dsent;
961+
total_done_qps += ddone;
962+
total_in_flight += in_flight;
963+
per_thread << " t" << i << "{sent=" << dsent
964+
<< " done=" << ddone
965+
<< " inflight=" << in_flight << "}";
966+
}
967+
out << "t=" << std::fixed << std::setprecision(1) << elapsed_s
968+
<< " total{sent_qps=" << total_sent_qps
969+
<< " done_qps=" << total_done_qps
970+
<< " inflight=" << total_in_flight << "} |"
971+
<< per_thread.str() << std::endl;
972+
}
973+
out << "# DCPERF_DRIVER_QPS_TRACE stop" << std::endl;
974+
});
975+
}
976+
}
977+
837978
double start_time = getTimeSec();
838979

839980
// Main event loop (for SIGINT handling)
@@ -920,6 +1061,16 @@ void FeedSimDriver::shutdown() {
9201061
if (impl_->main_base) {
9211062
event_base_loopbreak(impl_->main_base);
9221063
}
1064+
1065+
// Step 5: stop the DCPERF_DRIVER_QPS_TRACE thread (if it was started).
1066+
// Do this after the main loop has been broken so the trace thread
1067+
// doesn't keep firing through the join(); the trace lambda checks
1068+
// trace_running every iteration and exits cleanly.
1069+
if (impl_->trace_running.exchange(false)) {
1070+
if (impl_->trace_thread.joinable()) {
1071+
impl_->trace_thread.join();
1072+
}
1073+
}
9231074
}
9241075

9251076
} // namespace feedsim

packages/feedsim/third_party/src/workloads/ranking/FeedSimDriver.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class DriverStats {
4747

4848
uint64_t getQueryCount(uint32_t type) const;
4949
uint64_t getSessionCount() const { return session_count_; }
50+
// DCPERF_DRIVER_QPS_TRACE accessors — read by the trace thread once per
51+
// second to compute per-second sent/completed rates and the per-thread
52+
// in-flight (sent - completed). Aligned uint64 reads are atomic on
53+
// x86-64 and aarch64, so no atomic counters are needed for 1-Hz polling.
54+
uint64_t getSentCount() const { return query_count_; }
55+
uint64_t getCompletedCount() const { return completed_count_; }
5056
uint64_t getStartTimeNano() const { return start_time_; }
5157
uint64_t getEndTimeNano() const { return end_time_; }
5258
void setEndTimeNano(uint64_t t) { end_time_ = t; }
@@ -68,6 +74,7 @@ class DriverStats {
6874
uint64_t tx_bytes_ = 0;
6975
uint64_t rx_bytes_ = 0;
7076
uint64_t query_count_ = 0;
77+
uint64_t completed_count_ = 0;
7178
uint64_t session_count_ = 0;
7279
};
7380

0 commit comments

Comments
 (0)