LCOV - code coverage report
Current view: top level - daqconf/apps - GraphBuilder.cpp (source / functions) Coverage Total Hit
Test: code.result Lines: 0.0 % 437 0
Test Date: 2026-03-29 15:29:34 Functions: 0.0 % 25 0

            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>&#10230; Network Connection</i></b></font>>, shape=plaintext];",
     665              :     "legendGB [label=<<font color=\"blue\"><b><i>&#10230; 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>&#10230; Data Move Callback</i></b></font>>, shape=plaintext];",
     669              :     "legendGD [label=<<font color=\"red\"><b><i>&#10230; Queue</i></b></font>>, shape=plaintext];",
     670              :     "legendGE [label=<<font color=\"orange\"><b><i>&#10230; 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
        

Generated by: LCOV version 2.0-1