1
0
Fork 0

src/Viewer/ViewPropertyEvaluator.*: added ViewPropertyEvaluator system.

Evaluates and caches nested properties.
This commit is contained in:
Julian Smith 2019-07-27 08:43:44 +01:00
parent e778848dcb
commit bf29e469b0
3 changed files with 808 additions and 0 deletions

View file

@ -6,6 +6,7 @@ set(SOURCES
WindowSystemAdapter.cxx
fg_os_osgviewer.cxx
fgviewer.cxx
ViewPropertyEvaluator.cxx
splash.cxx
view.cxx
viewmgr.cxx

View file

@ -0,0 +1,687 @@
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "ViewPropertyEvaluator.hxx"
#include "Main/globals.hxx"
#include <algorithm>
#include <cassert>
namespace ViewPropertyEvaluator {
/* We represent a spec as graph, using alternating Sequence and Node
objects so that different specs share common information; e.g. this ensures
that we don't install more than one listener for the same SGPropertyNode.
Evaluating top-level nodes:
Currently ViewPropertyEvaluator::getDoubleValue() will always
reevaluate the top-level SGPropertyNode by calling its getDoubleValue()
member. This usually gives the desired behaviour because most
final property nodes that we are used with don't appear to make
valueChanged() callbacks, and it's anyway probably more efficient to
not use such callbacks for rapidly-changing values.
However it would be good to be clearer about this, e.g. maybe we could
have a second bracket notation to indicate that we should evaluate and
cache the SGPropertyNode but not its string/double value. E.g.:
ViewPropertyEvaluator::getDoubleValue(
"{(/sim/view[0]/config/root)/position/altitude-ft}"
);
- would not attempt to install a valueChanged() callback for the
top-level SGPropertyNode.
*/
struct Sequence;
struct Node;
struct Sequence
{
Sequence();
std::vector<std::shared_ptr<Node>> _nodes;
std::vector<Node*> _parents;
bool _rescan;
std::string _value;
};
struct Node : SGPropertyChangeListener
{
explicit Node(const char* spec);
/* SGPropertyChangeListener callback. */
void valueChanged(SGPropertyNode* node);
const char* _begin;
const char* _end;
bool _rescan;
std::string _value;
std::vector<Sequence*> _parents;
// Only used if _begin.._end is (...).
std::shared_ptr<Sequence> _child;
SGPropertyNode_ptr _sgnode;
SGPropertyNode_ptr _sgnode_listen;
};
/* Helper for dumping a Sequence to an ostream. Prefixes all lines with
<indent>. If <deep> is true, recursively shows all child sequences and
nodes. */
struct SequenceDump
{
SequenceDump(const Sequence& sequence, const std::string& indent="", bool deep=false);
const Sequence& _sequence;
const std::string& _indent;
bool _deep;
friend std::ostream& operator << (std::ostream& out, const SequenceDump& self);
};
/* Helper for dumping a Node to an ostream. Prefixes all lines with
<indent>. If <deep> is true, recursively shows all child sequences and
nodes. */
struct NodeDump
{
NodeDump(const Node& node, const std::string& indent="", bool deep=false);
const Node& _node;
const std::string& _indent;
bool _deep;
friend std::ostream& operator << (std::ostream& out, const NodeDump& self);
};
/* Support for debug statistics. */
struct Debug
{
/* Support for tracking how many property system accesses we are making. */
struct Stat
{
Stat() : n(0) {}
int n;
};
/* Increments counter for <name>. Periodically outputs stats with
SG_LOG(SG_VIEW, SG_DEBUG, ...) and detailed information about Sequences
and Nodes with SG_LOG(SG_VIEW, SG_BULK, ...). */
void statsAdd(const char* name);
void statsReset();
struct StatsShow {};
friend std::ostream& operator << (std::ostream& out, const StatsShow&);
/* Track how many listeners we have. */
void listensAdd(SGPropertyNode_ptr node);
void listensRemove(SGPropertyNode_ptr node);
time_t statsT0 = 0;
std::map<std::string, std::shared_ptr<Stat>> stats;
std::vector<SGPropertyNode_ptr> listens;
};
Debug debug;
/* Forces this node and all of its sequence and node parents to be
re-read the next time they are evaluated - e.g. the next call of
getNodeStringValue() will call getSequenceStringValue() on _child and
write the result into the <_value> member before returning <_value>. */
void rescanNode(Node& node);
/* Forces this sequence and all of its node and sequence parents to
be re-read the next time they are evaluated - e.g. the next call of
getSequenceStringValue() will call getNodeStringValue() on each child
node and concatenate the results into the <_value> member before
returning <_value>. */
void rescanSequence(Sequence& sequence);
/* Returns Sequence for spec starting at <spec>, which can be subsequently
used to evaluate the spec efficiently. We require that the string <spec>
will remain unchanged forever. */
std::shared_ptr<Sequence> getSequence(const char* spec);
/* Returns evaluated spec for <node> using caching. Mainly used
internally.
If node spec is of the form "<...>", we look up the value in the
property system. */
const std::string& getNodeStringValue(Node& node);
/* Returns evaluated spec for <sequence> using caching. Mainly used
internally. Concatenates the string value of each child node. */
const std::string& getSequenceStringValue(Sequence& sequence);
/* <sequence> must be from a spec with a top-level '(...)'. Returns the
specified property-tree node. */
SGPropertyNode* getSequenceNode(Sequence& sequence);
double getSequenceDoubleValue(Sequence& sequence, double default_=0);
/* Finds end of section of spec starting at <spec>, to be used as the
region of a Sequence; this will contain one or more regions corresponding
to a Node. Any ')' without a corresponding '(' is treated as a terminator.
*/
const char* getSequenceEnd(const char* spec)
{
int nesting = 0;
for (const char* s=spec; ; ++s) {
if (*s == 0) {
assert(nesting == 0);
return s;
}
if (*s == '(') {
nesting += 1;
}
if (*s == ')') {
if (nesting == 0) {
return s;
}
nesting -= 1;
}
}
}
/* Finds end of section of spec starting at <spec>, to be used as the
region of a Node.
We parse things similarly to getSequenceEnd() except that we also terminate
early in the following situations:
The first character is not '(' and we find a '('.
The first character is '(' and we find the corresponding ')'.
*/
const char* getNodeEnd(const char* spec)
{
int nesting = 0;
for (const char* s=spec; ; ++s) {
if (*s == 0) {
assert(nesting == 0);
return s;
}
if (*s == '(') {
if (spec[0] != '(') {
return s;
}
nesting += 1;
}
if (*s == ')') {
if (nesting == 0) {
return s;
}
nesting -= 1;
if (spec[0] == '(' && nesting == 0) {
return s + 1;
}
}
}
}
/* Raw constructor. We only set up basic values; the rest is done
by getNodeInternal(). */
Node::Node(const char* spec)
:
_begin(spec),
_end(getNodeEnd(_begin)),
_rescan(true)
{
}
void Node::valueChanged(SGPropertyNode* node)
{
debug.statsAdd("valueChanged");
SG_LOG(SG_VIEW, SG_DEBUG, "valueChanged():"
<< " node->_sgnode_listen->getPath()='" << _sgnode_listen->getPath() << "'"
<< " node->getPath()='" << node->getPath() << "'"
);
rescanNode(*this);
}
Sequence::Sequence()
:
_rescan(true)
{
}
void rescanNode(Node& node)
{
node._rescan = true;
for (auto sequence: node._parents) {
rescanSequence(*sequence);
}
}
void rescanSequence(Sequence& sequence)
{
sequence._rescan = true;
for (auto node: sequence._parents) {
rescanNode(*node);
}
}
/* Caches of various structures so that we can look things up quickly.
*/
/* This is the main cache. It uses raw C string pointers (pointing
to specs) as keys, so lookups only require a handful of pointer
comparisons. */
std::map<const char*, std::shared_ptr<Sequence>> spec_to_sequence;
/* These are only used when parsing new specs and creating new nodes and
sequencies, so are not speed-critical. */
std::map<std::string, std::shared_ptr<Sequence>> string_to_sequence;
std::map<std::string, std::shared_ptr<Node>> string_to_node;
/* Finds or creates new Sequence for (possibly initial) portion of <spec>.
*/
std::shared_ptr<Sequence> getSequenceInternal(const char* spec, Node* parent);
/* Finds or creates new Node for (possibly initial) portion of <spec>.
*/
std::shared_ptr<Node> getNodeInternal(const char* spec, Sequence* parent);
std::shared_ptr<Sequence> getSequenceInternal(const char* spec, Node* parent)
{
if (spec[0] == 0 || spec[0] == ')') {
return NULL;
}
std::shared_ptr<Sequence> sequence;
std::string spec_string(spec, getSequenceEnd(spec));
auto it = string_to_sequence.find(spec_string);
if (it == string_to_sequence.end()) {
sequence.reset(new Sequence);
for(const char* s = spec;;) {
std::shared_ptr<Node> node
= getNodeInternal(s, sequence.get() /*parent*/);
if (!node) break;
sequence->_nodes.push_back(node);
s += (node->_end - node->_begin);
}
string_to_sequence[spec_string] = sequence;
}
else {
sequence = it->second;
}
if (parent) {
auto it = std::find(
sequence->_parents.begin(),
sequence->_parents.end(),
parent
);
if (it == sequence->_parents.end()) {
sequence->_parents.push_back(parent);
}
}
return sequence;
}
std::shared_ptr<Node> getNodeInternal(const char* spec, Sequence* parent)
{
if (spec[0] == 0 || spec[0] == ')') {
return NULL;
}
std::shared_ptr<Node> node;
std::string s(spec, getNodeEnd(spec));
auto it = string_to_node.find(s);
if (it == string_to_node.end()) {
node.reset(new Node(spec));
if (node->_begin[0] == '(') {
node->_child = getSequenceInternal(node->_begin + 1, node.get() /*parent*/);
}
string_to_node[s] = node;
}
else {
node = it->second;
}
if (parent) {
if (std::find(node->_parents.begin(), node->_parents.end(), parent)
== node->_parents.end()) {
node->_parents.push_back(parent);
}
}
return node;
}
/* Finds or creates new Sequence for (possibly initial) portion of <spec>
and sets spec_to_sequence[spec] so that it can be quickly looked up in
future. */
std::shared_ptr<Sequence> getSequence(const char* spec)
{
auto it = spec_to_sequence.find(spec);
if (it != spec_to_sequence.end()) {
return it->second;
}
std::shared_ptr<Sequence> sequence
= getSequenceInternal(spec, NULL /*parent*/);
spec_to_sequence[spec] = sequence;
SG_LOG(SG_VIEW, SG_DEBUG,
"Created new sequence:\n"
<< SequenceDump(*sequence, " ", true /*deep*/)
);
return sequence;
}
/* Evaluates <node> to find path and returns corresponding SGPropertyNode*
in global properties. If <cache> is true, we install a listener on
the returned node, so that we force a rescan of all the node's parent
Sequence's if its value changes. */
SGPropertyNode* getNodeSGNode(Node& node, bool cache=true)
{
assert(node._begin[0] == '(');
assert(node.child);
if (node._rescan) {
node._rescan = false;
if (!node._sgnode || node._child->_rescan) {
const std::string& path = getSequenceStringValue(*node._child);
SGPropertyNode* sgnode = NULL;
if (path != "") {
debug.statsAdd( "propertypath_getNode");
sgnode = globals->get_props()->getNode(path);
if (!sgnode) {
debug.statsAdd( "propertypath_getNode_failed");
SG_LOG(SG_VIEW, SG_DEBUG, ": getNodeSGNode(): getNode() failed, path='" << path << "'");
}
}
if (sgnode != node._sgnode) {
if (node._sgnode_listen) {
node._sgnode_listen->removeChangeListener(&node);
debug.listensRemove(node._sgnode_listen);
node._sgnode_listen = NULL;
}
if (node._sgnode) {
node._sgnode->removeChangeListener(&node);
}
node._sgnode = sgnode;
if (node._sgnode && cache) {
node._sgnode_listen = node._sgnode;
node._sgnode->addChangeListener(&node, false /*initial*/);
debug.listensAdd(node._sgnode);
}
}
if (!node._sgnode && path != "") {
/* Ideally we'd ask Simgear's property system to call us
back if <path> was created, but this is non-trivial, and
not actually required when we are used by view.cxx. */
}
}
}
return node._sgnode;
}
const std::string& getNodeStringValue(Node& node)
{
if (node._rescan) {
if (node._begin[0] == '(') {
getNodeSGNode(node);
if (node._sgnode) {
debug.statsAdd( "property_getStringValue");
node._value = node._sgnode->getStringValue();
}
else {
node._value = "";
}
}
else {
node._rescan = false;
node._value = std::string(node._begin, node._end);
}
}
return node._value;
}
const std::string& getSequenceStringValue(Sequence& sequence)
{
if (sequence._rescan) {
sequence._rescan = false;
sequence._value = "";
for (auto node: sequence._nodes) {
sequence._value += getNodeStringValue(*node);
}
}
return sequence._value;
}
/* Assumes that sequence has a single child Node object with a non-empty
Sequence child object whose value is a path in the property system. */
SGPropertyNode* getSequenceNode(Sequence& sequence)
{
assert(sequence._nodes.size() == 1);
Node& node = *sequence._nodes.front();
return getNodeSGNode(node, false /*cache*/);
}
/* This is different from getSequenceStringValue() in that it assumes that
a spec has a single top-level (...) and so the top-level sequence has a
single child Node object which in turn has a Sequence child object whose
value is a path in the property system.
We always call the SGPropertyNode's getDoubleValue() method - we don't
cache the double value. But the underlying SGPropertyNode* will be cached.
*/
double getSequenceDoubleValue(Sequence& sequence, double default_)
{
SGPropertyNode* node = getSequenceNode(sequence);
if (node) {
return node->getDoubleValue();
}
return default_;
}
const std::string& getStringValue(const char* spec)
{
std::shared_ptr<Sequence> sequence = getSequence(spec);
return getSequenceStringValue(*sequence);
}
double getDoubleValue(const char* spec, double default_)
{
std::shared_ptr<Sequence> sequence = getSequence(spec);
if (sequence->_nodes.size() != 1 || sequence->_nodes.front()->_begin[0] != '(') {
SG_LOG(SG_VIEW, SG_DEBUG, "bad sequence for getDoubleValue() - must have outermost '(...)': '" << spec);
abort();
}
double ret = getSequenceDoubleValue(*sequence, default_);
return ret;
}
std::ostream& operator << (std::ostream& out, const Dump& dump)
{
out << "ViewPropertyEvaluator\n";
out << " Number of specs: " << spec_to_sequence.size() << ":\n";
int i = 0;
for (auto it: spec_to_sequence) {
out << " " << (i+1) << "/" << spec_to_sequence.size() << ": spec: " << it.first << "\n";
out << SequenceDump(*it.second, " ", true /*deep*/);
i += 1;
}
out << " Number of sequences: " << string_to_sequence.size() << "\n";
i = 0;
for (auto it: string_to_sequence) {
out << " " << (i+1) << "/" << string_to_sequence.size()
<< ": spec='" << it.first << "'"
<< ": " << SequenceDump(*it.second, "", false /*deep*/)
;
i += 1;
}
out << " Number of nodes: " << string_to_node.size() << "\n";
i = 0;
for (auto it: string_to_node) {
out << " " << (i+1) << "/" << string_to_node.size()
<< ": spec='" << it.first << "'"
<< ": " << NodeDump(*it.second, "", false /*deep*/)
;
i += 1;
}
out << " Number of listens: " << debug.listens.size() << "\n";
i = 0;
for (auto it: debug.listens) {
out << " " << (i+1) << "/" << debug.listens.size()
<< ": " << it->getPath()
<< "\n";
i += 1;
}
return out;
}
void clear()
{
spec_to_sequence.clear();
string_to_sequence.clear();
string_to_node.clear();
debug = Debug();
}
/* === Everything below here is for diagnostics and/or debugging. */
SequenceDump::SequenceDump(const Sequence& sequence, const std::string& indent, bool deep)
:
_sequence(sequence),
_indent(indent),
_deep(deep)
{}
NodeDump::NodeDump(const Node& node, const std::string& indent, bool deep)
:
_node(node),
_indent(indent),
_deep(deep)
{}
void Debug::listensAdd(SGPropertyNode_ptr node)
{
debug.listens.push_back(node);
}
void Debug::listensRemove(SGPropertyNode_ptr node)
{
auto it = std::find(debug.listens.begin(), debug.listens.end(), node);
if (it == debug.listens.end()) {
SG_LOG(SG_VIEW, SG_ALERT, "Unable to find node in debug.listens");
}
else {
debug.listens.erase(it);
}
}
std::ostream& operator << (std::ostream& out, const Debug::StatsShow&)
{
time_t t = time(NULL);
time_t dt = t - debug.statsT0;
if (dt == 0) dt = 1;
out << "ViewPropertyEvaluator stats: dt=" << dt << "\n";
for (auto it: debug.stats) {
const std::string& name = it.first;
int n = it.second->n;
out << " : n=" << n << " n/sec=" << (1.0 * n / dt) << ": " << name << "\n";
}
return out;
}
void Debug::statsReset() {
for (auto it: debug.stats) {
it.second->n = 0;
}
debug.statsT0 = time(NULL);
}
void Debug::statsAdd(const char* name) {
if (debug.statsT0 == 0) debug.statsT0 = time(NULL);
std::shared_ptr<Debug::Stat>& stat = debug.stats[name];
if (!stat) {
stat.reset(new(Debug::Stat));
}
stat->n += 1;
if (1) {
static time_t t0 = time(NULL);
time_t t = time(NULL);
if (t - t0 > 10) {
t0 = t;
SG_LOG(SG_VIEW, SG_DEBUG, StatsShow());
statsReset();
/* Output all specs with SG_BULK. */
SG_LOG(SG_VIEW, SG_BULK, Dump());
}
}
}
std::ostream& operator << (std::ostream& out, const SequenceDump& self)
{
std::string spec;
for (auto node: self._sequence._nodes) {
spec += std::string(node->_begin, node->_end);
}
out << self._indent
<< "Sequence at " << &self._sequence << ":"
<< " _rescan=" << self._sequence._rescan
<< " _parents.size()=" << self._sequence._parents.size()
<< " _nodes.size()=" << self._sequence._nodes.size()
<< " _value='" << self._sequence._value << "'"
<< " spec='" << spec << "'"
<< "\n";
if (self._deep) {
for (auto node: self._sequence._nodes) {
out << NodeDump(*node, self._indent + " ", self._deep);
}
}
return out;
}
std::ostream& operator << (std::ostream& out, const NodeDump& self)
{
out << self._indent
<< "Node at " << &self._node << ":"
<< " _rescan=" << self._node._rescan
<< " _parents.size()=" << self._node._parents.size()
<< " _child=" << self._node._child.get()
<< " _value='" << self._node._value << "'"
<< " _sgnode=" << self._node._sgnode
;
if (self._node._sgnode) {
out
<< " (path=" << self._node._sgnode->getPath()
<< ", value=" << self._node._sgnode->getStringValue()
<< ")";
}
out
<< " _begin.._end='" << std::string(self._node._begin, self._node._end) << "'"
<< "\n";
if (self._deep) {
if (self._node._child) {
out << SequenceDump(*self._node._child, self._indent + " ", self._deep);
}
}
return out;
}
}

View file

@ -0,0 +1,120 @@
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#pragma once
#include "simgear/props/props.hxx"
#include <map>
#include <ostream>
#include <string>
#include <vector>
namespace ViewPropertyEvaluator {
/*
Overview:
We provide efficient evaluation of 'nested properties', where the path
of the property to be evaluated is determined by the value of other,
possibly dynamically-changing, properties.
Details:
Code is expected to specify a nested property as a C string 'spec'
using (...) to denote evaluation of a substring as a property path
where required. We use the raw address of C string specs as keys in an
internal std::map, to provide fast lookup of previously-created specs
using just a small number of raw pointer comparisons. This requires
that the C string specs remain valid and unchanged; typically they will
be immediate strings specifed directly in source code.
We set up listeners to detect changes to relevant property nodes so
that we can force re-evaluation of dependent nested properties when
required. Thus most lookups end up not touching the property system at
all, and instead access cached values directly.
For example this:
SGPropertyNode* node = ViewPropertyEvaluator::getDoubleValue(
"((/sim/view[0]/config/root)/position/altitude-ft)"
);
- will behave like this:
globals->get_props()->getDoubleValue(
globals->get_props()->getStringValue("/sim/view[0]/config/root")
+ "/position/altitude-ft"
);
In this example, an internal listener will be set up to detect changes
to property '/sim/view[0]/config/root'; if this listener has not fired,
we will use a cached SGPropertyNode* directly without querying the
property tree.
Note that with ViewPropertyEvaluator::getDoubleValue, while the final
SGPropertyNode* is cached, its value is not cached. Instead we always
call SGPropertyNode::getDoubleValue(). This is in anticipation of
typical usage where the final double values will change frequently, so
caching its value becomes less useful.
Missing property nodes:
If a property node does not exist with the required path, we use "" as
the value.
We do not attempt to detect the creation of such property nodes,
because simgear's doesn't really support this. Typically the path will
depend on other properties which do exist, and which change when the
missing property node is created, in which case we will re-evaluate
correctly.
*/
/* Evaluates a spec as a string. The returned reference will be valid for
ever (until ViewPropertyEvaluator::clear() is called); its value will be
unchanged until the next time the spec (or a spec that depends on it) is
evaluated.
For example, getStringValue("/sim/chase-distance-m") will return
"/sim/chase-distance-m", while getStringValue("(/sim/chase-distance-m)")
will return "-25" or similar, depending on the aircraft.
*/
const std::string& getStringValue(const char* spec);
/* Evaluates a spec as a double. Only makes sense if <spec> has top-level
"(...)".
For example, getDoubleValue("(/sim/chase-distance-m)") will return -25.0 or
similar, depending on the aircraft.
When this function is used, it doesn't install a listener for the top-level
node, instead it always calls <top-level-node>->getDoubleValue().
*/
double getDoubleValue(const char* spec, double default_=0);
/* Outputs detailed information about all specs that have been seen.
E.g.:
SG_LOG(SG_VIEW, SG_DEBUG, "ViewPropertyEvaluator:\n" << ViewPropertyEvaluator::Dump());
*/
struct Dump {};
std::ostream& operator << (std::ostream& out, const Dump& dump);
/* Clears all internal state. */
void clear();
}