Add support for Backtraces and profiling#1550
Conversation
Each call to CqlEngine.evaluate causes a call to State.init which, before this change, pushed one window. The actual evaluation associated with the CqlEngine.evaluate call pushed and popped a balanced number of windows but the one pushed by State.init remained on the stack in the end. Successive CqlEngine.evaluate calls would therefore accumulate an unbounded number of windows on the stack. As a slight interface change, clients that bypass CqlEngine.evaluate[Expressions] by using CqlEngine.getEvaluationVisitor now have to call State.beginEvaluation and State.endEvaluation themselves. The ExpressionCacheTest.expression_cache test had to be change in this way.
This makes the association between local variables introduced by, say, query expressions and the containing expression definition clear. For function definitions, there is no change since function invocation already pushed a fresh window.
baa0b81 to
23b902f
Compare
The stack of ActivationFrames serves the same purpose as the window stack but records additional information compared to just groups of local variables: * Each ActivationFrame (except the dummy root frame) has an associated element which is an ExpressionDef (possibly a FunctionDef) or a Retrieve. * Each ActivationFrame has a start time and an end time so that the time spent evaluating the frame can be tracked. * Each ActivationFrame has an isCached field so that the cache hits and misses when evaluating ExpressionDef elements (but not FunctionDef elements) can be tracked. This additional information will be useful for at least two purposes: 1. The associated elements can be used to construct a backtrace with source information when an exception is thrown during evaluation. 2. The timing and cache hit information can be used to build a deterministic profile of a given evaluation which is useful for performance analysis. Subsequent commits will add code for these two use-cases.
A backtrace represents a stack of call-like frames from the root of an evaluation to a particular sub-expression, commonly a sub-expression in which an exception was thrown. Client code can extract this Backtrace object from a caught exception and use it to display a backtrace similar to those known from other programming languages.
Before this change, there were two performance issues in the profile construction code 1. Profile.register traversed the whole stack to find the appropriate profile node which is O(N) where N is the depth of the "call stack". 2. Children of profile nodes were stored in a HashMap the keys of which relied on object-equality involving Element objects. Object-equality for those is very expensive. This change solves issue 1 by remembering the stack of profile nodes that correspond to the stack of current activation frames and incrementally updating the top of the stack which is O(1). This change solves issue 2 by (basically; the context adds a slight complication) storing the children of profile nodes in an IdentityHashMap so that Element.equals is not used.
23b902f to
5e89f72
Compare
|
This is very cool, thank you @scymtym ! A couple of enhancements that I would suggest:
As this PR goes through the paces I would also like to ensure the following take place:
With thorough testing and proper documentation, these features will be valuable additions to the project. Thanks again! |
|
@c-schuler Thank you for your feedback and suggestions. I can address some of the suggested changes by myself but will need more guidance for others.
I can see this as a useful distinction when presenting the results of profiling - the flamegraph-SVG renderer included in the pull request is mainly a demo of one particular result presentation style that works without dependencies and is not too complex (and happened to be sufficient for my use-case). One question could be whether this renderer should be omitted from the pull request so that clients have complete control and responsibility for the presentation of profiling results. None of this has any bearing on runtime performance, of course. Regarding runtime behavior and the level of activation frames, I'm not sure what a summary level or other finer grained options could look like. The only information in activation frames that is specific to profiling are the start and end times of the respective evaluation and the cache hit/miss bit. The impact on execution time is (presumably) very minor compared to the profiling work of building the call tree and cumulative timings which are controlled by the singular engine option I added. A related topic is coverage tracking. I remember seeing something about coverage tracking in the codebase but I can't remember where now. Since a flat profile (just call counts, no nesting) also provides coverage tracking, some integration work may be required in that area to avoid multiple mechanisms for the same functionality.
Since the stack of activation frames replaces the previous stack of windows, activation frames themselves are not optional. For clients that can tolerate worse backtraces, there could be an option to only push activation frames when absolutely necessary and only with the absolutely necessary information. I doubt the impact will be large, possibly to the point that querying the engine options frequently would negate the savings, but that would have to be measured. Also, most of the work for backtraces is done only after an error has already occurred by executing code in
I think I can do that. The documentation would mainly explain how get the backtrace and profiling information, not how to present them. Something like the backtrace in the above screenshots needs some work on the client's part. The flamegraph in the screenshot can be produced with just the code in this pull request - provided we don't decide against having the rendering code in the
Such an option exists for profiling. Activation frames cannot be turned off completely since their main purpose, holding variable bindings, is not optional. Reducing the number of activation frames could be an option, as I mentioned above, if it turns out to have a noticeable performance impact.
I would need help with this one since I don't have access to any relevant production environments.
Since the existing tests exercise those edge cases and activation frames are used throughout, the main missing piece would be running the existing tests with profiling enabled. Any suggestions regarding how I could do that?
I can add test cases that validate the information in backtraces. |
|
@c-schuler With the most recent commits, the points you brought up should be mostly addressed:
I haven't pushed the change that allows |
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/exception/Backtrace.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/exception/Backtrace.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/exception/Backtrace.java
Outdated
Show resolved
Hide resolved
Reported-by: Anton Vasetenkov <antvaset@gmail.com>
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java
Outdated
Show resolved
Hide resolved
...java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/exception/Backtrace.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/exception/Backtrace.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.java
Outdated
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/Profile.java
Show resolved
Hide resolved
Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java
Show resolved
Hide resolved
Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/ProfilingTest.java
Outdated
Show resolved
Hide resolved
Suggested-by: Anton Vasetenkov <antvaset@gmail.com>
Suggested-by: Anton Vasetenkov <antvaset@gmail.com>
Suggested-by: Anton Vasetenkov <antvaset@gmail.com>
The language level was changed in commit b55dc7b. Suggested-by: Anton Vasetenkov <antvaset@gmail.com>
|
Hi @scymtym , this is so cool and very well done! And thanks for merging in my suggested changes. Agree with @c-schuler that more toggles will be needed to control future profiling features like verbosity and granularity. In terms of what those toggles could look like, this TODO is captured by #1287 and will be addressed separately and perhaps more holistically. We don't currently have a README for CQL engine options in this repo (only code comments), and I can do that separately as well for all engine options (see new issue #1574). |
|
@antvaset Thank you for the review. I agree that some way of controlling the profiling feature, that is capturing profiling info as well as processing the finished profile, in more detail will be needed. The ad-hoc flamegraph-SVG writer I included is more a demo than anything. Whether those fleshed out details should take the form of engine options, I'm not really sure but that can be worked later as you suggest. |
c-schuler
left a comment
There was a problem hiding this comment.
Looks great! Thank you!
This is a draft for extending the "stack of windows" concept of the
CqlEngineto a stack ofActivationFrames which track more information than just arguments and local variables, namelyExpression/function definitions being evaluated in particular activation frames
Start and end times for the evaluation for activation frames
Cache hit information for certain activation frames
On top of that extension, this pull request adds two features to the
CQLEngineBacktraces in CQL exceptions that represent runtime error. The backtrace information can be used by clients to display backtraces with expression/function names, arguments, local variables and source information. Example:

Optional profiling. A new engine option can be used to turn enable profiling which the engine to aggregate information from

ActivationFrames into a deterministic performance profile that can be used, for example, to render a flamegraph. Example: