Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Papyrus/Oxygen Work Description/NewFeature/ModelIndexer

UML Model Indexing with VIATRA

In current versions of Papyrus (and MDT UML), to speed up access to UML models, including support for backwards navigation or derived features, a CacheAdapter instance is used. A lazy caching approach is followed: the cache start empty, and calculated values are stored inside the CacheAdapter. The cache is cleaned in case of the containing resource changes.

There are four further ways to provide better caching:

  1. Incrementality: the current CacheAdapter uses a very simple cache invalidation
  2. Eager caching: precompute all values to be stored in the CacheAdapter. If it can be expected that almost all values are necessary, this could
  3. Notifications: if a cached value changes, it might require UI updates; however, for that notifications are required.
  4. More general storage: the CacheAdapter only allows caching EMF Features. In general, other elements might be also interesting to cache, including notifications. E.g. lists, trees and tables often display a high-level view of the model that could also be cached incrementally.

In the following it will be introduced how VIATRA can help to express these tasks (including references to Gerrit changes for Papyrus).

Overview of VIATRA Query

The VIATRA Query framework allows the definition and efficient executions of model queries, especially focusing on incremental evaluation: precalculating the results of all queries, then updating them in case the underlying model changes.

There are two important components of VIATRA Query to know: (1) the Base indexer is responsible for caching model elements and references, while (2) the Query engine is responsible for calculating the results of more complex queries (but relies on Base for the elementary functionalities).

The Base Indexer behaves similar to CacheAdapter with a few notable differences:

  • The cache is eager: indexed elements are to be collected before the indexer can be used.
  • It is possible to manually define what instances and edges are to be indexed, e.g. it is possible to index all 'Class' instances and 'QualifiedName' references. By default, nothing is indexed; if the indexing rules change, the model has to be traversed.
  • The indexer can send change notifications on model changes, thus it is possible to listen to the appearances and updates of specific edges, etc.

Lazy caching of UML profile application with VIATRA Base

The changes for this indexer were introduced a set of Gerrit changes:

  • [1] defines a Papyrus model indexer service that starts with the service registry
  • [2] accesses the model indexer in the UMLProfileHelper utility class
  • [3] accesses the same matcher in SysML14ProfileMatcher

TODO describe lazy cache implementation in more details

Performance measurements

To measure the performance of the indexer, a specific benchmark code was created that loads and traverses a given UML model, and calculates for each model element the set of Profile EPackages that is assigned to it. This use case was extracted from use case 7 defined in Papyrus/Oxygen_Work_Description/Refactoring/PerformancesImprovements#Use_cases:.

In the test class ProfileHelperMeasurements the obfuscated UML model introduced in [4] was loaded then traversed using three strategies:

  1. No Indexer: the original code of UMLProfileHelper#getAppliedProfiles is called for each UML model element.
  2. Late Indexing: the VIATRA Base indexer service was initialized after the model was loaded, then this service was used to access the set of applied profiles.
  3. Indexing on Model Load: the VIATRA Base indexer was initialized before the model was loaded, then this service was used to access the set of applied profiles.

In the following, the third case is depicted; the other cases are similar (and can be looked at [5]).

//Start model indexer from the ServicesRegistry
//At this point, initialization is cheap as the ModelSet does not contain any model
registry.startServicesByClassKeys(UMLModelIndexerService.class);
//Loading model
Stopwatch loadTime = Stopwatch.createStarted();
ModelsReader modelsReader = new ModelsReader();
modelsReader.readModel(modelSet);
//The UMLModelIndexerService is also triggered by the loading of the model
modelSet.loadModels(URI.createPlatformPluginURI("org.eclipse.papyrus.uml.profile.measures/obfuscated.di", true));
loadTime.stop();
 
//Traverse loaded model
Stopwatch traverseTime = Stopwatch.createStarted();
TreeIterator<Notifier> it = modelSet.getAllContents();
UMLProfileHelper helper = new UMLProfileHelper();
while(it.hasNext()) {
	Notifier obj = it.next();
	if (obj instanceof EObject) {
		//UMLProfileHelper accesses the indexer service internally
		helper.getAppliedProfiles((EObject)obj);
	}
}
traverseTime.stop();

Each strategy was measured three times, and the average results are displayed below:

Load Time (ms) Indexing Time (ms) Traverse Time (ms)
No Indexer 6900 0 386
Late Indexing 6998 932 288
Indexing on Model Load 7203 0 286

It is visible that the eager indexing of VIATRA is expensive if done after model load (+0.9 seconds), but during model load it only results in only a slight slowdown (+0.2-0.3 seconds). On the other hand, traversing the model got faster (by 0.1 seconds). If the task is to be executed multiple times, the faster calculation of the result can result in faster response times in longer workflows.

