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