3333#include " Framework/LifetimeHelpers.h"
3434#include < array>
3535#include < cstring>
36+ #include < new>
37+ #include < cstdlib>
38+ #include < atomic>
3639#include < vector>
3740#include < uv.h>
3841
@@ -42,6 +45,41 @@ using DataHeader = o2::header::DataHeader;
4245using Stack = o2::header::Stack;
4346using RecordAction = o2::framework::DataRelayer::RecordAction;
4447
48+ // Replacing the global allocation functions lets a test assert an allocation
49+ // *budget* rather than a wall-clock time: the DataRelayer's storage layout is
50+ // supposed to cost a bounded number of allocations per timeslice, and that is a
51+ // deterministic property, unlike a benchmark on a shared machine. Counting is
52+ // off unless a test arms it, so nothing else in the binary is affected.
53+ namespace
54+ {
55+ std::atomic<bool > gCountAllocations {false };
56+ std::atomic<size_t > gAllocations {0 };
57+
58+ struct AllocationCounter {
59+ AllocationCounter ()
60+ {
61+ gAllocations .store (0 , std::memory_order_relaxed);
62+ gCountAllocations .store (true , std::memory_order_relaxed);
63+ }
64+ ~AllocationCounter () { gCountAllocations .store (false , std::memory_order_relaxed); }
65+ static size_t count () { return gAllocations .load (std::memory_order_relaxed); }
66+ };
67+ } // namespace
68+
69+ void * operator new (std::size_t size)
70+ {
71+ if (gCountAllocations .load (std::memory_order_relaxed)) {
72+ gAllocations .fetch_add (1 , std::memory_order_relaxed);
73+ }
74+ if (void * p = std::malloc (size ? size : 1 )) {
75+ return p;
76+ }
77+ throw std::bad_alloc ();
78+ }
79+
80+ void operator delete (void * p) noexcept { std::free (p); }
81+ void operator delete (void * p, std::size_t ) noexcept { std::free (p); }
82+
4583TEST_CASE (" DataRelayer" )
4684{
4785 ServiceRegistry registry;
@@ -1171,6 +1209,91 @@ TEST_CASE("DataRelayer")
11711209 uint32_t seen = 0 ;
11721210 memcpy (&seen, payload->GetData (), sizeof (seen));
11731211 REQUIRE (seen == stampOf (i));
1212+
1213+ // A storage-layout change is supposed to cost a bounded number of allocations
1214+ // per timeslice regardless of how many inputs there are. Assert that budget
1215+ // directly: it is deterministic, unlike timing it on a machine that is also
1216+ // compiling. The bound below is what upstream costs; if a change makes the
1217+ // relayer allocate more per timeslice, this fails without anyone having to
1218+ // read a benchmark table.
1219+ SECTION (" RelayAllocationBudget" )
1220+ {
1221+ constexpr size_t kInputs = 8 ;
1222+ std::vector<InputSpec> specs;
1223+ std::vector<InputRoute> inputs;
1224+ std::vector<DataHeader> prototypes;
1225+ std::array<char const *, kInputs > const descriptions = {
1226+ " CLUSTERS" , " TRACKS" , " DIGITS" , " VERTICES" , " ERRORS" , " CALIB" , " RAWDATA" , " MCLABELS" };
1227+ for (size_t i = 0 ; i < kInputs ; ++i) {
1228+ o2::header::DataDescription desc;
1229+ desc.runtimeInit (descriptions[i]);
1230+ specs.emplace_back (InputSpec{" in" , " TST" , desc});
1231+ }
1232+ for (size_t i = 0 ; i < kInputs ; ++i) {
1233+ inputs.emplace_back (InputRoute{specs[i], i, " Fake" , 0 });
1234+ DataHeader dh;
1235+ dh.dataOrigin = " TST" ;
1236+ dh.dataDescription .runtimeInit (descriptions[i]);
1237+ dh.subSpecification = 0 ;
1238+ dh.splitPayloadIndex = 0 ;
1239+ dh.splitPayloadParts = 1 ;
1240+ dh.payloadSize = 8 ;
1241+ prototypes.push_back (dh);
1242+ }
1243+
1244+ std::vector<InputChannelInfo> infos{1 };
1245+ TimesliceIndex index{1 , infos};
1246+ ref.registerService (ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
1247+
1248+ auto policy = CompletionPolicyHelpers::consumeWhenAll ();
1249+ DataRelayer relayer (policy, inputs, index, {registry}, -1 );
1250+ relayer.setPipelineLength (1 );
1251+
1252+ auto transport = fair::mq::TransportFactory::CreateTransportFactory (" zeromq" );
1253+ auto channelAlloc = o2::pmr::getTransportAllocator (transport.get ());
1254+
1255+ // Build the messages first: creating them allocates, and that cost has
1256+ // nothing to do with how the relayer stores them. Only the relay + consume
1257+ // is measured.
1258+ auto makeMessages = [&](size_t timeslice) {
1259+ std::vector<std::array<fair::mq::MessagePtr, 2 >> msgs (kInputs );
1260+ for (size_t i = 0 ; i < kInputs ; ++i) {
1261+ msgs[i][0 ] = o2::pmr::getMessage (Stack{channelAlloc, prototypes[i], DataProcessingHeader{timeslice, 1 }});
1262+ msgs[i][1 ] = transport->CreateMessage (8 );
1263+ }
1264+ return msgs;
1265+ };
1266+
1267+ auto cycle = [&](std::vector<std::array<fair::mq::MessagePtr, 2 >>& msgs) {
1268+ for (size_t i = 0 ; i < kInputs ; ++i) {
1269+ DataRelayer::InputInfo info{0 , 2 , DataRelayer::InputType::Data, {ChannelIndex::INVALID }};
1270+ relayer.relay (msgs[i][0 ]->GetData (), msgs[i].data (), info, 2 );
1271+ }
1272+ std::vector<RecordAction> ready;
1273+ relayer.getReadyToProcess (ready);
1274+ REQUIRE (ready.size () == 1 );
1275+ return relayer.consumeAllInputsForTimeslice (ready[0 ].slot );
1276+ };
1277+
1278+ // Warm up, so the measured cycle is the recurring cost rather than the
1279+ // first-time growth of every internal buffer.
1280+ for (size_t t = 0 ; t < 4 ; ++t) {
1281+ auto msgs = makeMessages (t);
1282+ auto warm = cycle (msgs);
1283+ }
1284+
1285+ auto msgs = makeMessages (4 );
1286+ size_t allocations = 0 ;
1287+ {
1288+ AllocationCounter counting;
1289+ auto result = cycle (msgs);
1290+ allocations = AllocationCounter::count ();
1291+ }
1292+ // With one vector per input this measures 18 for eight inputs. The exact
1293+ // figure matters less than the fact that it must not grow when the way a
1294+ // slot's messages are stored changes; tighten the bound if it drops.
1295+ REQUIRE (allocations <= 18 );
1296+ }
11741297 }
11751298 }
11761299}
0 commit comments