Skip to content

Commit

Permalink
Issue #465 Update Node storage arch docs
Browse files Browse the repository at this point in the history
  • Loading branch information
prmr committed Jul 13, 2022
1 parent c36060c commit d9ae848
Show file tree
Hide file tree
Showing 5 changed files with 10 additions and 10 deletions.
16 changes: 8 additions & 8 deletions docs/functional/NodeStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@

## Scope

The Node Storage feature concerns how the bounds (bounding box) of nodes are stored during the execution of the `DiagramViewer#draw` method. This is a performance feature to avoid the inefficiency of continually re-computing the bounds of nodes during a single diagram rendering pass. The principle behind the Node Storage feature is that the bounding box for a node should only be computed _once for a given diagram rendering pass_. Because bounding boxes are used in many calculations for node and edge placement, it is necessary to cache the computed value to avoid repeatedly recomputing it.
_Node Storage_ concerns how the bounds (bounding box) of nodes are stored during the execution of the `DiagramRenderer#draw` method. This is a performance feature to avoid the inefficiency of continually re-computing the bounds of nodes during a single diagram rendering pass. The principle behind the Node Storage feature is that the bounding box for a node should only be computed _once for a given diagram rendering pass_. Because bounding boxes are used in many calculations for node and edge placement, it is necessary to cache the computed value to avoid repeatedly recomputing it.

## Overall Design

The feature is supported by a `NodeStorage` object that maps individual nodes to their corresponding bounds `Rectangle`. `NodeStorage` objects are managed by node viewers. In this way, a given node viewer, say `NoteNodeViewer`, will cache the bounds for all the `NoteNode`s.
The feature is supported by a `NodeStorage` object that maps individual nodes to their corresponding bounds `Rectangle`. `NodeStorage` objects are managed by node renderers. In this way, a given node renderer, say `NoteNodeRenderer`, will cache the bounds for all the `NoteNode`s of a given diagram.

The following class diagram illustrates how the design is realized with the illustration of a `NoteNode` (a node that represents a note on a UML diagram).

![JetUML Class Diagram](nodestorage1.class.png)

* The `NodeViewerRegistry` declares two methods to manage node caching. `activateNodeStorages()` prepares and activates the cache by going through all its viewers and activating _their_ cache. It should be called once before a diagram is drawn. `deactivateAndClearNodeStorages()` clears the cache by going through all the viewers and clearing _their_ cache. It should be called once after a diagram is drawn.
* Method `getBounds(Node)` of interface `NodeViewer` is implemented by a final method in `AbstractNodeViewer` that delegates the call to `getBounds` of class `NodeStorage`. This call takes as second argument a `Function<Node,Rectangle>` that is the _bounds calculator_. This function object is use to compute the bounds in case of a cache miss. The design of the bounds calculation mechanism is described below.
* The (Class)`DiagramRenderer` declares two methods to manage node caching. `activateNodeStorages()` prepares and activates the cache by going through all its viewers and activating _their_ cache. It should be called once before a diagram is drawn. `deactivateAndClearNodeStorages()` clears the cache by going through all the viewers and clearing _their_ cache. It should be called once after a diagram is drawn.
* For nodes, method `getBounds(DiagramElement)` of interface `DiagramElementRenderer` is implemented by a final method in `AbstractNodeRenderer` that delegates the call to `getBounds` of class `NodeStorage`. This call takes as second argument a `Function<Node,Rectangle>` that is the _bounds calculator_. This function object is use to compute the bounds in case of a cache miss. The design of the bounds calculation mechanism is described below.

The following sequence diagram illustrates a scenario where a diagram that contains a single `NoteNode` is drawn.

![JetUML Class Diagram](NodeStorage.sequence.png)

1. The top section shows the activation of the node storages (caches). What's important to note here is that in practice the `NodeViewerRegistry` aggregates many `NodeViewer` objects, so the call to `activateNodeStorage()` will be repeated once for each viewer in the registry.
2. The middle section shows how the call to `NodeViewerRegistry#draw` eventually reaches `NoteNodeViewer#getBounds`, because this information is used to draw the node. This results in a call to `NodeStorage#getBounds` where the second argument (`::internalGetBounds`) is a _reference to a instance method of a particular object_, namely method `internalGetBounds` of the `NoteNodeViewer` object.
1. The top section shows the activation of the node storages (caches). What's important to note here is that in practice the `DiagramRenderer` aggregates many `NodeVRenderer` objects, so the call to `activateNodeStorage()` will be repeated once for each element renderer managed by the diagram renderer.
2. The middle section shows how the call to `DiagramRenderer#draw` eventually reaches `NoteNodeRenderer#getBounds`, because this information is used to draw the node. This results in a call to `NodeStorage#getBounds` where the second argument (`::internalGetBounds`) is a _reference to a instance method of a particular object_, namely method `internalGetBounds` of the `NoteNodeRenderer` object.
3. Because there's a cache miss (the bounds of the note node were never previously computed), the bounds calculator `internalGetBounds` is called to compute and return the bounds.
4. The bottom section is very similar to the top section, except that now the node storages are cleared.

