DUNE-DAQ
DUNE Trigger and Data Acquisition software
Loading...
Searching...
No Matches
GraphBuilder.cpp
Go to the documentation of this file.
1/************************************************************
2 *
3 * GraphBuilder.cpp
4 *
5 * JCF, Sep-11-2024
6 *
7 * Implementation of GraphBuilder::construct_graph and GraphBuilder::write_graph
8 *
9 * This is part of the DUNE DAQ Application Framework, copyright 2020.
10 * Licensing/copyright details are in the COPYING file that you should have
11 * received with this code.
12 *
13 *************************************************************/
14
15#include "GraphBuilder.hpp"
16
25
29
31#include "conffwk/Schema.hpp"
35#include "confmodel/Session.hpp"
36#include "ers/ers.hpp"
37
38#include "boost/graph/graphviz.hpp"
39
40#include <algorithm>
41#include <cassert>
42#include <fstream>
43#include <iostream>
44#include <map>
45#include <ranges>
46#include <regex>
47#include <sstream>
48#include <string>
49#include <unordered_map>
50#include <vector>
51
52namespace daqconf {
53
54GraphBuilder::GraphBuilder(const std::string& oksfilename, const std::string& sessionname)
55 : m_oksfilename{ oksfilename }
56 , m_confdb{ nullptr }
57 , m_included_classes{ { ObjectKind::kSession, { "Session", "Segment", "Application" } },
58 { ObjectKind::kSegment, { "Segment", "Application" } },
59 { ObjectKind::kApplication, { "Application", "Module" } },
60 { ObjectKind::kModule, { "Module" } } }
61 , m_root_object_kind{ ObjectKind::kUndefined }
62 , m_session{ nullptr }
63 , m_session_name{ sessionname }
64{
65
66 // Open the database represented by the OKS XML file
67
68 try {
69 m_confdb = new dunedaq::conffwk::Configuration("oksconflibs:" + m_oksfilename);
70 } catch (dunedaq::conffwk::Generic& exc) {
71 TLOG() << "Failed to load OKS database: " << exc << "\n";
72 throw exc;
73 }
74
75 // Get the session in the database
76 std::vector<ConfigObject> session_objects{};
77
78 m_confdb->get("Session", session_objects);
79
80 if (m_session_name == "") { // If no session name given, use the one-and-only session expected in the database
81 if (session_objects.size() == 1) {
82 m_session_name = session_objects[0].UID();
83 } else {
84 std::stringstream errmsg;
85 errmsg << "No Session instance name was provided, and since " << session_objects.size()
86 << " session instances were found in \"" << m_oksfilename << "\" this is an error";
87
88 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
89 }
90 } else { // session name provided by the user, let's make sure it's there
91 auto it =
92 std::ranges::find_if(session_objects, [&](const ConfigObject& obj) { return obj.UID() == m_session_name; });
93
94 if (it == session_objects.end()) {
95 std::stringstream errmsg;
96 errmsg << "Did not find Session instance \"" << m_session_name << "\" in \"" << m_oksfilename
97 << "\" and its includes";
98 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
99 }
100 }
101
102 // The following not-brief section of code is dedicated to
103 // determining which applications in the configuration are
104 // disabled
105
106 // First, we need the session object to check if an application
107 // has been disabled
108
109 // Note the "const_cast" is needed since "m_confdb->get"
110 // returns a const pointer, but since m_session is a member needed
111 // by multiple functions and can't be determined until after we've
112 // opened the database and found the session, we need to change
113 // its initial value here. Once this is done, it shouldn't be
114 // changed again.
115
116 m_session = const_cast<dunedaq::confmodel::Session*>( // NOLINT
117 m_confdb->get<dunedaq::confmodel::Session>(m_session_name));
118
119 if (!m_session) {
120 std::stringstream errmsg;
121 errmsg << "Unable to get session with UID \"" << m_session_name << "\"";
122 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
123 }
124
125 std::vector<ConfigObject> every_object_deriving_from_class{}; // Includes objects of the class itself
126 std::vector<ConfigObject> objects_of_class{}; // A subset of every_object_deriving_from_class
127
128 // m_confdb->superclasses() returns a conffwk::fmap; see the conffwk package for details
129
130 auto classnames = m_confdb->superclasses() | std::views::keys |
131 std::views::transform([](const auto& ptr_to_class_name) { return *ptr_to_class_name; });
132
133 for (const auto& classname : classnames) {
134
135 every_object_deriving_from_class.clear();
136 objects_of_class.clear();
137
138 m_confdb->get(classname, every_object_deriving_from_class);
139
140 std::ranges::copy_if(every_object_deriving_from_class,
141 std::back_inserter(objects_of_class),
142 [&classname](const ConfigObject& obj) { return obj.class_name() == classname; });
143
144 std::ranges::copy(objects_of_class, std::back_inserter(m_all_objects));
145
146 if (classname.find("Application") != std::string::npos) { // DFApplication, ReadoutApplication, etc.
147 for (const auto& appobj : objects_of_class) {
148
149 auto daqapp = m_confdb->get<dunedaq::appmodel::SmartDaqApplication>(appobj.UID());
150
151 if (daqapp) {
152
153 auto res = daqapp->cast<dunedaq::confmodel::Resource>();
154
155 if (res && res->is_disabled(*m_session)) {
156 m_ignored_application_uids.push_back(appobj.UID());
157 TLOG() << "Skipping disabled application " << appobj.UID() << "@" << daqapp->class_name();
158 continue;
159 }
160 } else {
161 TLOG(TLVL_DEBUG) << "Skipping non-SmartDaqApplication " << appobj.UID() << "@" << appobj.class_name();
162 m_ignored_application_uids.push_back(appobj.UID());
163 }
164 }
165 }
166 }
167}
168
169void
171{
172
173 m_candidate_objects.clear();
174
175 for (const auto& obj : m_all_objects) {
176 for (const auto& classname : this->m_included_classes.at(m_root_object_kind)) {
177
178 if (obj.class_name().find(classname) != std::string::npos &&
179 std::ranges::find(m_ignored_application_uids, obj.UID()) == m_ignored_application_uids.end()) {
180 m_candidate_objects.emplace_back(obj);
181 }
182 }
183 }
184}
185
186void
187GraphBuilder::calculate_graph(const std::string& root_obj_uid)
188{
189
190 // To start, get the session / segments / applications in the
191 // session by setting up a temporary graph with the session as its
192 // root. This way we can check to see if the actual requested root
193 // object lies within the session in question.
194
195 auto true_root_object_kind = m_root_object_kind;
198
199 auto it_session =
200 std::ranges::find_if(m_all_objects, [&](const ConfigObject& obj) { return obj.UID() == m_session_name; });
201
202 find_objects_and_connections(*it_session);
203
204 if (!m_objects_for_graph.contains(root_obj_uid)) {
205 std::stringstream errmsg;
206 errmsg << "Unable to find requested object \"" << root_obj_uid << "\" in session \"" << m_session_name << "\"";
207 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
208 }
209
210 // Since we used our first call to find_objects_and_connections
211 // only as a fact-finding mission, reset the containers it filled
212
213 m_objects_for_graph.clear();
216
217 m_candidate_objects.clear();
218
219 m_root_object_kind = true_root_object_kind;
221
222 bool found = false;
223 for (auto& obj : m_candidate_objects) {
224 if (obj.UID() == root_obj_uid) {
225 found = true;
226 find_objects_and_connections(obj); // Put differently, "find what will make up the vertices and edges"
227 break;
228 }
229 }
230
231 assert(found);
232
233 calculate_network_connections(); // Put differently, "find the edges between the vertices"
234}
235
236void
238{
239
240 // Will use "incoming_matched" and "outgoing_matched" to keep
241 // track of incoming and outgoing connections which don't get
242 // matched, i.e. would terminate external to the graph
243
244 std::vector<std::string> incoming_matched;
245 std::vector<std::string> outgoing_matched;
246
247 for (auto& incoming : m_incoming_connections) {
248
249 std::regex incoming_pattern(incoming.first);
250
251 for (auto& outgoing : m_outgoing_connections) {
252
253 std::regex outgoing_pattern(outgoing.first);
254
255 bool match = false;
256
257 if (incoming.first == outgoing.first) {
258 match = true;
259 } else if (incoming.first.find(".*") != std::string::npos) {
260 if (std::regex_match(outgoing.first, incoming_pattern)) {
261 match = true;
262 }
263 } else if (outgoing.first.find(".*") != std::string::npos) {
264 if (std::regex_match(incoming.first, outgoing_pattern)) {
265 match = true;
266 }
267 }
268
269 if (match) {
270
271 bool low_level_plot =
273
274 for (auto& receiver : incoming.second) {
275 for (auto& sender : outgoing.second) {
276
277 // We just want to plot applications sending to other
278 // applications and queues sending to other
279 // queues. Showing, e.g., a queue directly sending to
280 // some other application via a network connection makes
281 // the plot too busy.
282
283 if (!m_objects_for_graph.contains(sender) || !m_objects_for_graph.contains(receiver)) {
284 continue;
285 } else if (m_objects_for_graph.at(sender).kind != m_objects_for_graph.at(receiver).kind) {
286 continue;
287 } else if (low_level_plot && incoming.first.find("NetworkConnection") != std::string::npos) {
288
289 // Don't want to directly link modules in an
290 // application if the data is transferred over network
291 continue;
292 }
293
294 if (!low_level_plot || incoming.first.find(".*") == std::string::npos) {
295 if (std::ranges::find(incoming_matched, incoming.first) == incoming_matched.end()) {
296 incoming_matched.push_back(incoming.first);
297 }
298 }
299
300 if (!low_level_plot || outgoing.first.find(".*") == std::string::npos) {
301 if (std::ranges::find(outgoing_matched, outgoing.first) == outgoing_matched.end()) {
302 outgoing_matched.push_back(outgoing.first);
303 }
304 }
305
306 const EnhancedObject::ReceivingInfo receiving_info{ incoming.first, receiver };
307
308 auto res = std::ranges::find(m_objects_for_graph.at(sender).receiving_object_infos, receiving_info);
309 if (res == m_objects_for_graph.at(sender).receiving_object_infos.end()) {
310 m_objects_for_graph.at(sender).receiving_object_infos.push_back(receiving_info);
311 }
312 }
313 }
314 }
315 }
316 }
317
318 auto incoming_unmatched =
319 m_incoming_connections | std::views::keys | std::views::filter([&incoming_matched](auto& connection) {
320 return std::ranges::find(incoming_matched, connection) == incoming_matched.end();
321 });
322
323 auto included_classes = m_included_classes.at(m_root_object_kind);
324
325 for (auto& incoming_conn : incoming_unmatched) {
326
328 const std::string incoming_vertex_name = incoming_conn;
329
330 // Find the connections appropriate to the granularity level of this graph
331 for (auto& receiving_object_name : m_incoming_connections[incoming_conn]) {
332
333 if (!m_objects_for_graph.contains(receiving_object_name)) {
334 continue;
335 }
336
337 if (std::ranges::find(included_classes, "Module") != included_classes.end()) {
338 if (m_objects_for_graph.at(receiving_object_name).kind == ObjectKind::kModule) {
339 external_obj.receiving_object_infos.push_back({ incoming_conn, receiving_object_name });
340 }
341 } else if (std::ranges::find(included_classes, "Application") != included_classes.end()) {
342 if (m_objects_for_graph.at(receiving_object_name).kind == ObjectKind::kApplication) {
343 external_obj.receiving_object_infos.push_back({ incoming_conn, receiving_object_name });
344 }
345 }
346 }
347
348 m_objects_for_graph.insert({ incoming_vertex_name, external_obj });
349 }
350
351 auto outgoing_unmatched =
352 m_outgoing_connections | std::views::keys | std::views::filter([&outgoing_matched](auto& connection) {
353 return std::ranges::find(outgoing_matched, connection) == outgoing_matched.end();
354 });
355
356 for (auto& outgoing_conn : outgoing_unmatched) {
357
359 const std::string outgoing_vertex_name = outgoing_conn;
360
361 // Find the connections appropriate to the granularity level of this graph
362 for (auto& sending_object_name : m_outgoing_connections[outgoing_conn]) {
363
364 if (!m_objects_for_graph.contains(sending_object_name)) {
365 continue;
366 }
367
368 if (std::ranges::find(included_classes, "Module") != included_classes.end()) {
369 if (m_objects_for_graph.at(sending_object_name).kind == ObjectKind::kModule) {
370 m_objects_for_graph.at(sending_object_name)
371 .receiving_object_infos.push_back({ outgoing_conn, outgoing_vertex_name });
372 }
373 } else if (std::ranges::find(included_classes, "Application") != included_classes.end()) {
374 if (m_objects_for_graph.at(sending_object_name).kind == ObjectKind::kApplication) {
375 m_objects_for_graph.at(sending_object_name)
376 .receiving_object_infos.push_back({ outgoing_conn, outgoing_vertex_name });
377 }
378 }
379 }
380
381 m_objects_for_graph.insert({ outgoing_vertex_name, external_obj });
382 }
383}
384
385void
387{
388
389 EnhancedObject starting_object{ object, get_object_kind(object.class_name()) };
390
391 // If we've got a session or a segment, look at its OKS-relations,
392 // and recursively process those relation objects which are on the
393 // candidates list and haven't already been processed
394
395 if (starting_object.kind == ObjectKind::kSession || starting_object.kind == ObjectKind::kSegment) {
396
397 for (auto& child_object : find_child_objects(starting_object.config_object)) {
398
399 if (std::ranges::find(m_candidate_objects, child_object) != m_candidate_objects.end()) {
400 find_objects_and_connections(child_object);
401 starting_object.child_object_names.push_back(child_object.UID());
402 }
403 }
404 } else if (starting_object.kind == ObjectKind::kApplication) {
405
406 // If we've got an application object, try to determine what
407 // modules are in it and what their connections are. Recursively
408 // process the modules, and then add connection info to class-wide
409 // member maps to calculate edges corresponding to the connections
410 // for the plotted graph later
411
412 dunedaq::conffwk::Configuration* local_database{ nullptr };
413
414 try {
415 local_database = new dunedaq::conffwk::Configuration("oksconflibs:" + m_oksfilename);
416 } catch (dunedaq::conffwk::Generic& exc) {
417 TLOG() << "Failed to load OKS database: " << exc << "\n";
418 throw exc;
419 }
420
421 auto daqapp = local_database->get<dunedaq::appmodel::SmartDaqApplication>(object.UID());
422 if (daqapp) {
423 auto local_session = const_cast<dunedaq::confmodel::Session*>( // NOLINT
425
426 auto helper = std::make_shared<dunedaq::appmodel::ConfigurationHelper>(local_session);
427 daqapp->generate_modules(helper);
428 auto modules = daqapp->get_modules();
429
430 std::vector<std::string> allowed_conns{};
431
433 allowed_conns = { "NetworkConnection" };
435 allowed_conns = { "NetworkConnection", "Queue", "QueueWithSourceId", "DataMoveCallbackConf" };
436 }
437
438 for (const auto& module : modules) {
439
440 for (auto in : module->get_inputs()) {
441
442 // Elsewhere in the code it'll be useful to know if the
443 // connection is a network or a queue, so include the
444 // class name in the std::string key
445
446 std::string key = in->config_object().UID() + "@" + in->config_object().class_name();
447
448 if (in->config_object().class_name() == "NetworkConnection") {
449 auto innc = in->cast<dunedaq::confmodel::NetworkConnection>();
450 key += "@" + innc->get_connection_type();
451 }
452
453 if (std::ranges::find(allowed_conns, in->config_object().class_name()) != allowed_conns.end()) {
454 m_incoming_connections[key].push_back(object.UID());
455 m_incoming_connections[key].push_back(module->UID());
456 }
457 }
458
459 for (auto out : module->get_outputs()) {
460
461 std::string key = out->config_object().UID() + "@" + out->config_object().class_name();
462
463 if (out->config_object().class_name() == "NetworkConnection") {
464 auto outnc = out->cast<dunedaq::confmodel::NetworkConnection>();
465 key += "@" + outnc->get_connection_type();
466 }
467
468 if (std::ranges::find(allowed_conns, out->config_object().class_name()) != allowed_conns.end()) {
469 m_outgoing_connections[key].push_back(object.UID());
470 m_outgoing_connections[key].push_back(module->UID());
471 }
472 }
473
474 // Look for DataMoveCallbackConfs
475 auto datareader = module->cast<dunedaq::appmodel::DataReaderModule>();
476 auto datahandler = module->cast<dunedaq::appmodel::DataHandlerModule>();
477
478 if (datareader != nullptr) {
479 for (auto& out : datareader->get_raw_data_callbacks()) {
480 const std::string key = out->config_object().UID() + "@" + out->config_object().class_name();
481 if (std::ranges::find(allowed_conns, out->config_object().class_name()) != allowed_conns.end()) {
482 m_outgoing_connections[key].push_back(object.UID());
483 m_outgoing_connections[key].push_back(module->UID());
484 }
485 }
486 }
487 if (datahandler != nullptr) {
488 auto in = datahandler->get_raw_data_callback();
489 if (in != nullptr) {
490 const std::string key = in->config_object().UID() + "@" + in->config_object().class_name();
491
492 if (std::ranges::find(allowed_conns, in->config_object().class_name()) != allowed_conns.end()) {
493 m_incoming_connections[key].push_back(object.UID());
494 m_incoming_connections[key].push_back(module->UID());
495 }
496 }
497 }
498
499 if (std::ranges::find(m_included_classes.at(m_root_object_kind), "Module") !=
501 find_objects_and_connections(module->config_object());
502 starting_object.child_object_names.push_back(module->UID());
503 }
504 }
505 }
506 }
507
508 assert(!m_objects_for_graph.contains(object.UID()));
509
510 m_objects_for_graph.insert({ object.UID(), starting_object });
511}
512
513void
514GraphBuilder::construct_graph(std::string root_obj_uid)
515{
516
517 if (root_obj_uid == "") {
518 root_obj_uid = m_session_name;
519 }
520
521 // Next several lines just mean "tell me the class type of the root object in the config plot's graph"
522
523 auto class_names_view =
524 m_all_objects | std::views::filter([root_obj_uid](const ConfigObject& obj) { return obj.UID() == root_obj_uid; }) |
525 std::views::transform([](const ConfigObject& obj) { return obj.class_name(); });
526
527 if (std::ranges::distance(class_names_view) != 1) {
528 std::stringstream errmsg;
529 errmsg << "Failed to find instance of desired root object \"" << root_obj_uid << "\"";
530 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
531 }
532
533 const std::string& root_obj_class_name = *class_names_view.begin();
534
535 m_root_object_kind = get_object_kind(root_obj_class_name);
536
537 calculate_graph(root_obj_uid);
538
539 for (auto& enhanced_object : m_objects_for_graph | std::views::values) {
540
541 if (enhanced_object.kind == ObjectKind::kIncomingExternal) {
542 enhanced_object.vertex_in_graph = boost::add_vertex(VertexLabel("O", ""), m_graph);
543 } else if (enhanced_object.kind == ObjectKind::kOutgoingExternal) {
544 enhanced_object.vertex_in_graph = boost::add_vertex(VertexLabel("X", ""), m_graph);
545 } else {
546 auto& obj = enhanced_object.config_object;
547 enhanced_object.vertex_in_graph = boost::add_vertex(VertexLabel(obj.UID(), obj.class_name()), m_graph);
548 }
549 }
550
551 for (auto& parent_obj : m_objects_for_graph | std::views::values) {
552 for (auto& child_obj_name : parent_obj.child_object_names) {
553 boost::add_edge(parent_obj.vertex_in_graph,
554 m_objects_for_graph.at(child_obj_name).vertex_in_graph,
555 { "" }, // No label for an edge which just describes "ownership" rather than dataflow
556 m_graph);
557 }
558 }
559
560 for (auto& possible_sender_object : m_objects_for_graph | std::views::values) {
561 for (auto& receiver_info : possible_sender_object.receiving_object_infos) {
562
563 // If we're plotting at the level of a session or segment,
564 // show the connections as between applications; if we're
565 // doing this for a single application, show them entering and
566 // exiting the individual modules
567
569 if (m_objects_for_graph.at(receiver_info.receiver_label).kind == ObjectKind::kModule) {
570 continue;
571 }
572 }
573
575 if (m_objects_for_graph.at(receiver_info.receiver_label).kind == ObjectKind::kApplication) {
576 continue;
577 }
578 }
579
580 boost::add_edge(possible_sender_object.vertex_in_graph,
581 m_objects_for_graph.at(receiver_info.receiver_label).vertex_in_graph,
582 { receiver_info.connection_name },
583 m_graph)
584 .first;
585 }
586 }
587}
588
589std::vector<dunedaq::conffwk::ConfigObject>
591{
592
593 std::vector<ConfigObject> connected_objects{};
594
595 dunedaq::conffwk::class_t classdef = m_confdb->get_class_info(parent_obj.class_name(), false);
596
597 for (const dunedaq::conffwk::relationship_t& relationship : classdef.p_relationships) {
598
599 // The ConfigObject::get(...) function doesn't have a
600 // const-qualifier on it for no apparent good reason; we need
601 // this cast in order to call it
602
603 auto parent_obj_casted = const_cast<ConfigObject&>(parent_obj); // NOLINT
604
605 if (relationship.p_cardinality == dunedaq::conffwk::only_one ||
607 ConfigObject connected_obj{};
608 parent_obj_casted.get(relationship.p_name, connected_obj);
609 connected_objects.push_back(connected_obj);
610 } else {
611 std::vector<ConfigObject> connected_objects_in_relationship{};
612 parent_obj_casted.get(relationship.p_name, connected_objects_in_relationship);
613 connected_objects.insert(
614 connected_objects.end(), connected_objects_in_relationship.begin(), connected_objects_in_relationship.end());
615 }
616 }
617
618 return connected_objects;
619}
620
621void
622GraphBuilder::write_graph(const std::string& outputfilename) const
623{
624
625 std::stringstream outputstream;
626
627 boost::write_graphviz(outputstream,
628 m_graph,
629 boost::make_label_writer(boost::get(&GraphBuilder::VertexLabel::displaylabel, m_graph)),
630 boost::make_label_writer(boost::get(&GraphBuilder::EdgeLabel::displaylabel, m_graph)));
631
632 // It's arguably hacky to edit the DOT code generated by
633 // boost::write_graphviz after the fact to give vertices colors,
634 // but the fact is that the color-assigning logic in Boost's graph
635 // library is so messy and clumsy that this is a worthwhile
636 // tradeoff
637
638 struct VertexStyle
639 {
640 const std::string shape;
641 const std::string color;
642 };
643
644 const std::unordered_map<ObjectKind, VertexStyle> vertex_styles{ { ObjectKind::kSession, { "octagon", "black" } },
645 { ObjectKind::kSegment, { "hexagon", "brown" } },
646 { ObjectKind::kApplication, { "pentagon", "blue" } },
647 { ObjectKind::kModule, { "rectangle", "red" } } };
648
649 std::string dotfile_slurped = outputstream.str();
650 std::vector<std::string> legend_entries{
651 "legendGA [label=<<font color=\"black\"><b><i>&#10230; Network Connection</i></b></font>>, shape=plaintext];",
652 "legendGB [label=<<font color=\"blue\"><b><i>&#10230; Pub/Sub Network</i></b></font>>, shape=plaintext];"
653 };
654 std::vector<std::string> internal_legend_entries{
655 "legendGC [label=<<font color=\"green\"><b><i>&#10230; Data Move Callback</i></b></font>>, shape=plaintext];",
656 "legendGD [label=<<font color=\"red\"><b><i>&#10230; Queue</i></b></font>>, shape=plaintext];",
657 "legendGE [label=<<font color=\"orange\"><b><i>&#10230; Queue w/ Source ID</i></b></font>>, shape=plaintext];"
658 };
659 bool internal_legend_added = false;
660 std::vector<std::string> legend_ordering_code{};
661
662 for (auto& eo : m_objects_for_graph | std::views::values) {
663
664 std::stringstream vertexstr{};
665 std::stringstream legendstr{};
666 std::stringstream labelstringstr{};
667 size_t insertion_location{ 0 };
668
669 auto calculate_insertion_location = [&]() {
670 labelstringstr << "label=\"" << eo.config_object.UID() << "\n";
671 insertion_location = dotfile_slurped.find(labelstringstr.str());
672 assert(insertion_location != std::string::npos);
673 return insertion_location;
674 };
675
676 // TODO: John Freeman (jcfree@fnal.gov), Sep-17-2024
677
678 // Switch to std::format for line construction rather than
679 // std::stringstream when we switch to a gcc version which
680 // supports it
681
682 auto add_vertex_info = [&]() {
683 vertexstr << "shape=" << vertex_styles.at(eo.kind).shape << ", color=" << vertex_styles.at(eo.kind).color
684 << ", fontcolor=" << vertex_styles.at(eo.kind).color << ", ";
685 dotfile_slurped.insert(calculate_insertion_location(), vertexstr.str());
686 };
687
688 auto add_legend_entry = [&](char letter, const std::string objkind) {
689 legendstr << "legend" << letter << " [label=<<font color=\"" << vertex_styles.at(eo.kind).color << "\"><b><i>"
690 << vertex_styles.at(eo.kind).color << ": " << objkind
691 << "</i></b></font>>, shape=plaintext, color=" << vertex_styles.at(eo.kind).color
692 << ", fontcolor=" << vertex_styles.at(eo.kind).color << "];";
693 };
694
695 // Note that the seemingly arbitrary single characters added
696 // after "legend" aren't just to uniquely identify each entry in
697 // the legend, it's also so that when the entries are sorted
698 // alphabetically they'll appear in the correct order
699
700 switch (eo.kind) {
702 add_vertex_info();
703 add_legend_entry('A', "session");
704 break;
706 add_vertex_info();
707 add_legend_entry('B', "segment");
708 break;
710 add_vertex_info();
711 add_legend_entry('C', "application");
712 break;
714 add_vertex_info();
715 add_legend_entry('D', "DAQModule");
716 if (!internal_legend_added) {
717 legend_entries.insert(legend_entries.end(),
718 internal_legend_entries.begin(),
719 internal_legend_entries.end());
720 internal_legend_added = true;
721 }
722 break;
724 legendstr
725 << "legendE [label=<<font color=\"black\">O:<b><i> External Data Source</i></b></font>>, shape=plaintext];";
726 break;
728 legendstr
729 << "legendF [label=<<font color=\"black\">X:<b><i> External Data Sink</i></b></font>>, shape=plaintext];";
730 break;
731 default:
732 assert(false);
733 }
734
735 if (std::ranges::find(legend_entries, legendstr.str()) == legend_entries.end()) {
736 legend_entries.emplace_back(legendstr.str());
737 }
738 }
739
740 std::ranges::sort(legend_entries);
741
742 // We have the line-by-line entries containing the labels in our
743 // legend and their colors, but more DOT code is needed in order
744 // to show the labels in order. E.g., if we have:
745
746 // legendA [label=<<font color="blue">Blue: Segment</font>>, shape=box, color=blue, fontcolor=blue];
747 // legendB [label=<<font color="red">Red: Application</font>>, shape=box, color=red, fontcolor=red];
748 //
749 // Then we'll also need
750 //
751 // legendA -> legendB [style=invis];
752
753 auto legend_tokens = legend_entries | std::views::transform([](const std::string& line) {
754 return line.substr(0, line.find(' ')); // i.e., grab the first word on the line
755 });
756
757 auto it = legend_tokens.begin();
758 for (auto next_it = std::next(it); next_it != legend_tokens.end(); ++it, ++next_it) {
759 std::stringstream astr{};
760 astr << " " << *it << " -> " << *next_it << " [style=invis];";
761 legend_ordering_code.push_back(astr.str());
762 }
763
764 constexpr int chars_to_last_brace = 2;
765 auto last_brace_iter = dotfile_slurped.end() - chars_to_last_brace;
766 assert(*last_brace_iter == '}');
767 size_t last_brace_loc = last_brace_iter - dotfile_slurped.begin();
768
769 std::string legend_code{};
770 legend_code += "\n\n\n";
771
772 for (const auto& l : legend_entries) {
773 legend_code += l + "\n";
774 }
775
776 legend_code += "\n\n\n";
777
778 for (const auto& l : legend_ordering_code) {
779 legend_code += l + "\n";
780 }
781
782 dotfile_slurped.insert(last_brace_loc, legend_code);
783
784 // Take advantage of the fact that the edges describing ownership
785 // rather than data flow (e.g., hsi-segment owning hsi-01, the
786 // FakeHSIApplication) have null labels in order to turn them into
787 // arrow-free dotted lines
788
789 const std::string unlabeled_edge = "label=\"\"";
790 const std::string edge_modifier = ", style=\"dotted\", arrowhead=\"none\"";
791
792 size_t pos = 0;
793 while ((pos = dotfile_slurped.find(unlabeled_edge, pos)) != std::string::npos) {
794 dotfile_slurped.replace(pos, unlabeled_edge.length(), unlabeled_edge + edge_modifier);
795 pos += (unlabeled_edge + edge_modifier).length();
796 }
797
798 // Replace the connection types with color information
799 std::vector<std::pair<std::string, std::string>> connection_colors = { { "@NetworkConnection@kSendRecv\"", "\", color=black" },
800 { "@NetworkConnection@kPubSub\"", "\", color=blue" },
801 { "@QueueWithSourceId\"", "\", color=orange" },
802 { "@Queue\"", "\", color=red" },
803 { "@DataMoveCallbackConf\"",
804 "\", color=green" } };
805 for (auto& color_pair : connection_colors) {
806 auto conn_type = color_pair.first;
807 auto color_info = color_pair.second;
808 pos = 0;
809 while ((pos = dotfile_slurped.find(conn_type, pos)) != std::string::npos) {
810 dotfile_slurped.replace(pos, conn_type.length(), color_info);
811 pos += color_info.length();
812 }
813 }
814
815 // And now with all the edits made to the contents of the DOT code, write it to file
816
817 std::ofstream outputfile;
818 outputfile.open(outputfilename);
819
820 if (outputfile.is_open()) {
821 outputfile << dotfile_slurped.c_str();
822 } else {
823 std::stringstream errmsg;
824 errmsg << "Unable to open requested file \"" << outputfilename << "\" for writing";
825 throw daqconf::GeneralGraphToolError(ERS_HERE, errmsg.str());
826 }
827}
828
830get_object_kind(const std::string& class_name)
831{
832
833 using ObjectKind = GraphBuilder::ObjectKind;
834
835 ObjectKind kind = ObjectKind::kSession;
836
837 if (class_name.find("Session") != std::string::npos) {
838 kind = ObjectKind::kSession;
839 } else if (class_name.find("Segment") != std::string::npos) {
840 kind = ObjectKind::kSegment;
841 } else if (class_name.find("Application") != std::string::npos) {
842 kind = ObjectKind::kApplication;
843 } else if (class_name.find("Module") != std::string::npos) {
844 kind = ObjectKind::kModule;
845 } else {
846 throw daqconf::GeneralGraphToolError(
847 ERS_HERE, "Unsupported class type \"" + std::string(class_name) + "\"passed to get_object_kind");
848 }
849
850 return kind;
851}
852
853} // namespace appmodel
#define ERS_HERE
void find_objects_and_connections(const ConfigObject &object)
std::unordered_map< std::string, std::vector< std::string > > m_incoming_connections
std::vector< ConfigObject > m_all_objects
std::unordered_map< std::string, std::vector< std::string > > m_outgoing_connections
void construct_graph(std::string root_obj_uid)
const std::unordered_map< ObjectKind, std::vector< std::string > > m_included_classes
std::unordered_map< std::string, EnhancedObject > m_objects_for_graph
void write_graph(const std::string &outputfilename) const
dunedaq::conffwk::Configuration * m_confdb
std::vector< ConfigObject > m_candidate_objects
std::vector< std::string > m_ignored_application_uids
void calculate_graph(const std::string &root_obj_uid)
std::vector< dunedaq::conffwk::ConfigObject > find_child_objects(const ConfigObject &parent_obj)
const std::string m_oksfilename
GraphBuilder(const std::string &oksfilename, const std::string &sessionname)
dunedaq::conffwk::ConfigObject ConfigObject
const std::string & class_name() const noexcept
Return object's class name.
Defines base class for cache of template objects.
void get(const std::string &class_name, const std::string &id, ConfigObject &object, unsigned long rlevel=0, const std::vector< std::string > *rclasses=0)
Get object by class name and object id (multi-thread safe).
conffwk entry point
#define TLOG(...)
Definition macro.hpp:22
constexpr GraphBuilder::ObjectKind get_object_kind(const std::string &class_name)
std::vector< std::string > child_object_names
std::vector< ReceivingInfo > receiving_object_infos
const std::vector< relationship_t > p_relationships
Definition Schema.hpp:171