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