## Bounds Calculation Mechanism

The `NodeStorage` class is responsible for caching the bounds of different nodes, and computing these bounds for any node not in the cache. This computation requires specialized algorithms to compute the bounds of different nodes. To make it possible to reuse the `NodeStorage` class for all different types of nodes, its method `getBounds` takes as second argument a _bounds calculator_ function object of type `Function<Node, Rectangle>`. The concrete bounds calculators are implemented as protected methods of the `NodeViewer` subclasses that implement the abstract method `AbstractNodeViewer#internalGetBounds`. For example, in the class diagram above, `NoteNodeViewer` implements `internalGetBounds` with the code to compute the bounds of `NodeNodes`. With this design in place, the implementation of `AbstractViewer#getBounds` is greatly simplified as:
The `NodeStorage` class is responsible for caching the bounds of different nodes, and computing these bounds for any node not in the cache. This computation requires specialized algorithms to compute the bounds of different nodes. To make it possible to reuse the `NodeStorage` class for all different types of nodes, its method `getBounds` takes as second argument a _bounds calculator_ function object of type `Function<Node, Rectangle>`. The concrete bounds calculators are implemented as protected methods of the `NodeRenderer` subclasses that implement the abstract method `AbstractNodeRenderer#internalGetBounds`. For example, in the class diagram above, `NoteNodeRenderer` implements `internalGetBounds` with the code to compute the bounds of `NodeNode`s. With this design in place, the implementation of `AbstractNodeRenderer#getBounds` is greatly simplified as:

```java
@Override
Expand All @@ -36,4 +36,4 @@ public final Rectangle getBounds(Node pNode)
}
```

