Skip to content

Commit c1bb92c

Browse files
committed
DPL: assert an allocation budget for a relay/consume cycle
A timing benchmark cannot tell a storage-layout regression from a busy machine. Count allocations instead: with eight inputs, relaying every input plus the consume costs 18 allocations, and that number must not grow when the way a slot holds its messages changes. The messages are built before the counter is armed, so what is measured is the relayer rather than fair::mq. The global operator new replacement only counts while a test arms it, so the rest of the binary is unaffected.
1 parent 4d3caee commit c1bb92c

1 file changed

Lines changed: 123 additions & 1 deletion

File tree

Framework/Core/test/test_DataRelayer.cxx

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
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;
4245
using Stack = o2::header::Stack;
4346
using 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+
4583
TEST_CASE("DataRelayer")
4684
{
4785
ServiceRegistry registry;
@@ -1171,5 +1209,89 @@ 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
}
1175-
}

0 commit comments

Comments
 (0)