CalcEngine Plus

User-authored calculations for .NET

The Need for Speed

After reading through the basics of integrating the calc engine, eagle-eyed and/or skeptical readers might at this point be thinking “that’s fine for a mickey-mouse, hello-world example, but my application will have ENORMOUS numbers of calculations, with SIGNIFICANT LATENCIES every time I fetch a tag value from the underlying store”.

That’s a pretty legitimate concern. The “GetNumericValue” method in your helper class is called every time an operand’s “Result” property is accessed – usually multiple times in fact, once for each external tag. So if an application is not structured well, there a real potential for it to be slow, and a real danger of thrashing the underlying data store.

We can’t pretend that the latencies don’t exist. But we can minimise the number of times we make those expensive calls to the underlying system. In fact, we need make only one!

The trick is to separate out (i) the parsing of the tags, (ii) the retrieval of the required data (in one hit) and (iii) the evaluation of the results. In your app, these should be three separate steps:

Step 1: Parsing ALL the tags

Applications that require a calculation engine usually need to parse and evaluate a bunch of expressions all at once. Maybe there’s a collection of calculations on a particular page. Let’s create a list of three expressions to simulate that:


    var expressions = new List { "distance/time", "force*distance", "work/time" };

Note that there are some recurring tagnames in the above. Only four unique tags are required to service those three expressions.

Wouldn’t it be nice if we could retrieve those four values in one go? Of course we can. We’ll need to instantiate our ITagValueService class separately from the Parser, as it will need to perform double duties in this case.


    var tagValueServer = new TagValueServer();
    var parser = new Parser(tagValueServer);

The key thing is, when you parse an expression with the Parser...


    var tmpOperand = parser.Parse(expression);

...the resultant Operand has a very handy property called RequiredTags, that gives you all the tagnames required by that expression. That is true even if the expression contains nested expressions (see link). The RequiredTags property returns a HashSet<string>, which gives us a great way of gathering up all the tags required to evaluate a bunch of expressions, with no duplicates:


        var requiredTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        // Build up a collection of Operands; one for each parsed expression
        var operandList = new List<Operand>();
        foreach (var expression in expressions)
        {
            var tmpOperand = parser.Parse(expression);

            // The RequiredTags property tells us all the tagnames needed to evaluate a Result
            requiredTags.UnionWith(tmpOperand.RequiredTags);

            operandList.Add(tmpOperand);
        }

 

At the end of this, there will be an Operand for every expression parsed.

 

Step 2: Retrieving all the required values

After merging the RequiredTags from all of our expressions we will have a HashSet called requiredTags that contains all the tagnames we need to evaluate those three expressions.

For efficiency's sake, we want to get all those values in one hit, and cache them inside our TagValueServer. That way, we can implement the GetNumericValue method of our TagValueServer so that it really won't have much work to do. It can just serve up the values from the internal cache and everything will be really fast.

So we’d better make sure that all of our source values are present before we evaluate any operands:


    tagValueServer.RetrieveAllValuesFromSomeExternalDatabase(requiredTags);

In the example, the above method represents some kind of mechanism that fetches values from the underlying data source. It has to be a method on your ITagValueService class.

It would not clarify anything to display the details of this method here; it only matters that the method caches a value for all the tags required by the original expressions (in the sample code, the 4 required values are cached in a private Dictionary called “valuecache”).

Note that you will almost always be doing this as a bulk fetch. So even if your external database is slow, or there are high latencies, at least you’re only hitting it once - not in a loop.

Step 3: Accessing the Results

Now that the external results are cached in your ITagValueService class, we can access the Result property of any of the Operands. Each time we do, the expressions will be evaluated using the underlying values cached in the TagValueServer:


        foreach (var operand in operandList)
        {
            Console.WriteLine("{0} = {1}", operand.OriginalExpression, operand.Result);
        }

 

To reiterate: the “GetNumericValue” method of our TagValueServer is called every time an Operand’s Result property is accessed. The GetNumericValue method has been written to return previously fetched values from its cache:


        public double GetNumericValue(string tagname, out bool tagFound, out string errorMessage)
        {
            if (valueCache.ContainsKey(tagname))
            {
                errorMessage = string.Empty;
                tagFound = true;
                return valueCache[tagname];
            }

            tagFound = false;
            errorMessage = "tag '" + tagname + "' not found";
            return double.NaN;
        }

 

See the “EfficientAppDesign” download for where this example is worked through fully, with detailed comments.

Wrap up

We've taken quite some time here to step through a fairly simple idea: "Parse, Retrieve, Evaluate". But really, you're not doing much extra work on top of what you would have to do anyway, to retrieve values for your UI.

Tip: Generally, the parsing, retrieval and evaluation steps happen so quickly that there's no point dividing your application entities up into "simple things" and "calculated things" - just make everything a "calculated thing". The parsing overhead is negligible, and your user experience will be much simpler and more powerful.