Project
Loading...
Searching...
No Matches
test_DataRelayer.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
12#include <catch_amalgamated.hpp>
14#include "Headers/DataHeader.h"
15#include "Headers/Stack.h"
24#include "../src/DataRelayerHelpers.h"
28#include <Monitoring/Monitoring.h>
29#include <fairmq/TransportFactory.h>
30#include <fairmq/Channel.h>
34#include <array>
35#include <cstring>
36#include <new>
37#include <cstdlib>
38#include <atomic>
39#include <vector>
40#include <uv.h>
41
42using Monitoring = o2::monitoring::Monitoring;
43using namespace o2::framework;
47
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.
53namespace
54{
55std::atomic<bool> gCountAllocations{false};
56std::atomic<size_t> gAllocations{0};
57
58struct 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
69void* 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
80void operator delete(void* p) noexcept { std::free(p); }
81void operator delete(void* p, std::size_t) noexcept { std::free(p); }
82
83TEST_CASE("DataRelayer")
84{
85 ServiceRegistry registry;
86 ServiceRegistryRef ref{registry};
87 Monitoring monitoring;
88 const DriverConfig driverConfig{
89 .batch = false,
90 };
96 TimingHelpers::defaultCPUTimeConfigurator(uv_default_loop()), {});
97 int quickUpdateInterval = 1;
98 using MetricSpec = DataProcessingStats::MetricSpec;
99 std::vector<MetricSpec> specs{
100 MetricSpec{.name = "malformed_inputs", .metricId = static_cast<short>(ProcessingStatsId::MALFORMED_INPUTS), .minPublishInterval = quickUpdateInterval},
101 MetricSpec{.name = "dropped_computations", .metricId = static_cast<short>(ProcessingStatsId::DROPPED_COMPUTATIONS), .minPublishInterval = quickUpdateInterval},
102 MetricSpec{.name = "dropped_incoming_messages", .metricId = static_cast<short>(ProcessingStatsId::DROPPED_INCOMING_MESSAGES), .minPublishInterval = quickUpdateInterval},
103 MetricSpec{.name = "relayed_messages", .metricId = static_cast<short>(ProcessingStatsId::RELAYED_MESSAGES), .minPublishInterval = quickUpdateInterval}};
104
105 for (auto& spec : specs) {
106 stats.registerMetric(spec);
107 }
108
110 ref.registerService(ServiceRegistryHelpers::handleForService<Monitoring>(&monitoring));
111 ref.registerService(ServiceRegistryHelpers::handleForService<DataProcessingStats>(&stats));
112 ref.registerService(ServiceRegistryHelpers::handleForService<DataProcessingStates>(&states));
113 ref.registerService(ServiceRegistryHelpers::handleForService<DriverConfig const>(&driverConfig));
114 ref.registerService(ServiceRegistryHelpers::handleForService<DeviceState>(&state));
115 // A simple test where an input is provided
116 // and the subsequent InputRecord is immediately requested.
117 SECTION("TestNoWait")
118 {
119 InputSpec spec{"clusters", "TPC", "CLUSTERS"};
120
121 std::vector<InputRoute> inputs = {
122 InputRoute{spec, 0, "Fake", 0}};
123
124 std::vector<ForwardRoute> forwards;
125 std::vector<InputChannelInfo> infos{1};
126 TimesliceIndex index{1, infos};
127 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
128
130 DataRelayer relayer(policy, inputs, index, {registry}, -1);
131 relayer.setPipelineLength(4);
132
133 // Let's create a dummy O2 Message with two headers in the stack:
134 // - DataHeader matching the one provided in the input
135 DataHeader dh;
136 dh.dataDescription = "CLUSTERS";
137 dh.dataOrigin = "TPC";
138 dh.subSpecification = 0;
139 dh.splitPayloadIndex = 0;
140 dh.splitPayloadParts = 1;
141
142 DataProcessingHeader dph{0, 1};
143 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
144 std::array<fair::mq::MessagePtr, 2> messages;
145 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
146 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, dph});
147 messages[1] = transport->CreateMessage(1000);
148 fair::mq::MessagePtr& header = messages[0];
149 fair::mq::MessagePtr& payload = messages[1];
150 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
151 relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
152 std::vector<RecordAction> ready;
153 relayer.getReadyToProcess(ready);
154 REQUIRE(ready.size() == 1);
155 REQUIRE(ready[0].slot.index == 0);
156 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
157 REQUIRE(header.get() == nullptr);
158 REQUIRE(payload.get() == nullptr);
159 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
160 // one MessageSet with one PartRef with header and payload
161 REQUIRE((result | count_inputs{}) == 1);
162 REQUIRE((result[0] | count_parts{}) == 1);
163 }
164
165 //
166 SECTION("TestNoWaitMatcher")
167 {
169 auto specs = o2::framework::select("clusters:TPC/CLUSTERS");
170
171 std::vector<InputRoute> inputs = {
172 InputRoute{specs[0], 0, "Fake", 0}};
173
174 std::vector<ForwardRoute> forwards;
175 std::vector<InputChannelInfo> infos{1};
176 TimesliceIndex index{1, infos};
177 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
178
180 DataRelayer relayer(policy, inputs, index, {registry}, -1);
181 relayer.setPipelineLength(4);
182
183 // Let's create a dummy O2 Message with two headers in the stack:
184 // - DataHeader matching the one provided in the input
185 DataHeader dh;
186 dh.dataDescription = "CLUSTERS";
187 dh.dataOrigin = "TPC";
188 dh.subSpecification = 0;
189 dh.splitPayloadIndex = 0;
190 dh.splitPayloadParts = 1;
191
192 DataProcessingHeader dph{0, 1};
193 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
194 std::array<fair::mq::MessagePtr, 2> messages;
195 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
196 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, dph});
197 messages[1] = transport->CreateMessage(1000);
198 fair::mq::MessagePtr& header = messages[0];
199 fair::mq::MessagePtr& payload = messages[1];
200 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
201 relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
202 std::vector<RecordAction> ready;
203 relayer.getReadyToProcess(ready);
204 REQUIRE(ready.size() == 1);
205 REQUIRE(ready[0].slot.index == 0);
206 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
207 REQUIRE(header.get() == nullptr);
208 REQUIRE(payload.get() == nullptr);
209 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
210 // one MessageSet with one PartRef with header and payload
211 REQUIRE((result | count_inputs{}) == 1);
212 REQUIRE((result[0] | count_parts{}) == 1);
213 }
214
215 // This test a more complicated set of inputs, and verifies that data is
216 // correctly relayed before being processed.
217 SECTION("TestRelay")
218 {
220 InputSpec spec1{
221 "clusters",
222 "TPC",
223 "CLUSTERS",
224 };
225 InputSpec spec2{
226 "clusters_its",
227 "ITS",
228 "CLUSTERS",
229 };
230
231 std::vector<InputRoute> inputs = {
232 InputRoute{spec1, 0, "Fake1", 0},
233 InputRoute{spec2, 1, "Fake2", 0}};
234
235 std::vector<ForwardRoute> forwards;
236
237 std::vector<InputChannelInfo> infos{1};
238 TimesliceIndex index{1, infos};
239 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
240
242 DataRelayer relayer(policy, inputs, index, {registry}, -1);
243 relayer.setPipelineLength(4);
244
245 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
246 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
247
248 auto createMessage = [&transport, &channelAlloc, &relayer](DataHeader& dh, size_t time) {
249 std::array<fair::mq::MessagePtr, 2> messages;
250 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{time, 1}});
251 messages[1] = transport->CreateMessage(1000);
252 fair::mq::MessagePtr& header = messages[0];
253 fair::mq::MessagePtr& payload = messages[1];
254 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
255 relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
256 REQUIRE(header.get() == nullptr);
257 REQUIRE(payload.get() == nullptr);
258 };
259
260 // Let's create a dummy O2 Message with two headers in the stack:
261 // - DataHeader matching the one provided in the input
262 DataHeader dh1;
263 dh1.dataDescription = "CLUSTERS";
264 dh1.dataOrigin = "TPC";
265 dh1.subSpecification = 0;
266 dh1.splitPayloadIndex = 0;
267 dh1.splitPayloadParts = 1;
268
269 // Let's create the second O2 Message:
270 DataHeader dh2;
271 dh2.dataDescription = "CLUSTERS";
272 dh2.dataOrigin = "ITS";
273 dh2.subSpecification = 0;
274 dh2.splitPayloadIndex = 0;
275 dh2.splitPayloadParts = 1;
276
277 createMessage(dh1, 0);
278 std::vector<RecordAction> ready;
279 relayer.getReadyToProcess(ready);
280 REQUIRE(ready.size() == 0);
281
282 createMessage(dh2, 0);
283 ready.clear();
284 relayer.getReadyToProcess(ready);
285 REQUIRE(ready.size() == 1);
286 REQUIRE(ready[0].slot.index == 0);
287 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
288
289 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
290 // two MessageSets, each with one PartRef
291 REQUIRE((result | count_inputs{}) == 2);
292 REQUIRE((result[0] | count_parts{}) == 1);
293 REQUIRE((result[1] | count_parts{}) == 1);
294 }
295
296 // This test a more complicated set of inputs, and verifies that data is
297 // correctly relayed before being processed.
298 SECTION("TestRelayBug")
299 {
301 InputSpec spec1{
302 "clusters",
303 "TPC",
304 "CLUSTERS",
305 };
306 InputSpec spec2{
307 "clusters_its",
308 "ITS",
309 "CLUSTERS",
310 };
311
312 std::vector<InputRoute> inputs = {
313 InputRoute{spec1, 0, "Fake1", 0},
314 InputRoute{spec2, 1, "Fake2", 0}};
315
316 std::vector<ForwardRoute> forwards;
317
318 std::vector<InputChannelInfo> infos{1};
319 TimesliceIndex index{1, infos};
320 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
321
323 DataRelayer relayer(policy, inputs, index, {registry}, -1);
324 relayer.setPipelineLength(3);
325
326 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
327 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
328
329 auto createMessage = [&transport, &channelAlloc, &relayer](DataHeader& dh, size_t time) {
330 std::array<fair::mq::MessagePtr, 2> messages;
331 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{time, 1}});
332 messages[1] = transport->CreateMessage(1000);
333 fair::mq::MessagePtr& header = messages[0];
334 fair::mq::MessagePtr& payload = messages[1];
335 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
336 relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
337 REQUIRE(header.get() == nullptr);
338 REQUIRE(payload.get() == nullptr);
339 };
340
341 // Let's create a dummy O2 Message with two headers in the stack:
342 // - DataHeader matching the one provided in the input
343 DataHeader dh1;
344 dh1.dataDescription = "CLUSTERS";
345 dh1.dataOrigin = "TPC";
346 dh1.subSpecification = 0;
347 dh1.splitPayloadIndex = 0;
348 dh1.splitPayloadParts = 1;
349
350 // Let's create the second O2 Message:
351 DataHeader dh2;
352 dh2.dataDescription = "CLUSTERS";
353 dh2.dataOrigin = "ITS";
354 dh2.subSpecification = 0;
355 dh2.splitPayloadIndex = 0;
356 dh2.splitPayloadParts = 1;
357
358 // Let's create the second O2 Message:
359 DataHeader dh3;
360 dh3.dataDescription = "CLUSTERS";
361 dh3.dataOrigin = "FOO";
362 dh3.subSpecification = 0;
363 dh3.splitPayloadIndex = 0;
364 dh3.splitPayloadParts = 1;
365
367 createMessage(dh1, 0);
368 std::vector<RecordAction> ready;
369 relayer.getReadyToProcess(ready);
370 REQUIRE(ready.size() == 0);
371 createMessage(dh1, 1);
372 ready.clear();
373 relayer.getReadyToProcess(ready);
374 REQUIRE(ready.size() == 0);
375 createMessage(dh2, 0);
376 ready.clear();
377 relayer.getReadyToProcess(ready);
378 REQUIRE(ready.size() == 1);
379 REQUIRE(ready[0].slot.index == 0);
380 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
381 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
382 createMessage(dh2, 1);
383 ready.clear();
384 relayer.getReadyToProcess(ready);
385 REQUIRE(ready.size() == 1);
386 REQUIRE(ready[0].slot.index == 1);
387 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
388 result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
389 }
390
391 // This tests a simple cache pruning, where a single input is shifted out of
392 // the cache.
393 SECTION("TestCache")
394 {
396 InputSpec spec{"clusters", "TPC", "CLUSTERS"};
397
398 std::vector<InputRoute> inputs = {
399 InputRoute{spec, 0, "Fake", 0}};
400 std::vector<ForwardRoute> forwards;
401
403 std::vector<InputChannelInfo> infos{1};
404 TimesliceIndex index{1, infos};
405 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
406 DataRelayer relayer(policy, inputs, index, {registry}, -1);
407 // Only two messages to fill the cache.
408 relayer.setPipelineLength(2);
409
410 // Let's create a dummy O2 Message with two headers in the stack:
411 // - DataHeader matching the one provided in the input
412 DataHeader dh;
413 dh.dataDescription = "CLUSTERS";
414 dh.dataOrigin = "TPC";
415 dh.subSpecification = 0;
416 dh.splitPayloadIndex = 0;
417 dh.splitPayloadParts = 1;
418
419 DataProcessingHeader dph{0, 1};
420 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
421 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
422 auto createMessage = [&transport, &channelAlloc, &relayer, &dh](auto const& h) {
423 std::array<fair::mq::MessagePtr, 2> messages;
424 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, h});
425 messages[1] = transport->CreateMessage(1000);
426 fair::mq::MessagePtr& header = messages[0];
427 fair::mq::MessagePtr& payload = messages[1];
428 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
429 auto res = relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
430 REQUIRE((res.type != DataRelayer::RelayChoice::Type::WillRelay || header.get() == nullptr));
431 REQUIRE((res.type != DataRelayer::RelayChoice::Type::WillRelay || payload.get() == nullptr));
432 REQUIRE((res.type != DataRelayer::RelayChoice::Type::Backpressured || header.get() != nullptr));
433 REQUIRE((res.type != DataRelayer::RelayChoice::Type::Backpressured || payload.get() != nullptr));
434 };
435
436 // This fills the cache, and then empties it.
437 createMessage(DataProcessingHeader{0, 1});
438 createMessage(DataProcessingHeader{1, 1});
439 std::vector<RecordAction> ready;
440 relayer.getReadyToProcess(ready);
441 REQUIRE(ready.size() == 2);
442 REQUIRE(ready[0].slot.index == 1);
443 REQUIRE(ready[1].slot.index == 0);
444 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
445 REQUIRE(ready[1].op == CompletionPolicy::CompletionOp::Consume);
446 for (size_t i = 0; i < ready.size(); ++i) {
447 auto result = relayer.consumeAllInputsForTimeslice(ready[i].slot);
448 }
449
450 // This fills the cache and makes 2 obsolete.
451 createMessage(DataProcessingHeader{2, 1});
452 createMessage(DataProcessingHeader{3, 1});
453 createMessage(DataProcessingHeader{4, 1});
454 ready.clear();
455 relayer.getReadyToProcess(ready);
456 REQUIRE(ready.size() == 2);
457
458 auto result1 = relayer.consumeAllInputsForTimeslice(ready[0].slot);
459 auto result2 = relayer.consumeAllInputsForTimeslice(ready[1].slot);
460 // One for the header, one for the payload
461 REQUIRE((result1 | count_inputs{}) == 1);
462 REQUIRE((result2 | count_inputs{}) == 1);
463 }
464
465 // This the any policy. Even when there are two inputs, given the any policy
466 // it will run immediately.
467 SECTION("TestPolicies")
468 {
470 InputSpec spec1{"clusters", "TPC", "CLUSTERS"};
471 InputSpec spec2{"tracks", "TPC", "TRACKS"};
472
473 std::vector<InputRoute> inputs = {
474 InputRoute{spec1, 0, "Fake1", 0},
475 InputRoute{spec2, 1, "Fake2", 0},
476 };
477
478 std::vector<ForwardRoute> forwards;
479 std::vector<InputChannelInfo> infos{1};
480 TimesliceIndex index{1, infos};
481 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
482
484 DataRelayer relayer(policy, inputs, index, {registry}, -1);
485 // Only two messages to fill the cache.
486 relayer.setPipelineLength(2);
487
488 // Let's create a dummy O2 Message with two headers in the stack:
489 // - DataHeader matching the one provided in the input
490 DataHeader dh1;
491 dh1.dataDescription = "CLUSTERS";
492 dh1.dataOrigin = "TPC";
493 dh1.subSpecification = 0;
494 dh1.splitPayloadIndex = 0;
495 dh1.splitPayloadParts = 1;
496
497 DataHeader dh2;
498 dh2.dataDescription = "TRACKS";
499 dh2.dataOrigin = "TPC";
500 dh2.subSpecification = 0;
501 dh2.splitPayloadIndex = 0;
502 dh2.splitPayloadParts = 1;
503
504 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
505 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
506 auto createMessage = [&transport, &channelAlloc, &relayer](auto const& dh, auto const& h) {
507 std::array<fair::mq::MessagePtr, 2> messages;
508 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, h});
509 messages[1] = transport->CreateMessage(1000);
510 fair::mq::MessagePtr& header = messages[0];
511 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
512 return relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
513 };
514
515 // This fills the cache, and then empties it.
516 createMessage(dh1, DataProcessingHeader{0, 1});
517 std::vector<RecordAction> ready1;
518 relayer.getReadyToProcess(ready1);
519 REQUIRE(ready1.size() == 1);
520 REQUIRE(ready1[0].slot.index == 0);
521 REQUIRE(ready1[0].op == CompletionPolicy::CompletionOp::Process);
522
523 createMessage(dh1, DataProcessingHeader{1, 1});
524 std::vector<RecordAction> ready2;
525 relayer.getReadyToProcess(ready2);
526 REQUIRE(ready2.size() == 1);
527 REQUIRE(ready2[0].slot.index == 1);
528 REQUIRE(ready2[0].op == CompletionPolicy::CompletionOp::Process);
529
530 createMessage(dh2, DataProcessingHeader{1, 1});
531 std::vector<RecordAction> ready3;
532 relayer.getReadyToProcess(ready3);
533 REQUIRE(ready3.size() == 1);
534 REQUIRE(ready3[0].slot.index == 1);
535 REQUIRE(ready3[0].op == CompletionPolicy::CompletionOp::Consume);
536 }
537
539 SECTION("TestClear")
540 {
542 InputSpec spec1{"clusters", "TPC", "CLUSTERS"};
543 InputSpec spec2{"tracks", "TPC", "TRACKS"};
544
545 std::vector<InputRoute> inputs = {
546 InputRoute{spec1, 0, "Fake1", 0},
547 InputRoute{spec2, 1, "Fake2", 0},
548 };
549
550 std::vector<ForwardRoute> forwards;
551 std::vector<InputChannelInfo> infos{1};
552 TimesliceIndex index{1, infos};
553 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
554
556 DataRelayer relayer(policy, inputs, index, {registry}, -1);
557 // Only two messages to fill the cache.
558 relayer.setPipelineLength(3);
559
560 // Let's create a dummy O2 Message with two headers in the stack:
561 // - DataHeader matching the one provided in the input
562 DataHeader dh1;
563 dh1.dataDescription = "CLUSTERS";
564 dh1.dataOrigin = "TPC";
565 dh1.subSpecification = 0;
566 dh1.splitPayloadIndex = 0;
567 dh1.splitPayloadParts = 1;
568
569 DataHeader dh2;
570 dh2.dataDescription = "TRACKS";
571 dh2.dataOrigin = "TPC";
572 dh2.subSpecification = 0;
573 dh2.splitPayloadIndex = 0;
574 dh2.splitPayloadParts = 1;
575
576 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
577 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
578 auto createMessage = [&transport, &channelAlloc, &relayer](auto const& dh, auto const& h) {
579 std::array<fair::mq::MessagePtr, 2> messages;
580 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, h});
581 messages[1] = transport->CreateMessage(1000);
582 fair::mq::MessagePtr& header = messages[0];
583 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
584 return relayer.relay(header->GetData(), messages.data(), fakeInfo, messages.size());
585 };
586
587 // This fills the cache, and then empties it.
588 createMessage(dh1, DataProcessingHeader{0, 1});
589 createMessage(dh1, DataProcessingHeader{1, 1});
590 createMessage(dh2, DataProcessingHeader{1, 1});
591 relayer.clear();
592 std::vector<RecordAction> ready;
593 relayer.getReadyToProcess(ready);
594 REQUIRE(ready.size() == 0);
595 }
596
598 SECTION("TestTooMany")
599 {
601 InputSpec spec1{"clusters", "TPC", "CLUSTERS"};
602 InputSpec spec2{"tracks", "TPC", "TRACKS"};
603
604 std::vector<InputRoute> inputs = {
605 InputRoute{spec1, 0, "Fake1", 0},
606 InputRoute{spec2, 1, "Fake2", 0},
607 };
608
609 std::vector<ForwardRoute> forwards;
610 std::vector<InputChannelInfo> infos{1};
611 TimesliceIndex index{1, infos};
612 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
613
615 DataRelayer relayer(policy, inputs, index, {registry}, -1);
616 // Only two messages to fill the cache.
617 relayer.setPipelineLength(1);
618
619 // Let's create a dummy O2 Message with two headers in the stack:
620 // - DataHeader matching the one provided in the input
621 DataHeader dh1;
622 dh1.dataDescription = "CLUSTERS";
623 dh1.dataOrigin = "TPC";
624 dh1.subSpecification = 0;
625 dh1.splitPayloadIndex = 0;
626 dh1.splitPayloadParts = 1;
627
628 DataHeader dh2;
629 dh2.dataDescription = "TRACKS";
630 dh2.dataOrigin = "TPC";
631 dh2.subSpecification = 0;
632 dh2.splitPayloadIndex = 0;
633 dh2.splitPayloadParts = 1;
634
635 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
636 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
637
638 std::array<fair::mq::MessagePtr, 4> messages;
639 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh1, DataProcessingHeader{0, 1}});
640 messages[1] = transport->CreateMessage(1000);
641 fair::mq::MessagePtr& header = messages[0];
642 fair::mq::MessagePtr& payload = messages[1];
643 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
644 relayer.relay(header->GetData(), &messages[0], fakeInfo, 2);
645 REQUIRE(header.get() == nullptr);
646 REQUIRE(payload.get() == nullptr);
647 // This fills the cache, and then waits.
648 messages[2] = o2::pmr::getMessage(Stack{channelAlloc, dh1, DataProcessingHeader{1, 1}});
649 messages[3] = transport->CreateMessage(1000);
650 fair::mq::MessagePtr& header2 = messages[2];
651 fair::mq::MessagePtr& payload2 = messages[3];
652 DataRelayer::InputInfo fakeInfo2{2, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
653 auto action = relayer.relay(header2->GetData(), &messages[2], fakeInfo2, 2);
654 REQUIRE(action.type == DataRelayer::RelayChoice::Type::Backpressured);
655 REQUIRE(header2.get() != nullptr);
656 REQUIRE(payload2.get() != nullptr);
657 }
658
659 SECTION("SplitParts")
660 {
662 InputSpec spec1{"clusters", "TPC", "CLUSTERS"};
663 InputSpec spec2{"its", "ITS", "CLUSTERS"};
664
665 std::vector<InputRoute> inputs = {
666 InputRoute{spec1, 0, "Fake1", 0},
667 InputRoute{spec2, 0, "Fake2", 0},
668 };
669
670 std::vector<ForwardRoute> forwards;
671 std::vector<InputChannelInfo> infos{1};
672 TimesliceIndex index{1, infos};
673 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
674
676 DataRelayer relayer(policy, inputs, index, {registry}, -1);
677 // Only two messages to fill the cache.
678 relayer.setPipelineLength(1);
679
680 // Let's create a dummy O2 Message with two headers in the stack:
681 // - DataHeader matching the one provided in the input
682 DataHeader dh1;
683 dh1.dataDescription = "CLUSTERS";
684 dh1.dataOrigin = "TPC";
685 dh1.subSpecification = 0;
686 dh1.splitPayloadIndex = 0;
687 dh1.splitPayloadParts = 1;
688
689 DataHeader dh2;
690 dh2.dataDescription = "TRACKS";
691 dh2.dataOrigin = "TPC";
692 dh2.subSpecification = 0;
693 dh2.splitPayloadIndex = 0;
694 dh2.splitPayloadParts = 1;
695
696 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
697 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
698
699 std::array<fair::mq::MessagePtr, 6> messages;
700 messages[0] = o2::pmr::getMessage(Stack{channelAlloc, dh1, DataProcessingHeader{0, 1}});
701 messages[1] = transport->CreateMessage(1000);
702 fair::mq::MessagePtr& header = messages[0];
703 fair::mq::MessagePtr& payload = messages[1];
704 DataRelayer::InputInfo fakeInfo{0, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
705 relayer.relay(header->GetData(), &messages[0], fakeInfo, 2);
706 REQUIRE(header.get() == nullptr);
707 REQUIRE(payload.get() == nullptr);
708 // This fills the cache, and then waits.
709 messages[2] = o2::pmr::getMessage(Stack{channelAlloc, dh1, DataProcessingHeader{1, 1}});
710 messages[3] = transport->CreateMessage(1000);
711 fair::mq::MessagePtr& header2 = messages[2];
712 fair::mq::MessagePtr& payload2 = messages[3];
713 DataRelayer::InputInfo fakeInfo2{2, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
714 auto action = relayer.relay(header2->GetData(), &messages[2], fakeInfo, 2);
715 REQUIRE(action.type == DataRelayer::RelayChoice::Type::Backpressured);
716 CHECK(action.timeslice.value == 1);
717 REQUIRE(header2.get() != nullptr);
718 REQUIRE(payload2.get() != nullptr);
719 // This fills the cache, and then waits.
720 messages[4] = o2::pmr::getMessage(Stack{channelAlloc, dh1, DataProcessingHeader{1, 1}});
721 messages[5] = transport->CreateMessage(1000);
722 DataRelayer::InputInfo fakeInfo3{4, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
723 relayer.relay(header2->GetData(), &messages[4], fakeInfo3, 2);
724 REQUIRE(action.type == DataRelayer::RelayChoice::Type::Backpressured);
725 CHECK(action.timeslice.value == 1);
726 REQUIRE(header2.get() != nullptr);
727 REQUIRE(payload2.get() != nullptr);
728 }
729
730 SECTION("SplitPayloadPairs")
731 {
733 InputSpec spec1{"clusters", "TPC", "CLUSTERS"};
734
735 std::vector<InputRoute> inputs = {
736 InputRoute{spec1, 0, "Fake1", 0},
737 };
738
739 std::vector<ForwardRoute> forwards;
740 std::vector<InputChannelInfo> infos{1};
741 TimesliceIndex index{1, infos};
742 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
743
745 DataRelayer relayer(policy, inputs, index, {registry}, -1);
746 relayer.setPipelineLength(4);
747
748 DataHeader dh{"CLUSTERS", "TPC", 0};
749
750 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
751 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
752 size_t timeslice = 0;
753
754 const int nSplitParts = 100;
755 std::vector<std::unique_ptr<fair::mq::Message>> splitParts;
756 splitParts.reserve(2 * nSplitParts);
757
758 for (size_t i = 0; i < nSplitParts; ++i) {
759 dh.splitPayloadIndex = i;
760 dh.splitPayloadParts = nSplitParts;
761
762 fair::mq::MessagePtr header = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{timeslice, 1}});
763 fair::mq::MessagePtr payload = transport->CreateMessage(100);
764
765 splitParts.emplace_back(std::move(header));
766 splitParts.emplace_back(std::move(payload));
767 }
768 REQUIRE(splitParts.size() == 2 * nSplitParts);
769
770 DataRelayer::InputInfo fakeInfo{0, splitParts.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
771 relayer.relay(splitParts[0]->GetData(), splitParts.data(), fakeInfo, splitParts.size());
772 std::vector<RecordAction> ready;
773 relayer.getReadyToProcess(ready);
774 REQUIRE(ready.size() == 1);
775 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
776 auto messageSet = relayer.consumeAllInputsForTimeslice(ready[0].slot);
777 // we have one input route and thus one message set containing pairs for all
778 // payloads
779 REQUIRE((messageSet | count_inputs{}) == 1);
780 REQUIRE((messageSet[0] | count_parts{}) == nSplitParts);
781 REQUIRE((messageSet[0] | get_num_payloads{0}) == 1);
782 }
783
784 SECTION("SplitPayloadSequence")
785 {
787 InputSpec spec1{"clusters", "TST", "COUNTER"};
788
789 std::vector<InputRoute> inputs = {
790 InputRoute{spec1, 0, "Fake1", 0},
791 };
792
793 std::vector<ForwardRoute> forwards;
794 std::vector<InputChannelInfo> infos{1};
795 TimesliceIndex index{1, infos};
796 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
797
799 DataRelayer relayer(policy, inputs, index, {registry}, -1);
800 relayer.setPipelineLength(4);
801
802 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
803 size_t timeslice = 0;
804
805 std::vector<size_t> sequenceSize;
806 size_t nTotalPayloads = 0;
807
808 auto createSequence = [&nTotalPayloads, &timeslice, &sequenceSize, &transport, &relayer](size_t nPayloads) -> void {
809 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
810 std::vector<std::unique_ptr<fair::mq::Message>> messages;
811 messages.reserve(nPayloads + 1);
812 DataHeader dh{"COUNTER", "TST", 0};
813
814 // one header with index set to the number of split parts indicates sequence
815 // of payloads without additional headers
816 dh.splitPayloadIndex = nPayloads;
817 dh.splitPayloadParts = nPayloads;
818 fair::mq::MessagePtr header = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{timeslice, 1}});
819 messages.emplace_back(std::move(header));
820
821 for (size_t i = 0; i < nPayloads; ++i) {
822 messages.emplace_back(transport->CreateMessage(100));
823 *(reinterpret_cast<size_t*>(messages.back()->GetData())) = nTotalPayloads;
824 ++nTotalPayloads;
825 }
826 REQUIRE(messages.size() == nPayloads + 1);
827 DataRelayer::InputInfo fakeInfo{0, messages.size(), DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
828 relayer.relay(messages[0]->GetData(), messages.data(), fakeInfo, messages.size(), nPayloads);
829 sequenceSize.emplace_back(nPayloads);
830 };
831 createSequence(100);
832 createSequence(1);
833 createSequence(42);
834
835 std::vector<RecordAction> ready;
836 relayer.getReadyToProcess(ready);
837 REQUIRE(ready.size() == 1);
838 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
839 auto messageSet = relayer.consumeAllInputsForTimeslice(ready[0].slot);
840 // we have one input route
841 REQUIRE((messageSet | count_inputs{}) == 1);
842 // one message set containing number of added sequences of messages
843 REQUIRE((messageSet[0] | count_parts{}) == sequenceSize.size());
844 size_t counter = 0;
845 for (size_t seqid = 0; seqid < sequenceSize.size(); ++seqid) {
846 REQUIRE((messageSet[0] | get_num_payloads{seqid}) == sequenceSize[seqid]);
847 for (size_t pi = 0; pi < (messageSet[0] | get_num_payloads{seqid}); ++pi) {
848 REQUIRE((messageSet[0] | get_payload{seqid, pi}));
849 auto const* data = (messageSet[0] | get_payload{seqid, pi})->GetData();
850 REQUIRE(*(reinterpret_cast<size_t const*>(data)) == counter);
851 ++counter;
852 }
853 }
854 }
855
856 SECTION("ProcessDanglingInputs")
857 {
858 InputSpec spec{"condition", "TST", "COND"};
859 std::vector<InputRoute> inputs = {
860 InputRoute{spec, 0, "from_source_to_self", 0}};
861
862 std::vector<InputChannelInfo> infos{1};
863 TimesliceIndex index{1, infos};
864 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
865
866 // Bind a fake input channel so FairMQDeviceProxy::getInputChannelIndex works
867 FairMQDeviceProxy proxy;
868 std::vector<fair::mq::Channel> channels{fair::mq::Channel("from_source_to_self")};
869 auto findChannel = [&channels](std::string const& name) -> fair::mq::Channel& {
870 for (auto& ch : channels) {
871 if (ch.GetName() == name) {
872 return ch;
873 }
874 }
875 throw std::runtime_error("Channel not found: " + name);
876 };
877 proxy.bind({}, inputs, {}, findChannel, [] { return false; });
878 ref.registerService(ServiceRegistryHelpers::handleForService<FairMQDeviceProxy>(&proxy));
879
881 DataRelayer relayer(policy, inputs, index, {registry}, -1);
882 relayer.setPipelineLength(4);
883
884 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
885 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
886
887 DataHeader dh{"COND", "TST", 0};
888 dh.splitPayloadParts = 1;
889 dh.splitPayloadIndex = 0;
890 DataProcessingHeader dph{0, 1};
891
892 ExpirationHandler handler;
893 handler.name = "test-condition";
894 handler.routeIndex = RouteIndex{0};
895 handler.lifetime = Lifetime::Condition;
896
897 // Creator: claim an empty slot and assign timeslice 0 to it
898 handler.creator = [](ServiceRegistryRef services, ChannelIndex channelIndex) -> TimesliceSlot {
899 auto& index = services.get<TimesliceIndex>();
900 for (size_t si = 0; si < index.size(); si++) {
901 TimesliceSlot slot{si};
902 if (!index.isValid(slot)) {
903 index.associate(TimesliceId{0}, slot);
904 (void)index.setOldestPossibleInput({1}, channelIndex);
905 return slot;
906 }
907 }
909 };
910
911 // Checker: always trigger expiration
912 handler.checker = LifetimeHelpers::expireAlways();
913
914 // Handler: materialise a dummy header+payload into the PartRef
915 handler.handler = [&transport, &channelAlloc, &dh, &dph](ServiceRegistryRef, PartRef& ref, data_matcher::VariableContext&) {
916 ref.header = o2::pmr::getMessage(o2::header::Stack{channelAlloc, dh, dph});
917 ref.payload = transport->CreateMessage(4);
918 };
919
920 std::vector<ExpirationHandler> handlers{handler};
921 auto activity = relayer.processDanglingInputs(handlers, {registry}, true);
922
923 REQUIRE(activity.newSlots == 1);
924 REQUIRE(activity.expiredSlots == 1);
925
926 // The materialised data should now be ready to consume
927 std::vector<RecordAction> ready;
928 relayer.getReadyToProcess(ready);
929 REQUIRE(ready.size() == 1);
930 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
931
932 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
933 REQUIRE((result | count_inputs{}) == 1);
934 REQUIRE((result[0] | count_parts{}) == 1);
935 }
936
937 SECTION("ProcessDanglingInputsSkipsWhenDataPresent")
938 {
939 // processDanglingInputs must not overwrite a slot that already has data.
940 // This is guarded by the (part.messages | get_header{0}) != nullptr check.
941 InputSpec spec{"condition", "TST", "COND"};
942 std::vector<InputRoute> inputs = {
943 InputRoute{spec, 0, "from_source_to_self", 0}};
944
945 std::vector<InputChannelInfo> infos{1};
946 TimesliceIndex index{1, infos};
947 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
948
949 FairMQDeviceProxy proxy;
950 std::vector<fair::mq::Channel> channels{fair::mq::Channel("from_source_to_self")};
951 auto findChannel = [&channels](std::string const& name) -> fair::mq::Channel& {
952 for (auto& ch : channels) {
953 if (ch.GetName() == name) {
954 return ch;
955 }
956 }
957 throw std::runtime_error("Channel not found: " + name);
958 };
959 proxy.bind({}, inputs, {}, findChannel, [] { return false; });
960 ref.registerService(ServiceRegistryHelpers::handleForService<FairMQDeviceProxy>(&proxy));
961
963 DataRelayer relayer(policy, inputs, index, {registry}, -1);
964 relayer.setPipelineLength(4);
965
966 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
967 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
968
969 DataHeader dh{"COND", "TST", 0};
970 dh.splitPayloadParts = 1;
971 dh.splitPayloadIndex = 0;
972 DataProcessingHeader dph{0, 1};
973
974 // Build an expiration handler that always tries to expire
975 ExpirationHandler handler;
976 handler.name = "test-condition";
977 handler.routeIndex = RouteIndex{0};
978 handler.lifetime = Lifetime::Condition;
979 handler.creator = [](ServiceRegistryRef services, ChannelIndex channelIndex) -> TimesliceSlot {
980 auto& index = services.get<TimesliceIndex>();
981 for (size_t si = 0; si < index.size(); si++) {
982 TimesliceSlot slot{si};
983 if (!index.isValid(slot)) {
984 index.associate(TimesliceId{0}, slot);
985 (void)index.setOldestPossibleInput({1}, channelIndex);
986 return slot;
987 }
988 }
990 };
992 int handlerCallCount = 0;
993 handler.handler = [&transport, &channelAlloc, &dh, &dph, &handlerCallCount](ServiceRegistryRef, PartRef& ref, data_matcher::VariableContext&) {
994 ref.header = o2::pmr::getMessage(o2::header::Stack{channelAlloc, dh, dph});
995 ref.payload = transport->CreateMessage(4);
996 handlerCallCount++;
997 };
998 std::vector<ExpirationHandler> handlers{handler};
999
1000 // First call: slot is empty, so the handler fires and materialises data
1001 auto activity1 = relayer.processDanglingInputs(handlers, {registry}, true);
1002 REQUIRE(activity1.expiredSlots == 1);
1003 REQUIRE(handlerCallCount == 1);
1004
1005 // Second call: slot already has data — the handler must NOT fire again
1006 auto activity2 = relayer.processDanglingInputs(handlers, {registry}, false);
1007 REQUIRE(activity2.expiredSlots == 0);
1008 REQUIRE(handlerCallCount == 1); // handler was not called a second time
1009 }
1010
1011 // Once the DataRelayer keeps a slot's messages in one shared buffer, every
1012 // input's parts live next to each other, so a slip in the offset bookkeeping
1013 // corrupts a *different* input's cell while leaving all the part counts
1014 // intact. Counting parts therefore cannot catch it: stamp each payload and
1015 // check identity. The arrival order below is interleaved on purpose -- after
1016 // step 2 input 0 is no longer the last cell, so step 3 has to relocate it,
1017 // and likewise input 1 at step 5.
1018 SECTION("InterleavedPartsKeepIdentity")
1019 {
1020 InputSpec spec0{"clusters", "TPC", "CLUSTERS"};
1021 InputSpec spec1{"its", "ITS", "CLUSTERS"};
1022 InputSpec spec2{"tracks", "TPC", "TRACKS"};
1023
1024 std::vector<InputRoute> inputs = {
1025 InputRoute{spec0, 0, "Fake0", 0},
1026 InputRoute{spec1, 1, "Fake1", 0},
1027 InputRoute{spec2, 2, "Fake2", 0},
1028 };
1029
1030 std::vector<InputChannelInfo> infos{1};
1031 TimesliceIndex index{1, infos};
1032 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
1033
1035 DataRelayer relayer(policy, inputs, index, {registry}, -1);
1036 relayer.setPipelineLength(1);
1037
1038 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
1039 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
1040
1041 std::array<DataHeader, 3> prototypes;
1042 prototypes[0].dataOrigin = "TPC";
1043 prototypes[0].dataDescription = "CLUSTERS";
1044 prototypes[1].dataOrigin = "ITS";
1045 prototypes[1].dataDescription = "CLUSTERS";
1046 prototypes[2].dataOrigin = "TPC";
1047 prototypes[2].dataDescription = "TRACKS";
1048
1049 auto stampOf = [](size_t input, size_t part) -> uint32_t {
1050 return 1000u * static_cast<uint32_t>(input + 1) + static_cast<uint32_t>(part);
1051 };
1052
1053 auto relayOne = [&](size_t input, size_t part, size_t timeslice) {
1054 DataHeader dh = prototypes[input];
1055 dh.subSpecification = 0;
1056 dh.splitPayloadIndex = 0;
1057 dh.splitPayloadParts = 1;
1058 dh.payloadSize = sizeof(uint32_t);
1059
1060 std::array<fair::mq::MessagePtr, 2> msgs;
1061 msgs[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{timeslice, 1}});
1062 msgs[1] = transport->CreateMessage(sizeof(uint32_t));
1063 uint32_t const stamp = stampOf(input, part);
1064 memcpy(msgs[1]->GetData(), &stamp, sizeof(stamp));
1065 DataRelayer::InputInfo info{0, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
1066 relayer.relay(msgs[0]->GetData(), msgs.data(), info, 2);
1067 REQUIRE(msgs[0].get() == nullptr);
1068 REQUIRE(msgs[1].get() == nullptr);
1069 };
1070
1071 std::array<std::pair<size_t, size_t>, 5> const arrivals = {{{0, 0}, {1, 0}, {0, 1}, {2, 0}, {1, 1}}};
1072 for (auto const& [input, part] : arrivals) {
1073 relayOne(input, part, 0);
1074 }
1075
1076 std::vector<RecordAction> ready;
1077 relayer.getReadyToProcess(ready);
1078 REQUIRE(ready.size() == 1);
1079 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
1080
1081 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
1082 REQUIRE((result | count_inputs{}) == 3);
1083
1084 std::array<size_t, 3> const expectedParts = {2, 2, 1};
1085 auto checkContents = [&]() {
1086 for (size_t i = 0; i < 3; ++i) {
1087 REQUIRE((result[i] | count_parts{}) == expectedParts[i]);
1088 for (size_t p = 0; p < expectedParts[i]; ++p) {
1089 auto& header = result[i] | get_header{p};
1090 auto& payload = result[i] | get_payload{p, 0};
1091 REQUIRE(header.get() != nullptr);
1092 REQUIRE(payload.get() != nullptr);
1093 uint32_t seen = 0;
1094 memcpy(&seen, payload->GetData(), sizeof(seen));
1095 REQUIRE(seen == stampOf(i, p));
1096 }
1097 }
1098 };
1099 checkContents();
1100
1101 // The consumed messages belong to the caller now. Refilling the very same
1102 // slot must not disturb them, whether the relayer handed over vectors or an
1103 // arena it has since reused.
1104 relayOne(0, 0, 1);
1105 checkContents();
1106 }
1107
1108 // An expiring input is materialised straight into the slot, so with one
1109 // shared buffer per slot it lands *after* whatever the other inputs already
1110 // hold -- the cells are then no longer in input order. Check that the data
1111 // which was already there survives the expiry untouched.
1112 SECTION("ExpiryDoesNotDisturbNeighbours")
1113 {
1114 InputSpec dataSpec0{"clusters", "TPC", "CLUSTERS"};
1115 InputSpec condSpec{"condition", "TST", "COND"};
1116 InputSpec dataSpec2{"tracks", "TPC", "TRACKS"};
1117
1118 std::vector<InputRoute> inputs = {
1119 InputRoute{dataSpec0, 0, "from_source_to_self", 0},
1120 InputRoute{condSpec, 1, "from_source_to_self", 0},
1121 InputRoute{dataSpec2, 2, "from_source_to_self", 0},
1122 };
1123
1124 std::vector<InputChannelInfo> infos{1};
1125 TimesliceIndex index{1, infos};
1126 ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
1127
1128 FairMQDeviceProxy proxy;
1129 std::vector<fair::mq::Channel> channels{fair::mq::Channel("from_source_to_self")};
1130 auto findChannel = [&channels](std::string const& name) -> fair::mq::Channel& {
1131 for (auto& ch : channels) {
1132 if (ch.GetName() == name) {
1133 return ch;
1134 }
1135 }
1136 throw std::runtime_error("Channel not found: " + name);
1137 };
1138 proxy.bind({}, inputs, {}, findChannel, [] { return false; });
1139 ref.registerService(ServiceRegistryHelpers::handleForService<FairMQDeviceProxy>(&proxy));
1140
1142 DataRelayer relayer(policy, inputs, index, {registry}, -1);
1143 relayer.setPipelineLength(1);
1144
1145 auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
1146 auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
1147
1148 auto stampOf = [](size_t input) -> uint32_t { return 7000u + static_cast<uint32_t>(input); };
1149
1150 auto relayData = [&](size_t input, char const* origin, char const* description) {
1151 DataHeader dh;
1154 dh.subSpecification = 0;
1155 dh.splitPayloadIndex = 0;
1156 dh.splitPayloadParts = 1;
1157 dh.payloadSize = sizeof(uint32_t);
1158 std::array<fair::mq::MessagePtr, 2> msgs;
1159 msgs[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{0, 1}});
1160 msgs[1] = transport->CreateMessage(sizeof(uint32_t));
1161 uint32_t const stamp = stampOf(input);
1162 memcpy(msgs[1]->GetData(), &stamp, sizeof(stamp));
1163 DataRelayer::InputInfo info{0, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
1164 relayer.relay(msgs[0]->GetData(), msgs.data(), info, 2);
1165 REQUIRE(msgs[0].get() == nullptr);
1166 };
1167
1168 // The two data inputs arrive first, so the slot is already occupied when
1169 // the condition expires into it.
1170 relayData(0, "TPC", "CLUSTERS");
1171 relayData(2, "TPC", "TRACKS");
1172
1173 DataHeader condDh{"COND", "TST", 0};
1174 condDh.splitPayloadParts = 1;
1175 condDh.splitPayloadIndex = 0;
1176 DataProcessingHeader condDph{0, 1};
1177
1178 ExpirationHandler handler;
1179 handler.name = "test-condition";
1180 handler.routeIndex = RouteIndex{1};
1181 handler.lifetime = Lifetime::Condition;
1182 // Deliberately *not* a fresh slot: return the one the data is already in,
1183 // which is what puts the materialised cell out of input order.
1185 return TimesliceSlot{0};
1186 };
1188 handler.handler = [&transport, &channelAlloc, &condDh, &condDph](ServiceRegistryRef, PartRef& part, data_matcher::VariableContext&) {
1189 part.header = o2::pmr::getMessage(o2::header::Stack{channelAlloc, condDh, condDph});
1190 part.payload = transport->CreateMessage(4);
1191 };
1192
1193 std::vector<ExpirationHandler> handlers{handler};
1194 auto activity = relayer.processDanglingInputs(handlers, {registry}, true);
1195 REQUIRE(activity.expiredSlots == 1);
1196
1197 std::vector<RecordAction> ready;
1198 relayer.getReadyToProcess(ready);
1199 REQUIRE(ready.size() == 1);
1200 REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume);
1201
1202 auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
1203 REQUIRE((result | count_inputs{}) == 3);
1204 REQUIRE((result[1] | count_parts{}) == 1);
1205 for (size_t i : {0u, 2u}) {
1206 REQUIRE((result[i] | count_parts{}) == 1);
1207 auto& payload = result[i] | get_payload{0, 0};
1208 REQUIRE(payload.get() != nullptr);
1209 uint32_t seen = 0;
1210 memcpy(&seen, payload->GetData(), sizeof(seen));
1211 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) {
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
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 }
1297 }
1298 }
1299}
header::DataOrigin origin
header::DataDescription description
benchmark::State & state
int16_t time
Definition RawEventData.h:4
int32_t i
uint32_t op
uint32_t res
Definition RawData.h:0
o2::monitoring::Monitoring Monitoring
Class for time synchronization of RawReader instances.
void setPipelineLength(size_t s)
Tune the maximum number of in flight timeslices this can handle.
void bind(std::vector< OutputRoute > const &outputs, std::vector< InputRoute > const &inputs, std::vector< ForwardRoute > const &forwards, std::function< fair::mq::Channel &(std::string const &)> bindChannelByName, std::function< bool(void)> newStateRequestedCallback)
#define CHECK
GLint GLsizei count
Definition glcorearb.h:399
GLuint64EXT * result
Definition glcorearb.h:5662
GLsizeiptr size
Definition glcorearb.h:659
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLsizei GLenum const void GLuint GLsizei GLfloat * metrics
Definition glcorearb.h:5500
GLboolean * data
Definition glcorearb.h:298
typedef void(APIENTRYP PFNGLCULLFACEPROC)(GLenum mode)
GLint ref
Definition glcorearb.h:291
GLuint * states
Definition glcorearb.h:4932
GLuint counter
Definition glcorearb.h:3987
Defining ITS Vertex explicitly as messageable.
Definition Cartesian.h:288
std::vector< InputSpec > select(char const *matcher="")
auto get(const std::byte *buffer, size_t=0)
Definition DataHeader.h:454
fair::mq::MessagePtr getMessage(ContainerT &&container, FairMQMemoryResource *targetResource=nullptr)
static constexpr int INVALID
static CompletionPolicy processWhenAny(const char *name, CompletionPolicy::Matcher matcher)
static CompletionPolicy consumeWhenAll(const char *name, CompletionPolicy::Matcher matcher)
Default Completion policy. When all the parts of a record have arrived, consume them.
static CompletionPolicy consumeWhenAny(const char *name, CompletionPolicy::Matcher matcher)
When any of the parts of the record have been received, consume them.
Helper struct to hold statistics about the data processing happening.
Running state information of a given device.
Definition DeviceState.h:34
bool batch
Whether the driver was started in batch mode or not.
static ExpirationHandler::Checker expireAlways()
Reference to an inflight part.
Definition PartRef.h:24
static constexpr uint64_t INVALID
static std::function< int64_t(int64_t base, int64_t offset)> defaultCPUTimeConfigurator(uv_loop_t *loop)
static std::function< void(int64_t &base, int64_t &offset)> defaultRealtimeBaseConfigurator(uint64_t offset, uv_loop_t *loop)
the main header struct
Definition DataHeader.h:620
SplitPayloadPartsType splitPayloadParts
Definition DataHeader.h:648
DataDescription dataDescription
Definition DataHeader.h:638
SubSpecificationType subSpecification
Definition DataHeader.h:658
PayloadSizeType payloadSize
Definition DataHeader.h:668
SplitPayloadIndexType splitPayloadIndex
Definition DataHeader.h:663
void runtimeInit(const char *string, short length=-1)
Definition DataHeader.h:261
a move-only header stack with serialized headers This is the flat buffer where all the headers in a m...
Definition Stack.h:33
TEST_CASE("DataRelayer")
std::vector< ChannelData > channels