Here, the expression `this::internalGetBounds` is a reference to method `internalGetBounds` of the `NodeViewer` object upon which `getBounds` gets called. As illustrated in the sequence diagram, when there is a cache miss, this method will be the one called to compute the node's bounds.
Here, the expression `this::internalGetBounds` is a reference to method `internalGetBounds` of the `NodeRenderer` object upon which `getBounds` gets called. As illustrated in the sequence diagram, when there is a cache miss, this method will be the one called to compute the node's bounds.
2 changes: 1 addition & 1 deletion docs/functional/NodeStorage.sequence.jet
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"diagram":"SequenceDiagram","nodes":[{"x":0,"y":0,"openBottom":false,"id":14,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":6,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":7,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":13,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":12,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":9,"type":"CallNode"},{"children":[4],"name":"context:","x":40,"y":39,"id":3,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":20,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":4,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":19,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":1,"type":"CallNode"},{"children":[1,2],"name":":DiagramViewer","x":160,"y":69,"id":0,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":11,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":18,"type":"CallNode"},{"children":[11,12,13,14,15,16],"name":":NoteNodeViewer","x":580,"y":56,"id":10,"type":"ImplicitParameterNode"},{"children":[18,19,20],"name":":NodeStorage","x":880,"y":61,"id":17,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":2,"type":"CallNode"},{"children":[6,7,8,9],"name":":NodeViewerRegistry","x":360,"y":35,"id":5,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":15,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":8,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":16,"type":"CallNode"}],"edges":[{"middleLabel":"draw(diagram,...)","start":4,"end":1,"type":"CallEdge","signal":false},{"middleLabel":"activateNodeStorages()","start":1,"end":6,"type":"CallEdge","signal":false},{"middleLabel":"activateNodeStorage()","start":6,"end":11,"type":"CallEdge","signal":false},{"middleLabel":"activate()","start":11,"end":18,"type":"CallEdge","signal":false},{"middleLabel":"","start":18,"end":11,"type":"ReturnEdge"},{"middleLabel":"","start":11,"end":6,"type":"ReturnEdge"},{"middleLabel":"","start":6,"end":1,"type":"ReturnEdge"},{"middleLabel":"drawNode(noteNode)","start":1,"end":2,"type":"CallEdge","signal":false},{"middleLabel":"draw(noteNode)","start":2,"end":7,"type":"CallEdge","signal":false},{"middleLabel":"viewerFor(noteNode)","start":7,"end":8,"type":"CallEdge","signal":false},{"middleLabel":"draw(noteNode)","start":7,"end":12,"type":"CallEdge","signal":false},{"middleLabel":"createNotePath(noteNode)","start":12,"end":13,"type":"CallEdge","signal":false},{"middleLabel":"getBounds(noteNode)","start":13,"end":14,"type":"CallEdge","signal":false},{"middleLabel":"getBounds(noteNode, ::internalGetBounds)","start":14,"end":19,"type":"CallEdge","signal":false},{"middleLabel":"internalGetBounds(noteNode)","start":19,"end":15,"type":"CallEdge","signal":false},{"middleLabel":"","start":15,"end":19,"type":"ReturnEdge"},{"middleLabel":"","start":19,"end":14,"type":"ReturnEdge"},{"middleLabel":"","start":12,"end":7,"type":"ReturnEdge"},{"middleLabel":"","start":7,"end":2,"type":"ReturnEdge"},{"middleLabel":"deactivateAndClearNodeStorages()","start":1,"end":9,"type":"CallEdge","signal":false},{"middleLabel":"deactivateAndClearNodeStorage()","start":9,"end":16,"type":"CallEdge","signal":false},{"middleLabel":"deactivateAndClear()","start":16,"end":20,"type":"CallEdge","signal":false}],"version":"3.4"}
{"diagram":"SequenceDiagram","nodes":[{"x":0,"y":0,"openBottom":false,"id":13,"type":"CallNode"},{"children":[1],"name":"context:","x":40,"y":39,"id":0,"type":"ImplicitParameterNode"},{"children":[3,4,5,6],"name":":DiagramRenderer","x":160,"y":89,"id":2,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":12,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":9,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":10,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":5,"type":"CallNode"},{"x":4,"y":11,"openBottom":false,"id":3,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":1,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":6,"type":"CallNode"},{"children":[8,9,10,11,12,13],"name":":NoteNodeRenderer","x":400,"y":10,"id":7,"type":"ImplicitParameterNode"},{"x":0,"y":0,"openBottom":false,"id":8,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":17,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":15,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":11,"type":"CallNode"},{"children":[15,16,17],"name":":NodeStorage","x":700,"y":25,"id":14,"type":"ImplicitParameterNode"},{"x":-222,"y":10,"openBottom":false,"id":16,"type":"CallNode"},{"x":0,"y":0,"openBottom":false,"id":4,"type":"CallNode"}],"edges":[{"middleLabel":"draw(...)","start":1,"end":3,"type":"CallEdge","signal":false},{"middleLabel":"activateNodeStorages()","start":3,"end":5,"type":"CallEdge","signal":false},{"middleLabel":"drawNode(noteNode)","start":3,"end":4,"type":"CallEdge","signal":false},{"middleLabel":"activateNodeStorage()","start":5,"end":8,"type":"CallEdge","signal":false},{"middleLabel":"activate()","start":8,"end":15,"type":"CallEdge","signal":false},{"middleLabel":"","start":15,"end":8,"type":"ReturnEdge"},{"middleLabel":"","start":8,"end":5,"type":"ReturnEdge"},{"middleLabel":"draw(noteNode)","start":4,"end":9,"type":"CallEdge","signal":false},{"middleLabel":"createNotePath(noteNode)","start":9,"end":10,"type":"CallEdge","signal":false},{"middleLabel":"getBounds(noteNode)","start":10,"end":11,"type":"CallEdge","signal":false},{"middleLabel":"getBounds(noteNode, ::internalGetBounds)","start":11,"end":16,"type":"CallEdge","signal":false},{"middleLabel":"internalGetBounds(noteNode)","start":16,"end":12,"type":"CallEdge","signal":false},{"middleLabel":"","start":12,"end":16,"type":"ReturnEdge"},{"middleLabel":"","start":16,"end":11,"type":"ReturnEdge"},{"middleLabel":"","start":9,"end":4,"type":"ReturnEdge"},{"middleLabel":"deactivateAndClearNodeStorages()","start":3,"end":6,"type":"CallEdge","signal":false},{"middleLabel":"deactivateAndClearNodeStorage()","start":6,"end":13,"type":"CallEdge","signal":false},{"middleLabel":"","start":13,"end":6,"type":"ReturnEdge"},{"middleLabel":"deactivateAndClear()","start":13,"end":17,"type":"CallEdge","signal":false},{"middleLabel":"","start":17,"end":13,"type":"ReturnEdge"}],"version":"3.4"}
Binary file modified docs/functional/NodeStorage.sequence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/functional/nodestorage1.class.jet
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"diagram":"ClassDiagram","nodes":[{"methods":"getBounds(Node):Rectangle\ndraw(Node,GraphicsContext ):void\ncreateIcon(Node):Canvas \ndrawSelectionHandles(Node, GraphicsContext):void\ncontains(Node, Point):boolean\ngetConnectionPoint(Node, Direction):Point\nactivateNodeStorage():void\ndeactivateAndClearNodeStorage():void","name":"NodeViewer","x":620,"y":120,"id":4,"type":"InterfaceNode"},{"methods":"draw(Diagram, GraphicsContext):void","name":"DiagramViewer","x":310,"y":50,"attributes":"","id":3,"type":"ClassNode"},{"methods":"getBounds(Node, Function<Node, Rectangle>):Rectangle\nactivate():void\ndeactivateAndClear():void","name":"NodeStorage","x":260,"y":390,"attributes":"","id":2,"type":"ClassNode"},{"methods":"contains(Node, Point):boolean\ngetConnectionPoint(Node, Direction):Point\ndrawSelectionHandles(Node, GraphicsContext):void\ncreateIcon(Node):Canvas \nfinal getBounds(Node):Rectangle\nfinal activateNodeStorage():void\nfinal deactivateAndClearNodeStorage():void\n# abstract internalGetBounds(Node):Rectangle","name":"«abstract»\nAbstractNodeViewer","x":620,"y":340,"attributes":"","id":6,"type":"ClassNode"},{"methods":"draw(Node,GraphicsContext ):void\n# internalGetBounds(Node):Rectangle","name":"NoteNodeViewer","x":650,"y":570,"attributes":"","id":1,"type":"ClassNode"},{"methods":"static activateNodeStorages():void\nstatic deactivateAndClearNodeStorages():void\nstatic draw(Node,GraphicsContext ):void","name":"NodeViewerRegistry","x":290,"y":130,"attributes":"","id":5,"type":"ClassNode"},{"methods":"","name":"Node","x":620,"y":40,"id":0,"type":"InterfaceNode"}],"edges":[{"middleLabel":"","start":3,"directionality":"Unidirectional","end":5,"type":"DependencyEdge"},{"startLabel":"","middleLabel":"","start":5,"end":4,"endLabel":"*","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"startLabel":"","middleLabel":"","start":5,"end":0,"endLabel":"*","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"Generalization Type":"Implementation","start":6,"end":4,"type":"GeneralizationEdge"},{"startLabel":"","middleLabel":"","start":6,"end":2,"endLabel":"","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"Generalization Type":"Inheritance","start":1,"end":6,"type":"GeneralizationEdge"}],"version":"3.4"}
{"diagram":"ClassDiagram","nodes":[{"methods":"","name":"Diagram","x":1170,"y":120,"attributes":"","id":5,"type":"ClassNode"},{"methods":"draw(Node,GraphicsContext ):void\n# internalGetBounds(Node):Rectangle","name":"NoteNodeRenderer","x":940,"y":470,"attributes":"","id":6,"type":"ClassNode"},{"methods":"AbstractNodeRenderer(DiagramRenderer)\n# parent(): DiagramRenderer\ncreateIcon(DiagramType, DiagramElement):Canvas \ngetBounds(DiagramElement):Rectangle\ncontains(DiagramElement, Point):boolean\ndrawSelectionHandles(DiagramElement, GraphicsContext):void\ngetConnectionPoint(Node, Direction):Point\nactivateNodeStorage():void\ndeactivateAndClearNodeStorage():void","name":"«abstract»\nAbstractNodeRenderer","x":880,"y":240,"attributes":"","id":2,"type":"ClassNode"},{"methods":"createIcon(DiagramType, DiagramElement):Canvas \ngetBounds(DiagramElement):Rectangle\ncontains(DiagramElement, Point):boolean\ndraw(DiagramElement,GraphicsContext ):void\ndrawSelectionHandles(DiagramElement, GraphicsContext):void","name":"DiagramElementRenderer","x":500,"y":140,"id":1,"type":"InterfaceNode"},{"methods":"getConnectionPoint(Node, Direction):Point\nactivateNodeStorage():void\ndeactivateAndClearNodeStorage():void","name":"NodeRenderer","x":550,"y":300,"id":0,"type":"InterfaceNode"},{"methods":"+ draw(GraphicsContext):void\n# activateNodeStorages(): void\n# deactivateAndClearNodeStorages(): void","name":"ClassDiagramRenderer","x":890,"y":110,"attributes":"","id":4,"type":"ClassNode"},{"methods":"getBounds(Node, Function<Node, Rectangle>):Rectangle\nactivate():void\ndeactivateAndClear():void","name":"NodeStorage","x":490,"y":430,"attributes":"","id":3,"type":"ClassNode"}],"edges":[{"Generalization Type":"Implementation","start":2,"end":0,"type":"GeneralizationEdge"},{"startLabel":"","middleLabel":"","start":2,"end":3,"endLabel":"","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"Generalization Type":"Inheritance","start":6,"end":2,"type":"GeneralizationEdge"},{"startLabel":"","middleLabel":"","start":4,"end":1,"endLabel":"*","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"startLabel":"","middleLabel":"","start":4,"end":5,"endLabel":"1","type":"AggregationEdge","Aggregation Type":"Aggregation"},{"Generalization Type":"Inheritance","start":0,"end":1,"type":"GeneralizationEdge"}],"version":"3.4"}
Binary file modified docs/functional/nodestorage1.class.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d9ae848

Please sign in to comment.