The Gaudi Framework  master (37c0b60a)
PrecedenceSvc.cpp
Go to the documentation of this file.
1 /***********************************************************************************\
2 * (c) Copyright 1998-2019 CERN for the benefit of the LHCb and ATLAS collaborations *
3 * *
4 * This software is distributed under the terms of the Apache version 2 licence, *
5 * copied verbatim in the file "LICENSE". *
6 * *
7 * In applying this licence, CERN does not waive the privileges and immunities *
8 * granted to it by virtue of its status as an Intergovernmental Organization *
9 * or submit itself to any jurisdiction. *
10 \***********************************************************************************/
11 #include "PrecedenceSvc.h"
12 #include "EventSlot.h"
16 
17 #include <Gaudi/Algorithm.h>
18 #include <Gaudi/Sequence.h>
19 
20 // C++
21 #include <fstream>
22 
23 #define ON_DEBUG if ( msgLevel( MSG::DEBUG ) )
24 #define ON_VERBOSE if ( msgLevel( MSG::VERBOSE ) )
25 
27 
28 // ============================================================================
29 // Initialization
30 // ============================================================================
31 StatusCode PrecedenceSvc::initialize() {
32  using namespace concurrency;
33 
34  auto sc = Service::initialize(); // parent class must be initialized first
35  if ( sc.isFailure() ) {
36  fatal() << "Base class failed to initialize" << endmsg;
37  return sc;
38  }
39 
40  // prepare a directory to dump precedence analysis files to.
41  if ( m_dumpPrecTrace || m_dumpPrecRules ) {
42  if ( !boost::filesystem::create_directory( m_dumpDirName ) ) {
43  error() << "Could not create directory " << m_dumpDirName
44  << "required "
45  "for task precedence tracing"
46  << endmsg;
47  return StatusCode::FAILURE;
48  }
49  }
50 
51  if ( m_dumpPrecRules ) m_PRGraph.enableAnalysis();
52 
53  // Get the algo resource pool
54  m_algResourcePool = serviceLocator()->service( "AlgResourcePool" );
55  if ( !m_algResourcePool.isValid() ) {
56  fatal() << "Error retrieving AlgoResourcePool" << endmsg;
57  return StatusCode::FAILURE;
58  }
59 
60  info() << "Assembling CF and DF task precedence rules" << endmsg;
61 
62  ON_DEBUG debug() << "Assembling CF precedence realm:" << endmsg;
63  // create the root CF node
64  m_PRGraph.addHeadNode( "RootDecisionHub", Concurrent{ true }, PromptDecision{ false }, ModeOr{ true },
65  AllPass{ true }, Inverted{ false } );
66  // assemble the CF rules
67  for ( const auto& ialgoPtr : m_algResourcePool->getTopAlgList() ) {
68  auto algorithm = dynamic_cast<Gaudi::Algorithm*>( ialgoPtr );
69  if ( !algorithm ) fatal() << "Conversion from IAlgorithm to Gaudi::Algorithm failed" << endmsg;
70  sc = assembleCFRules( algorithm, "RootDecisionHub" );
71  if ( sc.isFailure() ) {
72  fatal() << "Could not assemble the CF precedence realm" << endmsg;
73  return sc;
74  }
75  }
76 
77  if ( m_ignoreDFRules ) {
78  warning() << "Ignoring DF precedence rules, disabling all associated features" << endmsg;
79  return StatusCode::SUCCESS;
80  }
81 
82  ON_DEBUG debug() << "Assembling DF precedence realm:" << endmsg;
83  sc = m_PRGraph.initialize();
84  if ( sc.isFailure() ) {
85  fatal() << "Could not assemble the DF precedence realm" << endmsg;
86  return sc;
87  }
88 
89  // Rank algorithms if a prioritization rule is supplied
90  if ( m_mode == "PCE" ) {
92  m_PRGraph.rankAlgorithms( ranker );
93  } else if ( m_mode == "COD" ) {
95  m_PRGraph.rankAlgorithms( ranker );
96  } else if ( m_mode == "E" ) {
97  auto ranker = concurrency::RankerByEccentricity();
98  m_PRGraph.rankAlgorithms( ranker );
99  } else if ( m_mode == "T" ) {
100  auto ranker = concurrency::RankerByTiming();
101  m_PRGraph.rankAlgorithms( ranker );
102  } else if ( m_mode == "DRE" ) {
104  m_PRGraph.rankAlgorithms( ranker );
105  } else if ( !m_mode.empty() ) {
106  error() << "Requested prioritization rule '" << m_mode << "' is unknown" << endmsg;
107  return StatusCode::FAILURE;
108  }
109 
110  if ( m_showDataFlow ) { debug() << m_PRGraph.dumpDataFlow() << endmsg; }
111 
112  if ( m_verifyRules ) {
113  ON_DEBUG debug() << "Verifying task precedence rules" << endmsg;
114 
115  // Check if CF properties are self-consistent
116  auto propValidator = concurrency::NodePropertiesValidator();
117  m_PRGraph.accept( propValidator );
118  if ( !propValidator.passed() )
119  warning() << propValidator.reply() << endmsg;
120  else
121  ON_DEBUG debug() << propValidator.reply() << endmsg;
122 
123  // Check for violations in the DF topology
124  auto prodValidator = concurrency::ProductionAmbiguityFinder();
125  m_PRGraph.accept( prodValidator );
126  if ( !prodValidator.passed() ) {
127  error() << prodValidator.reply() << endmsg;
128  return StatusCode::FAILURE;
129  } else {
130  ON_DEBUG debug() << prodValidator.reply() << endmsg;
131  }
132 
133  auto sccFinder = concurrency::TarjanSCCFinder();
134  m_PRGraph.accept( sccFinder );
135  if ( !sccFinder.passed() ) {
136  error() << sccFinder.reply() << endmsg;
137  return StatusCode::FAILURE;
138  } else {
139  ON_DEBUG debug() << sccFinder.reply() << endmsg;
140  }
141  }
142 
143  if ( sc.isSuccess() ) info() << "PrecedenceSvc initialized successfully" << endmsg;
144 
145  return sc;
146 }
147 
148 // ============================================================================
150  unsigned int recursionDepth ) {
151  using namespace concurrency;
152 
154 
155  ++recursionDepth;
156 
157  bool isGaudiSequencer( false );
158  bool isAthSequencer( false );
159 
160  if ( !algo->isSequence() ) {
161  ON_DEBUG debug() << std::string( recursionDepth, ' ' ) << "Algorithm '" << algo->name() << "' discovered" << endmsg;
162  sc = m_PRGraph.addAlgorithmNode( algo, parentName );
163  return sc;
164  } else {
165  if ( algo->hasProperty( "ShortCircuit" ) )
166  isGaudiSequencer = true;
167  else if ( algo->hasProperty( "StopOverride" ) )
168  isAthSequencer = true;
169  }
170 
171  auto seq = dynamic_cast<Gaudi::Sequence*>( algo );
172  if ( seq == 0 ) {
173  error() << "Algorithm " << algo->name() << " has isSequence==true, but unable to dcast to Sequence" << endmsg;
174  return StatusCode::FAILURE;
175  }
176 
177  auto subAlgorithms = seq->subAlgorithms();
178 
179  // Recursively unroll
180  ON_DEBUG debug() << std::string( recursionDepth, ' ' ) << "Decision hub '" << algo->name() << "' discovered"
181  << endmsg;
182  bool modeOr = false;
183  bool allPass = false;
184  bool promptDecision = false;
185  bool isSequential = false;
186  bool isInverted = false;
187 
188  if ( isGaudiSequencer ) {
189  modeOr = ( algo->getProperty( "ModeOR" ).toString() == "True" );
190  allPass = ( algo->getProperty( "IgnoreFilterPassed" ).toString() == "True" );
191  promptDecision = ( algo->getProperty( "ShortCircuit" ).toString() == "True" );
192  isInverted = ( algo->getProperty( "Invert" ).toString() == "True" );
193  if ( allPass ) promptDecision = false; // standard Gaudi::Sequencer behavior on all pass is to execute everything
194  isSequential = ( algo->hasProperty( "Sequential" ) && ( algo->getProperty( "Sequential" ).toString() == "True" ) );
195  } else if ( isAthSequencer ) {
196  modeOr = ( algo->getProperty( "ModeOR" ).toString() == "True" );
197  allPass = ( algo->getProperty( "IgnoreFilterPassed" ).toString() == "True" );
198  promptDecision = ( algo->getProperty( "StopOverride" ).toString() == "False" );
199  isSequential = ( algo->hasProperty( "Sequential" ) && ( algo->getProperty( "Sequential" ).toString() == "True" ) );
200  }
201  sc = m_PRGraph.addDecisionHubNode( algo, parentName, Concurrent{ !isSequential }, PromptDecision{ promptDecision },
202  ModeOr{ modeOr }, AllPass{ allPass }, Inverted{ isInverted } );
203  if ( sc.isFailure() ) {
204  error() << "Failed to add DecisionHub " << algo->name() << " to graph of precedence rules" << endmsg;
205  return sc;
206  }
207 
208  for ( auto subalgo : *subAlgorithms ) {
209  sc = assembleCFRules( subalgo, algo->name(), recursionDepth );
210  if ( sc.isFailure() ) {
211  error() << "Algorithm " << subalgo->name() << " could not be flattened" << endmsg;
212  return sc;
213  }
214  }
215  return sc;
216 }
217 
218 // ============================================================================
220 
221  if ( Cause::source::Task == cause.m_source ) {
222  ON_VERBOSE verbose() << "Triggering bottom-up traversal at node '" << cause.m_sourceName << "'" << endmsg;
223  auto visitor = concurrency::DecisionUpdater( slot, cause, m_dumpPrecTrace );
224  m_PRGraph.getAlgorithmNode( cause.m_sourceName )->accept( visitor );
225  } else {
226  ON_VERBOSE verbose() << "Triggering top-down traversal at the root node" << endmsg;
227  auto visitor = concurrency::Supervisor( slot, cause, m_dumpPrecTrace );
228  m_PRGraph.getHeadNode()->accept( visitor );
229  }
230 
231  if ( m_dumpPrecTrace )
232  if ( CFRulesResolved( slot.parentSlot ? *slot.parentSlot : slot ) )
233  dumpPrecedenceTrace( slot.parentSlot ? *slot.parentSlot : slot );
234 
235  if ( m_dumpPrecRules )
236  if ( CFRulesResolved( slot.parentSlot ? *slot.parentSlot : slot ) )
237  dumpPrecedenceRules( slot.parentSlot ? *slot.parentSlot : slot );
238 
239  return StatusCode::SUCCESS;
240 }
241 
242 // ============================================================================
244 
245  Cause cs = { Cause::source::Root, "RootDecisionHub" };
246  auto visitor = concurrency::RunSimulator( slot, cs );
247 
248  auto& nodeDecisions = slot.controlFlowState;
249 
250  std::vector<int> prevNodeDecisions;
251  int cntr = 0;
252  std::vector<int> counters;
253 
254  while ( !CFRulesResolved( slot ) ) {
255  cntr += 1;
256  int prevAlgosNum = visitor.m_nodesSucceeded;
257  ON_DEBUG debug() << " Proceeding with iteration #" << cntr << endmsg;
258  prevNodeDecisions = slot.controlFlowState;
259  m_PRGraph.getHeadNode()->accept( visitor );
260  if ( prevNodeDecisions == nodeDecisions ) {
261  error() << " No progress on iteration " << cntr << " detected, node decisions are:" << nodeDecisions << endmsg;
262  return StatusCode::FAILURE;
263  }
264  info() << " Iteration #" << cntr << " finished, total algorithms executed: " << visitor.m_nodesSucceeded
265  << endmsg;
266 
268  s << cntr << ", " << ( visitor.m_nodesSucceeded - prevAlgosNum ) << "\n";
269 
270  std::ofstream myfile;
271  myfile.open( "RunSimulation.csv", std::ios::app );
272  myfile << s.str();
273  myfile.close();
274 
275  if ( visitor.m_nodesSucceeded != prevAlgosNum ) counters.push_back( visitor.m_nodesSucceeded );
276  }
277 
278  info() << "Asymptotical intra-event speedup: " << (float)visitor.m_nodesSucceeded / (float)counters.size() << endmsg;
279 
280  // Reset algorithm states and node decisions
281  slot.algsStates.reset();
282  nodeDecisions.assign( nodeDecisions.size(), -1 );
283 
284  return StatusCode::SUCCESS;
285 }
286 
287 // ============================================================================
289  return ( -1 != slot.controlFlowState[m_PRGraph.getHeadNode()->getNodeIndex()] ? true : false );
290 }
291 
292 // ============================================================================
294 
295  info() << std::endl << "==================== Control Flow Configuration ==================" << std::endl << std::endl;
296  info() << m_PRGraph.dumpControlFlow() << endmsg;
297 }
298 // ============================================================================
300  info() << std::endl << "===================== Data Flow Configuration ====================" << std::endl;
301  info() << m_PRGraph.dumpDataFlow() << endmsg;
302 }
303 
304 // ============================================================================
306 
308  m_PRGraph.printState( ss, slot, 0 );
309  return ss.str();
310 }
311 
312 // ============================================================================
314 
315  if ( !m_dumpPrecRules ) {
316  warning() << "To trace temporal and topological aspects of execution flow, "
317  << "set DumpPrecedenceRules property to True " << endmsg;
318  return;
319  }
320 
321  ON_DEBUG debug() << "Dumping temporal precedence rules" << endmsg;
322 
323  std::string fileName;
324  if ( m_dumpPrecRulesFile.empty() ) {
325  const auto& eventID = slot.eventContext->eventID();
326  fileName = "rules_evt-" + std::to_string( eventID.event_number() ) + "_slot-" +
327  std::to_string( slot.eventContext->slot() ) + "_run-" + std::to_string( eventID.run_number() ) +
328  ".graphml";
329  } else {
330  fileName = m_dumpPrecRulesFile;
331  }
332 
334  pth.append( fileName );
335 
336  m_PRGraph.dumpPrecRules( pth, slot );
337 }
338 
339 // ============================================================================
341 
342  if ( !m_dumpPrecTrace ) {
343  warning() << "To trace task precedence patterns, set DumpPrecedenceTrace "
344  << "property to True " << endmsg;
345  return;
346  }
347 
348  ON_DEBUG debug() << "Dumping temporal precedence trace" << endmsg;
349 
350  std::string fileName;
351  if ( m_dumpPrecTraceFile.empty() ) {
352  const auto& eventID = slot.eventContext->eventID();
353  fileName = "trace_evt-" + std::to_string( eventID.event_number() ) + "_slot-" +
354  std::to_string( slot.eventContext->slot() ) + "_run-" + std::to_string( eventID.run_number() ) +
355  ".graphml";
356  } else {
357  fileName = m_dumpPrecTraceFile;
358  }
359 
361  pth.append( fileName );
362 
363  m_PRGraph.dumpPrecTrace( pth, slot );
364 }
365 
366 // ============================================================================
367 // Finalize
368 // ============================================================================
EventSlot::eventContext
std::unique_ptr< EventContext > eventContext
Cache for the eventContext.
Definition: EventSlot.h:83
PrecedenceSvc
A service to resolve the task execution precedence.
Definition: PrecedenceSvc.h:31
PrecedenceSvc::m_dumpPrecRules
Gaudi::Property< bool > m_dumpPrecRules
Definition: PrecedenceSvc.h:99
PrecedenceSvc::dumpPrecedenceTrace
void dumpPrecedenceTrace(const EventSlot &) override
Dump precedence trace (available only in DEBUG mode, and must be enabled with the corresponding servi...
Definition: PrecedenceSvc.cpp:340
Service::initialize
StatusCode initialize() override
Definition: Service.cpp:118
concurrency::PrecedenceRulesGraph::printState
void printState(std::stringstream &output, EventSlot &slot, const unsigned int &recursionLevel) const
Print a string representing the control flow state.
Definition: PrecedenceRulesGraph.cpp:260
std::string
STL class.
concurrency::PrecedenceRulesGraph::dumpPrecTrace
void dumpPrecTrace(const boost::filesystem::path &, const EventSlot &slot)
dump to file the precedence trace
Definition: PrecedenceRulesGraph.cpp:666
concurrency::Supervisor
Definition: Promoters.h:64
PrecedenceSvc::dumpPrecedenceRules
void dumpPrecedenceRules(const EventSlot &) override
Dump precedence rules (available only in DEBUG mode, and must be enabled with the corresponding servi...
Definition: PrecedenceSvc.cpp:313
Read.app
app
Definition: Read.py:36
PrecedenceSvc::m_PRGraph
concurrency::PrecedenceRulesGraph m_PRGraph
Graph of precedence rules.
Definition: PrecedenceSvc.h:84
AtlasMCRecoFullPrecedenceDump.path
path
Definition: AtlasMCRecoFullPrecedenceDump.py:49
Gaudi::Algorithm::name
const std::string & name() const override
The identifying name of the algorithm object.
Definition: Algorithm.cpp:526
PrecedenceSvc::m_dumpPrecTraceFile
Gaudi::Property< std::string > m_dumpPrecTraceFile
Definition: PrecedenceSvc.h:94
EventContext::eventID
const EventIDBase & eventID() const
Definition: EventContext.h:55
EventSlot.h
gaudirun.s
string s
Definition: gaudirun.py:346
std::vector< int >
std::vector::size
T size(T... args)
PrecedenceSvc::assembleCFRules
StatusCode assembleCFRules(Gaudi::Algorithm *, const std::string &, unsigned int recursionDepth=0)
Definition: PrecedenceSvc.cpp:149
concurrency::RankerByProductConsumption
Definition: Rankers.h:20
EventSlot
Class representing an event slot.
Definition: EventSlot.h:24
concurrency::PrecedenceRulesGraph::dumpPrecRules
void dumpPrecRules(const boost::filesystem::path &, const EventSlot &slot)
dump to file the precedence rules
Definition: PrecedenceRulesGraph.cpp:625
std::stringstream
STL class.
PropertyHolder::getProperty
StatusCode getProperty(Gaudi::Details::PropertyBase *p) const override
get the property
Definition: PropertyHolder.h:190
concurrency::ProductionAmbiguityFinder
Definition: Validators.h:132
PrecedenceSvc::printState
const std::string printState(EventSlot &) const override
Definition: PrecedenceSvc.cpp:305
Service::finalize
StatusCode finalize() override
Definition: Service.cpp:222
concurrency::AlgorithmNode::accept
bool accept(IGraphVisitor &visitor) override
Visitor entry point.
Definition: PrecedenceRulesGraph.cpp:191
std::vector::push_back
T push_back(T... args)
PrecedenceSvc::simulate
StatusCode simulate(EventSlot &) const override
Simulate execution flow.
Definition: PrecedenceSvc.cpp:243
PrecedenceSvc::dumpDataFlow
void dumpDataFlow() const override
Definition: PrecedenceSvc.cpp:299
ON_VERBOSE
#define ON_VERBOSE
Definition: PrecedenceSvc.cpp:24
StatusCode
Definition: StatusCode.h:65
Gaudi::tagged_bool_ns::tagged_bool
Definition: TaggedBool.h:16
PrecedenceSvc::m_dumpDirName
boost::filesystem::path m_dumpDirName
Precedence analysis facilities.
Definition: PrecedenceSvc.h:90
PrecedenceSvc.h
EventSlot::parentSlot
EventSlot * parentSlot
Pointer to parent slot (null for top level)
Definition: EventSlot.h:96
std::ofstream
STL class.
EventContext::slot
ContextID_t slot() const
Definition: EventContext.h:51
Cause::m_source
source m_source
Definition: PrecedenceRulesGraph.h:399
Gaudi::Algorithm
Base class from which all concrete algorithm classes should be derived.
Definition: Algorithm.h:90
concurrency::RankerByEccentricity
Definition: Rankers.h:49
std::to_string
T to_string(T... args)
PrecedenceSvc::CFRulesResolved
bool CFRulesResolved(EventSlot &) const override
Check if the root CF decision is resolved.
Definition: PrecedenceSvc.cpp:288
PrecedenceSvc::m_dumpPrecTrace
Gaudi::Property< bool > m_dumpPrecTrace
Definition: PrecedenceSvc.h:92
Validators.h
Algorithm.h
std::ofstream::open
T open(T... args)
genconfuser.verbose
verbose
Definition: genconfuser.py:28
concurrency::PrecedenceRulesGraph::addDecisionHubNode
StatusCode addDecisionHubNode(Gaudi::Algorithm *daughterAlgo, const std::string &parentName, concurrency::Concurrent, concurrency::PromptDecision, concurrency::ModeOr, concurrency::AllPass, concurrency::Inverted)
Add a node, which aggregates decisions of direct daughter nodes.
Definition: PrecedenceRulesGraph.cpp:451
endmsg
MsgStream & endmsg(MsgStream &s)
MsgStream Modifier: endmsg. Calls the output method of the MsgStream.
Definition: MsgStream.h:202
AlgsExecutionStates::reset
void reset()
Definition: AlgsExecutionStates.h:63
Cause::source::Root
@ Root
concurrency::PrecedenceRulesGraph::dumpControlFlow
std::string dumpControlFlow() const
Print out control flow of Algorithms and Sequences.
Definition: PrecedenceRulesGraph.cpp:560
PrecedenceSvc::iterate
StatusCode iterate(EventSlot &, const Cause &) override
Infer the precedence effect caused by an execution flow event.
Definition: PrecedenceSvc.cpp:219
concurrency
Definition: PrecedenceRulesGraph.cpp:38
Rankers.h
StatusCode::isFailure
bool isFailure() const
Definition: StatusCode.h:129
concurrency::DecisionUpdater
Definition: Promoters.h:48
concurrency::PrecedenceRulesGraph::getAlgorithmNode
AlgorithmNode * getAlgorithmNode(const std::string &algoName) const
Get the AlgorithmNode from by algorithm name using graph index.
Definition: PrecedenceRulesGraph.h:651
concurrency::PrecedenceRulesGraph::dumpDataFlow
std::string dumpDataFlow() const
Print out all data origins and destinations, as reflected in the EF graph.
Definition: PrecedenceRulesGraph.cpp:591
StatusCode::SUCCESS
constexpr static const auto SUCCESS
Definition: StatusCode.h:100
Cause::source::Task
@ Task
std::endl
T endl(T... args)
concurrency::NodePropertiesValidator
Definition: Validators.h:28
DECLARE_COMPONENT
#define DECLARE_COMPONENT(type)
Definition: PluginServiceV1.h:46
concurrency::PrecedenceRulesGraph::addAlgorithmNode
StatusCode addAlgorithmNode(Gaudi::Algorithm *daughterAlgo, const std::string &parentName)
Add algorithm node.
Definition: PrecedenceRulesGraph.cpp:372
Promoters.h
PrecedenceSvc::m_dumpPrecRulesFile
Gaudi::Property< std::string > m_dumpPrecRulesFile
Definition: PrecedenceSvc.h:100
Sequence.h
PrecedenceSvc::dumpControlFlow
void dumpControlFlow() const override
Dump precedence rules.
Definition: PrecedenceSvc.cpp:293
std::stringstream::str
T str(T... args)
ON_DEBUG
#define ON_DEBUG
Definition: PrecedenceSvc.cpp:23
concurrency::RankerByCummulativeOutDegree
Definition: Rankers.h:28
EventSlot::controlFlowState
std::vector< int > controlFlowState
State of the control flow.
Definition: EventSlot.h:87
concurrency::DecisionNode::accept
bool accept(IGraphVisitor &visitor) override
Visitor entry point.
Definition: PrecedenceRulesGraph.cpp:65
ManySmallAlgs.seq
seq
Definition: ManySmallAlgs.py:102
StatusCode::FAILURE
constexpr static const auto FAILURE
Definition: StatusCode.h:101
Gaudi::Sequence
Definition: Sequence.h:18
concurrency::TarjanSCCFinder
Definition: Validators.h:169
concurrency::PrecedenceRulesGraph::getHeadNode
DecisionNode * getHeadNode() const
Get head node.
Definition: PrecedenceRulesGraph.h:647
EventSlot::algsStates
AlgsExecutionStates algsStates
Vector of algorithms states.
Definition: EventSlot.h:85
Cause
Definition: PrecedenceRulesGraph.h:396
concurrency::RankerByTiming
Definition: Rankers.h:41
Gaudi::Algorithm::isSequence
bool isSequence() const override
Are we a Sequence?
Definition: Algorithm.h:198
concurrency::RunSimulator
Definition: Promoters.h:87
concurrency::ControlFlowNode::getNodeIndex
const unsigned int & getNodeIndex() const
Get node index.
Definition: PrecedenceRulesGraph.h:427
PrecedenceSvc::finalize
StatusCode finalize() override
Finalize.
Definition: PrecedenceSvc.cpp:369
concurrency::RankerByDataRealmEccentricity
Definition: Rankers.h:57
Cause::m_sourceName
std::string m_sourceName
Definition: PrecedenceRulesGraph.h:400
PropertyHolder::hasProperty
bool hasProperty(std::string_view name) const override
Return true if we have a property with the given name.
Definition: PropertyHolder.h:227