@@ -294,26 +294,158 @@ static ranking::RankingResponse generateResponse(int num_objects) {
294294// destructor chain (was RPC-DataGen=6.3% in the t31v5 profile; this drops
295295// it to <0.5%).
296296//
297- // The 64-slot rotation prevents a single template from getting cache-hot
298- // enough to skew the Serialization/Compression bucket measurements. The
299- // queryID mutation is racy across threads but acceptable: we only need
297+ // To match prod's getStoriesUncompressed response_sizes distribution
298+ // (rpc_dist_v2.json: p50=173 KB, p99=2.7 MB, p99.9=6.5 MB), the pool
299+ // holds N templates at quantized percentiles. At request time we sample
300+ // a target_bytes from the inbound.getStoriesUncompressed.response_sizes
301+ // PercentileSampler and return the template whose pre-measured serialized
302+ // size is closest to target_bytes. When --rpc_dist_path is unset the pool
303+ // degrades to a single-size pool keyed off --num_objects (legacy behavior).
304+ //
305+ // The queryID mutation is racy across threads but acceptable: we only need
300306// wire-shape stability, not value correctness for the benchmark.
301307static constexpr size_t kResponseTemplatePoolSize = 64 ;
302308static std::vector<ranking::RankingResponse> g_response_templates;
309+ // Pre-measured serialized size of each template, in bytes. Used to map a
310+ // target_bytes (sampled from rpc_dist_v2.json) to the nearest template.
311+ static std::vector<size_t > g_response_template_sizes;
303312static std::atomic<size_t > g_response_template_idx{0 };
304313
305- static void initResponseTemplatePool (int num_objects) {
306- g_response_templates.reserve (kResponseTemplatePoolSize );
307- for (size_t i = 0 ; i < kResponseTemplatePoolSize ; ++i) {
308- g_response_templates.push_back (generateResponse (num_objects));
314+ // Pool of per-thread RNGs for response-size sampling (per-thread to avoid
315+ // shared-state contention; std::mt19937 is not thread-safe).
316+ static thread_local std::mt19937 g_response_size_rng{
317+ std::random_device{}()};
318+
319+ // Helper: measure the serialized size of a RankingResponse via CompactProto.
320+ // Used at pool init to build the (size → template) lookup table.
321+ static size_t measureSerializedSize (const ranking::RankingResponse& resp) {
322+ folly::IOBufQueue queue;
323+ apache::thrift::CompactSerializer::serialize (resp, &queue);
324+ auto buf = queue.move ();
325+ return buf ? buf->computeChainDataLength () : 0 ;
326+ }
327+
328+ // Map an integer obj count to an approximate serialized response size.
329+ // Empirically each RankingObject from the Silesia generator serializes
330+ // to ~300-500 bytes (4 i64 + 3 F14FastMap + list<Action> + double, with
331+ // payloadStrMap drawing strings from Silesia). The mapping is rebuilt
332+ // per-host at pool init by serializing a probe at num_objects=64 and
333+ // dividing by 64, so platform-specific layout doesn't drift the estimate.
334+ static size_t g_bytes_per_object_estimate = 400 ;
335+
336+ static void initResponseTemplatePool (int default_num_objects) {
337+ // Probe: build one template at a known num_objects, measure its
338+ // serialized size, derive bytes/object. We use 64 objects to amortize
339+ // the per-response fixed overhead (queryID, list header, etc).
340+ constexpr int kProbeObjects = 64 ;
341+ auto probe = generateResponse (kProbeObjects );
342+ size_t probe_bytes = measureSerializedSize (probe);
343+ if (probe_bytes > 0 ) {
344+ g_bytes_per_object_estimate =
345+ std::max<size_t >(1 , probe_bytes / kProbeObjects );
346+ }
347+ std::cerr << " Response template probe: " << kProbeObjects << " objects → "
348+ << probe_bytes << " bytes (" << g_bytes_per_object_estimate
349+ << " B/obj)" << std::endl;
350+
351+ // Choose target sizes for the pool. If the rpc_dist_v2 inbound
352+ // getStoriesUncompressed response_sizes sampler is loaded, draw
353+ // kResponseTemplatePoolSize percentile samples from it so the pool
354+ // covers the full prod distribution shape. Otherwise build a flat
355+ // pool at the legacy --num_objects size.
356+ std::vector<size_t > target_sizes;
357+ target_sizes.reserve (kResponseTemplatePoolSize );
358+ bool use_dist = false ;
359+ if (g_rpc_registry != nullptr ) {
360+ const auto & sampler = g_rpc_registry->inboundResponseSize (
361+ ranking::InboundIdx::kGetStoriesUncompressed );
362+ if (sampler.isLoaded ()) {
363+ use_dist = true ;
364+ // Deterministic-but-spread sampling: walk equally-spaced percentiles
365+ // across [0, 1). Avoids RNG dependency at init time.
366+ std::mt19937 init_rng (/* seed=*/ 0xfeed51 );
367+ for (size_t i = 0 ; i < kResponseTemplatePoolSize ; ++i) {
368+ target_sizes.push_back (
369+ std::max<size_t >(1 , static_cast <size_t >(sampler.sample (init_rng))));
370+ }
371+ }
309372 }
373+ if (!use_dist) {
374+ size_t default_size = static_cast <size_t >(default_num_objects) *
375+ g_bytes_per_object_estimate;
376+ for (size_t i = 0 ; i < kResponseTemplatePoolSize ; ++i) {
377+ target_sizes.push_back (default_size);
378+ }
379+ }
380+ // Sort target_sizes so g_response_template_sizes is ascending; lets
381+ // pickResponseTemplate binary-search for the nearest match.
382+ std::sort (target_sizes.begin (), target_sizes.end ());
383+
384+ g_response_templates.reserve (kResponseTemplatePoolSize );
385+ g_response_template_sizes.reserve (kResponseTemplatePoolSize );
386+ size_t min_built = SIZE_MAX , max_built = 0 , sum_built = 0 ;
387+ for (size_t target : target_sizes) {
388+ int n_obj = std::max<int >(
389+ 1 , static_cast <int >(target / g_bytes_per_object_estimate));
390+ auto resp = generateResponse (n_obj);
391+ size_t actual = measureSerializedSize (resp);
392+ g_response_templates.push_back (std::move (resp));
393+ g_response_template_sizes.push_back (actual);
394+ min_built = std::min (min_built, actual);
395+ max_built = std::max (max_built, actual);
396+ sum_built += actual;
397+ }
398+ std::cerr << " Response template pool: " << kResponseTemplatePoolSize
399+ << " templates built from "
400+ << (use_dist
401+ ? " rpc_dist_v2.inbound.getStoriesUncompressed.response_sizes"
402+ : " fixed --num_objects" )
403+ << " ; serialized bytes min=" << min_built << " mean="
404+ << (sum_built / kResponseTemplatePoolSize ) << " max=" << max_built
405+ << std::endl;
310406}
311407
408+ // Picks a response template whose serialized size is closest to a sample
409+ // drawn from the rpc_dist_v2 response_sizes distribution. Falls back to
410+ // round-robin when the registry is unset (legacy behavior).
312411static ranking::RankingResponse& pickResponseTemplate () {
313- size_t idx = g_response_template_idx.fetch_add (
314- 1 , std::memory_order_relaxed) % kResponseTemplatePoolSize ;
315- ranking::RankingResponse& resp = g_response_templates[idx];
316- resp.queryID () = static_cast <int64_t >(idx);
412+ size_t pool_idx;
413+ if (g_rpc_registry != nullptr ) {
414+ const auto & sampler = g_rpc_registry->inboundResponseSize (
415+ ranking::InboundIdx::kGetStoriesUncompressed );
416+ if (sampler.isLoaded ()) {
417+ size_t target =
418+ static_cast <size_t >(sampler.sample (g_response_size_rng));
419+ // Binary search for the template with serialized size closest to
420+ // target. g_response_template_sizes is sorted ascending.
421+ auto it = std::lower_bound (
422+ g_response_template_sizes.begin (),
423+ g_response_template_sizes.end (),
424+ target);
425+ if (it == g_response_template_sizes.end ()) {
426+ pool_idx = g_response_template_sizes.size () - 1 ;
427+ } else if (it == g_response_template_sizes.begin ()) {
428+ pool_idx = 0 ;
429+ } else {
430+ // Pick the closer of (*it) or (*(it - 1))
431+ size_t hi_diff = *it - target;
432+ size_t lo_diff = target - *(it - 1 );
433+ pool_idx = (lo_diff < hi_diff)
434+ ? static_cast <size_t >((it - 1 ) - g_response_template_sizes.begin ())
435+ : static_cast <size_t >(it - g_response_template_sizes.begin ());
436+ }
437+ } else {
438+ pool_idx = g_response_template_idx.fetch_add (
439+ 1 , std::memory_order_relaxed) %
440+ kResponseTemplatePoolSize ;
441+ }
442+ } else {
443+ pool_idx = g_response_template_idx.fetch_add (
444+ 1 , std::memory_order_relaxed) %
445+ kResponseTemplatePoolSize ;
446+ }
447+ ranking::RankingResponse& resp = g_response_templates[pool_idx];
448+ resp.queryID () = static_cast <int64_t >(pool_idx);
317449 return resp;
318450}
319451
@@ -2411,15 +2543,6 @@ int main(int argc, char** argv) {
24112543 << std::endl;
24122544 }
24132545
2414- // Pre-build the response template pool now that generateResponse is wired
2415- // (either Silesia or xor128). Each handler grabs a template by index +
2416- // mutates queryID, eliminating the per-request construct+destruct chain
2417- // that was 6.3% of total CPU (RPC-DataGen) in the t31v5 profile.
2418- initResponseTemplatePool (args.num_objects_arg );
2419- std::cout << " Response template pool: " << kResponseTemplatePoolSize
2420- << " pre-built " << args.num_objects_arg
2421- << " -object RankingResponse instances" << std::endl;
2422-
24232546 // Phase 5: load rpc_dist.json and instantiate the RpcDistRegistry. When
24242547 // --rpc_dist_path is empty (default) OR --use_legacy_sleep is set, the
24252548 // legacy folly::futures::sleep I/O simulation is used everywhere --
@@ -2434,6 +2557,9 @@ int main(int argc, char** argv) {
24342557 // every ThreadStartup skip MockServicesClient construction (gate at
24352558 // `rpc_registry != nullptr`) and simulateIoOrFanout fall through to
24362559 // folly::futures::sleep (gate at `mock_client != nullptr`).
2560+ //
2561+ // NOTE: registry is loaded BEFORE initResponseTemplatePool so the
2562+ // response-size distribution sampler can shape the template pool.
24372563 if (args.use_legacy_sleep_flag ) {
24382564 std::cout << " RPC fanout: disabled (--use_legacy_sleep override);"
24392565 << " using legacy folly::futures::sleep" << std::endl;
@@ -2459,6 +2585,14 @@ int main(int argc, char** argv) {
24592585 << " folly::futures::sleep" << std::endl;
24602586 }
24612587
2588+ // Pre-build the response template pool now that generateResponse is wired
2589+ // (either Silesia or xor128) AND g_rpc_registry is loaded (so the pool
2590+ // can shape itself to the response_sizes distribution). Each handler
2591+ // grabs a template by index + mutates queryID, eliminating the
2592+ // per-request construct+destruct chain that was 6.3% of total CPU
2593+ // (RPC-DataGen) in the t31v5 profile.
2594+ initResponseTemplatePool (args.num_objects_arg );
2595+
24622596 int fake_argc = 1 ;
24632597 char * fake_argv[2 ] = {const_cast <char *>(" ./LeafNodeRank" ), nullptr };
24642598 char ** sargv = static_cast <char **>(fake_argv);
0 commit comments