Finding complex patterns in models using VIATRA Queries

Often a more complex set of model elements are to be displayed or found, e.g. two classes referring to each other, while both of them has a specified stereotype. Such patterns are impossible to cache using CacheAdapter, as there is no corresponding feature in the UML metamodel; while manually calculating the caches using VIATRA Base is possible but quite error-prone.

However, VIATRA Query provides a graph pattern based language to define such structures, and uses Rete networks to calculate and store these structures.

Such a pattern is described as follows in the language of VIATRA Query to find two blocks whose classes are connected via UML Properties.

//UML metamodel reference
import "http://www.eclipse.org/uml2/5.0.0/UML"
//SysML 1.4 profile metamodel reference
import "http://www.eclipse.org/papyrus/sysml/1.4/SysML/Blocks"
 
/* Helper patter to connect Block stereotypes to classes */
private pattern block(b : Block, c : Class) {
  Block.base_Class(b, c);
}
 
/* Helper pattern to connect two classes with a property */
private pattern connectedClass(c1 : Class, c2 : Class) {
  Class.feature(c1, a);
  Property.association.endType(a, c2);
  c1 != c2;
}
 
pattern connectedBlocks(b1 : Block, b2 : Block) {
  find block(b1, c1);
  find block(b2, c2);
  find connectedClass(c1, c2);
}

After the pattern is loaded to a VIATRA Query Engine, such patterns can be evaluated instantly, while both parameters can be bound or unbound separately, thus supporting four different use cases together: (1) enumeration of all instances, (2) forward navigation from b1 to b2, (3) backward navigation from b2 to b1 and (4) checking whether both parameters are connected.

Live well-formedness validation using VIATRA Queries

It is important to note that VIATRA Query also provides notifications when the match set of a pattern changes. This can be used to define reactive user interface elements, e.g. provide live model validation. For live validation, VIATRA includes a validation framework that creates error markers automatically based on information provided in annotations for graph patterns.

Example:

/* Helper pattern for calculating the superclass relation between two UML Classes */
private pattern superclass(superclass : Class, spec : Class) {
	Class.generalization(spec, gen);
	Generalization.general(gen, superclass);
}
 
/**
 * 8.3.2.4 Bound Reference [8] Any classifier that specializes a Block must also have the Block stereotype or one of its specializations applied.
 */
@Constraint(key = {specific},
  severity = "error",
  message = "The class $specific.name$ specialized Block $general.name$ but is missing the stereotype Block.",
  //This is only required to allow initializing the validation framework from the user interface; can be also started as a Papyrus service instead
  targetEditorId = "org.eclipse.papyrus.infra.core.papyrusEditor"
)
pattern checkBlockInheritance(generalBlock : Block, general : Class, specific : Class) {
  //Reusing helper pattern defined for the previous section
  find block(generalBlock, general);
  neg find block(_, specific);
  find superclass+(general, specific);
}

While at first this pattern seems a bit complicated, it is important to note that implementing the same pattern in Java is also non-trivial: quite a few for-loops and null- and instanceof checks are needed to express the same (e.g. see the very similar http://git.eclipse.org/c/papyrus/org.eclipse.papyrus-sysml.git/tree/core/org.eclipse.papyrus.sysml14.validation/src/org/eclipse/papyrus/sysml14/validation/rules/blocks/BlockSpecializationModelConstraint.java?h=committers/bmaggi/oxygen ; also note the use of helper methods, like UMLUtil.getStereotypeApplication). At first glance, both implementations are of similar complexity; however the pattern-based approach supports reusing in different aspects, and also provides change notifications required for live validation as well.

Important considerations

While using VIATRA can result in very effective incremental behavior, there are some things to consider:

  • The eager caching of VIATRA Base and VIATRA Query has some costs associated: opening a model with VIATRA requires traversing the model, and requires some memory to store the results. In general, if the cache is used only occasionally (e.g. only once, or just for a few selected model elements), it might be better to calculate the result on demand (without any caching), or cache only partial results (that might be reused for other tasks as well).
  • To reduce the number of required model traversals by VIATRA it is important to ensure that only a single Base Indexer is created each required type is declared at start for indexing. Similarly, if using queries it is recommended to initialize all queries in a 'prepare' call together.
  •  Derived features of the UML metamodel are not directly reusable in graph patterns, as they do not always provide the required notifications on model changes. For a large part of these features VIATRA provides support by implementing them as graph patterns.
  •  As of now (2016. december), the query language of VIATRA can only reference static profiles. The runtime is capable of working with dynamically applied profiles, but the query language needs to be extended to support this use case.

Copyright © Eclipse Foundation, Inc. All Rights Reserved.