Using Xtext for the first time is usually a very positive experience. Although Xtext is a complex generic framework, it is very easy to create your first Xtext-based editor, because of Xtext’s smart defaults and intuitive APIs. Even with minimal initial effort, the results are quite spectacular. Unfortunately the initial excitement often turns into disillusion as soon as you use your plugin on a big project.
Many development teams hit a performance wall as their plugin gets deployed and has to support larger projects. Internally, Xtext is a complex beast. The internals are carefully hidden from the user, but understanding them is critical to understand where the performance bottlenecks come from.
At Sigasi we have built commercial tool support for complex hardware description languages (VHDL, Verilog, SystemVerilog) using the Xtext framework. Our plugin needs to handle big industrial sized projects (>400k lines of code) that include large generated files (2 to 10 MB). To handle these kinds of projects we have developed a set of techniques over the last four years.
In this talk we will cover some performance critical pieces of the Xtext framework and evaluate what can be done to optimize it (think: parallel loading, caching, fast linking,…). We will also discuss some workarounds that can be used if nothing else works (light-weight editors, reducing the workload of the compiler).
6. The challenge
● Pre-specified languages
● Large projects
○ > 250 KLOC is not uncommon
○ design + external libraries
○ big files
■ some libraries are distributed as 1 file
■ generated hardware cores
●
7. Adopting Xtext
● Started with the early Xtext 2.0 snapshots
● Initial performance analysis
○ Clean build performance of a big project (330k LOC)
■ > 20 minutes
■ > 2 GB
○ Editing big files (> 1 MB)
■ unusable
8. Adopting Xtext
● Started with the early Xtext 2.0 snapshots
● Initial performance analysis
○ Clean build performance of a big project (330k LOC)
■ > 20 minutes → < 1 min
■ > 2 GB → ~ 1 GB memory
○ Editing big files (> 1 MB)
■ unusable → usable with reduced editor
11. Analyzing builds: builder overview
Global
indexing
Linking
Validation
Custom
Validation
Global
index
Eclipse
resources
warnings
errors
resource
descriptions
Builder
Participants
resource
changes
?
12. Analyzing builds: builder overview
Global
indexing
Global
index
resource
descriptions
resource
changes
● Usage
○ Location of exported declarations
○ Incremental compilation
● Implementation
○ IResourceDescriptionsStrategy
○ Default: all declarations
○ Customize!
○ Runs before linking!
● IResourceDescriptions &
IEObjectDescriptions
○ Always in memory
○ Persisted tot disk @ shutdown
13. Analyzing builds: builder overview
Linking
Validation
● Usage
○ Determine IScope all cross references
○ Link cross reference or create linking error
● Implementation
○ ILinkingService, IScopeProvider,
LazyLinkingResource
○ Requires global index for global scope
○ Direct link or link to global scope
○ Linking may trigger linking and resource
loading
linking
errors
Eclipse
resources
14. Analyzing builds: builder overview
● Usage
○ Execute all custom validations
○ Creates errors / warnings
● Implementation
○ AbstractDeclarativeValidator
○ Execute validations using reflection
○ Works against linked model
○ May trigger linking & resource loading
Linking
Validation
errors
warnings
Eclipse
resources
15. Analyzing builds: builder overview
Global
indexing
Linking
Validation
Custom
Validation
Global
index
Eclipse
resources
warnings
errors
resource
descriptions
Builder
Participants
resource
changes
?
● iterations ?
● order ?
16. Analyzing builds: metrics
● For each build
○ # of files being build
○ timing: Global index, Linking, Validation, Individual
builder participants
● Instrument by overriding
ClusteringBuilderState & XtextBuilder
● Example: Building 134 resources, timing: {
global index=1806,
linking=378,
validation=823,
totalLinkingAndValidation=1364
}
17. Analyzing builds: resource loads
● Observation:
○ Most time spent in resource loads
○ Certain files are loaded multiple times?!
● Solutions
○ Reduce memory pressure
○ Make loading faster
Global
indexing
Linking
validation
Custom
Validation
Builder
Participants
resources
LOAD
POTENTIAL
RELOADS
POTENTIAL
RELOADS
19. Memory pressure: EMF models
Reduce EMF size
○ Watch out for inferred model
http://www.sigasi.com/content/view-complexity-your-xtext-ecore-model
○ Avoid
■ Emf classes with just one list of things
● ListOfThings : ‘(‘ things+=Thing (‘,’ things+=Thing)* ‘)’
● class ListOfThings { contains Thing[] things }
■ Often unused fields
○ Code duplication in grammar vs efficient model
○ Fine-grained control with Xcore model
20. Memory pressure: Global index
In YourResourceDescriptionStrategy
○ Export foo, foo.rec, foo.rec.field1, foo.rec.field2
○ Add user-data: someType & anotherType
○ To reduce memory usage: don’t export child elements
■ export foo.rec + hash of fields
■ export foo + hash of contents of foo
■ can’t link these elements without loading anymore
package foo is
record rec is
field1 : someType;
field2 : anotherType(X downto Y);
end;
end;
21. Optimize loading
● What is resource load?
○ Parse
○ build EMF model & install EMF proxies
○ build Node model
22. Optimize loading
● What is resource load?
○ Parse
○ build EMF model & install EMF proxies
○ build Node model
● Parallelise
○ parse multiple files simultaneously
○ ~3 time faster loads on 4 core machine
○ only loading, not linking
23. Optimize loading
● What is resource load?
○ Parse
○ build EMF model & install EMF proxies
○ build Node model
● Parallelise
○ parse multiple files simultaneously
○ ~3 time faster loads on 4 core machine
○ only loading, not linking
● Cache
○ serialize EMF and Node model in a cache
○ originally 3-4 time faster loads
○ now 1.5x (no backtracking, simplified grammar)
27. VHDL linking in depth
History of VHDL linking
● 1st version
○ AbstractDeclarativeScopeProvider
○ best effort scoping
● 2nd version
○ removed reflection
● 3rd version
○ special rule-based internal java dsl
○ first attempt to be 100% correct
● 4rth version
○ batch/eager linking
○ type errors
28. VHDL linking in depth
foo(baz) <= bar(bak.f(“?”), (‘1’, 2))) + 1;
● Most elements in an expression are overloaded
○ subprograms, literals, enumliterals
● foo(baz)
○ 9 kinds of meanings
○ 4 of them can have subprogram overloading
● overloading includes return type
● overload resolution is very hard
○ you have to find 1 unambiguous solution
○ resolve all cross-references together
29. VHDL linking in depth
Xtext lazy linking
● good
○ declarative: only a few rules
○ fine-grained: can be good for performance
○ re-use: scoping, auto-complete, serialisation
● bad
○ hard to debug
■ can call itself
■ lots of caching
■ indirection, huge stack traces
○ performance
■ build context for every cross-reference
30. VHDL linking in depth
Batch/Eager linking
● good
○ simple top-down algorithm
○ natural fit for vhdl
○ well described in literature
● bad
○ resolve 1 reference = resolve all references
○ a lot of extra xtext customisation
■ auto-complete & serialization?
■ linking errors?
31. VHDL linking in depth
Our hybrid approach
● Eager/batch linking of design units
○ Big files are partially scoped
○ Parent-scope of a design unit is the global scope
○ Local scoping is executed eagerly
● Global scope
○ Import declarations of other design units
○ Only query is find design unit x.y
■ load resource of x.y
■ create an ‘ExternalScope’ & cache it
● Always load dependent resources
○ needed for validation, hovers, highlighting anyway
32. Vhdl linking in depth
Conclusion
● Easier to implement, debug and optimize
● Type error reporting during linking
● Memory intensive?
○ Every dependency is loaded
○ OK in practice for VHDL
● A lot of xtext customisation!
○ A lot of classes are affected
○ Forward compatible?
33. UI responsiveness
● Measuring: detect a blocked UI thread
○ initially Svelto https://github.com/dragos/svelto
○ now our own method & logging
○ Eclipse Mars
● Improvements
○ UI is for drawing only!
○ Make sure everything is cancellable
● Safeguards
○ certain services should never be executed on the UI
thread => check & log
38. Come talk to us about...
● Documentation generation
● Fancy linking algorithms / type systems
● Graphical views
● Cross-language support
● Testing Xtext-plugins
● Lexical macros
● Manage large amount of validations
● ...
Editor's Notes
Overall structure of the presentation
Quickly introduce Sigasi & it’s relationship with Xtext
Explain the requirements of our clients, their projects, their expectations of our product
Break down the scalability problems that we had to face
Explain the problems faced with build performance & responsive UIs
Build performance
Start with grammar & model => mostly memory considerations
Explain a build in the ‘builder’ and in the editor
Focus on resource loads
Build performance breakdown: global index, linking, validation, builder participants => introduce metrics?
Evaluate global index phase
Evaluate linking phase
Introduce us:
3500 active users
We’re 2 engineers from sigasi
We’re a company that builds a plugin for VHDL & starting (System)Verilog, hardware design languages, these languages are standardized by IEEE and are the industry standard for designing digital hardware.
We’ve mainly focused on VHDL, but we’re starting to improve our Verilog support
We started using Xtext 4 years ago (just this, we revisit this decision later)
We have a lot of users, worldwide
<add screenshot with code>
to discuss
user definable types
hover, semantic highlighting
complex expressions
hierarchy view
Assist hardware designer:
HW design is very complex. Assist HW designer as good as possible so that she can focus on the import aspects.
The core of our product is the compiler, written with Xtext
We’re only a front-end compler => we don’t do code generation, design simulation or synthesis
We use it to… [see points]
Our product gets used on some existing project by our clients.
Vhdl is old
files have a compilation order (we disregard this)
huge spec (800p); can be used as a general purpose language, many additions over the years & many constructs are never used
extra verbose language => loads of opportunities for improving the experience with an IDE
Projects are
messy: source and binary artefacts are sometimes mixed, source folders contain old versions, many files with compilation errors
everything is distributed as source: even huge libraries, that we will have to compile, can’t see the difference with user code
huge files: see slide
emacs & notepad++:
they aren’t used to making clean projects that IDEs can consume
they don’t accept slow editors actions, or an editor that suddenly blocks until a compilation has finished
used to simply being able to open a file, free of context
In 2011 Sigasi had a difficult choice to make, improve our old plugin basically a proof of concept or adopt Xtext.
In a sort-of bet-the-company move we chose xtext. Why? see an older presentation by Hendrik.
After some quick experimentation we chose to use Xtext. Although we knew that there were major performance problems we were confident that we could overcome them. Some of this was with the excellent support of Itemis.
In 2011 Sigasi had a difficult choice to make, improve our old plugin basically a proof of concept or adopt Xtext.
In a sort-of bet-the-company move we chose xtext. Why? see an older presentation by Hendrik.
After some quick experimentation we chose to use Xtext. Although we knew that there were major performance problems we were confident that we could overcome them. Some of this was with the excellent support of Itemis.
How did we improve?
do nothing => wait for xtext framework improvements
we did some contributions, we’re happy with the cooperation
every release some improvements => don’t forget to read the release notes
typical improvement cycle
what to measure & how to measure?
learn xtext performance characteristics
then you try to improve or try to find a way to circumvent the issue or cheat
This is what happens when you trigger a clean/incremental build
We start with a set of resources.
We make an index of the resources in the global index phase
why? it’s impossible to keep the entire compilation graph in memory
the global index will help us find where certain elements are located without having to load those files
all files are processed, each file gets an entry in the global index
all data goes into a global index, this index stays in memory (at startup/shutdown it is persisted to the disk)
During linking:
all crossreferences are resolved or not in that case there is a linking error placed on the eclipse resource
validation follows directly after linking
Builder participants (done)
they do whatever they want
Understand where time is spent
This is a gross simplification of the build pipeline
GI -> L&V -> Part
GI: ResourceDescriptionStrategy
absolutely no linking here
walk parsed, unlinked AST, using ‘get*’ on a cross-reference will cause linking!!
used to find global names and to determine if the public interface exposed by a file has changed
linking: IScopeProvider => link cross-references, if it can’t be linked => create link-error
Validation: Abstract*Validator: create errors & warnings
Builder participant: can do virtually anything, there are ParallelBuilderParticipants
Memory consumption profile is basically your ResourceSet + the global index
fat arrow: => passing a resource set
normally everything is loaded once in the GI phase
loading means parsing, building the node model and building the emf ast => it’s expensive!
if you run out of memory => the resource set gets cleared & a lot of files will need to be reloaded
running out of memory is bad m’kay
in principle, once the GI is made, you can link & validate 1 resource with an empty resource set
small arrow: passing a resource => linking passes linked resource to validator
Performance profile
GI: first ast walk: minimize what you want to do in the global indexing phase (you normally don’t have to walk the entire model)
linking: second ast walk: depends on your scoping complexity
validation: 3rd ast walk: Your validation budget is pretty big (linking & GI is expensive) as long as the analysis is mainly local & dependent resources
avoid throwing uncaught exceptions (mostly NPEs), they are expensive & always caught behind the scenes
This is what happens when you trigger a clean/incremental build
We start with a set of resources.
We make an index of the resources in the global index phase
why? it’s impossible to keep the entire compilation graph in memory
the global index will help us find where certain elements are located without having to load those files
all files are processed, each file gets an entry in the global index
all data goes into a global index, this index stays in memory (at startup/shutdown it is persisted to the disk)
During linking:
all crossreferences are resolved or not in that case there is a linking error placed on the eclipse resource
validation follows directly after linking
Builder participants (done)
they do whatever they want
Understand where time is spent
This is a gross simplification of the build pipeline
GI -> L&V -> Part
GI: ResourceDescriptionStrategy
absolutely no linking here
walk parsed, unlinked AST, using ‘get*’ on a cross-reference will cause linking!!
used to find global names and to determine if the public interface exposed by a file has changed
linking: IScopeProvider => link cross-references, if it can’t be linked => create link-error
Validation: Abstract*Validator: create errors & warnings
Builder participant: can do virtually anything, there are ParallelBuilderParticipants
Memory consumption profile is basically your ResourceSet + the global index
fat arrow: => passing a resource set
normally everything is loaded once in the GI phase
loading means parsing, building the node model and building the emf ast => it’s expensive!
if you run out of memory => the resource set gets cleared & a lot of files will need to be reloaded
running out of memory is bad m’kay
in principle, once the GI is made, you can link & validate 1 resource with an empty resource set
small arrow: passing a resource => linking passes linked resource to validator
Performance profile
GI: first ast walk: minimize what you want to do in the global indexing phase (you normally don’t have to walk the entire model)
linking: second ast walk: depends on your scoping complexity
validation: 3rd ast walk: Your validation budget is pretty big (linking & GI is expensive) as long as the analysis is mainly local & dependent resources
avoid throwing uncaught exceptions (mostly NPEs), they are expensive & always caught behind the scenes
This is what happens when you trigger a clean/incremental build
We start with a set of resources.
We make an index of the resources in the global index phase
why? it’s impossible to keep the entire compilation graph in memory
the global index will help us find where certain elements are located without having to load those files
all files are processed, each file gets an entry in the global index
all data goes into a global index, this index stays in memory (at startup/shutdown it is persisted to the disk)
During linking:
all crossreferences are resolved or not in that case there is a linking error placed on the eclipse resource
validation follows directly after linking
Builder participants (done)
they do whatever they want
Understand where time is spent
This is a gross simplification of the build pipeline
GI -> L&V -> Part
GI: ResourceDescriptionStrategy
absolutely no linking here
walk parsed, unlinked AST, using ‘get*’ on a cross-reference will cause linking!!
used to find global names and to determine if the public interface exposed by a file has changed
linking: IScopeProvider => link cross-references, if it can’t be linked => create link-error
Validation: Abstract*Validator: create errors & warnings
Builder participant: can do virtually anything, there are ParallelBuilderParticipants
Memory consumption profile is basically your ResourceSet + the global index
fat arrow: => passing a resource set
normally everything is loaded once in the GI phase
loading means parsing, building the node model and building the emf ast => it’s expensive!
if you run out of memory => the resource set gets cleared & a lot of files will need to be reloaded
running out of memory is bad m’kay
in principle, once the GI is made, you can link & validate 1 resource with an empty resource set
small arrow: passing a resource => linking passes linked resource to validator
Performance profile
GI: first ast walk: minimize what you want to do in the global indexing phase (you normally don’t have to walk the entire model)
linking: second ast walk: depends on your scoping complexity
validation: 3rd ast walk: Your validation budget is pretty big (linking & GI is expensive) as long as the analysis is mainly local & dependent resources
avoid throwing uncaught exceptions (mostly NPEs), they are expensive & always caught behind the scenes
This is what happens when you trigger a clean/incremental build
We start with a set of resources.
We make an index of the resources in the global index phase
why? it’s impossible to keep the entire compilation graph in memory
the global index will help us find where certain elements are located without having to load those files
all files are processed, each file gets an entry in the global index
all data goes into a global index, this index stays in memory (at startup/shutdown it is persisted to the disk)
During linking:
all crossreferences are resolved or not in that case there is a linking error placed on the eclipse resource
validation follows directly after linking
Builder participants (done)
they do whatever they want
Understand where time is spent
This is a gross simplification of the build pipeline
GI -> L&V -> Part
GI: ResourceDescriptionStrategy
absolutely no linking here
walk parsed, unlinked AST, using ‘get*’ on a cross-reference will cause linking!!
used to find global names and to determine if the public interface exposed by a file has changed
linking: IScopeProvider => link cross-references, if it can’t be linked => create link-error
Validation: Abstract*Validator: create errors & warnings
Builder participant: can do virtually anything, there are ParallelBuilderParticipants
Memory consumption profile is basically your ResourceSet + the global index
fat arrow: => passing a resource set
normally everything is loaded once in the GI phase
loading means parsing, building the node model and building the emf ast => it’s expensive!
if you run out of memory => the resource set gets cleared & a lot of files will need to be reloaded
running out of memory is bad m’kay
in principle, once the GI is made, you can link & validate 1 resource with an empty resource set
small arrow: passing a resource => linking passes linked resource to validator
Performance profile
GI: first ast walk: minimize what you want to do in the global indexing phase (you normally don’t have to walk the entire model)
linking: second ast walk: depends on your scoping complexity
validation: 3rd ast walk: Your validation budget is pretty big (linking & GI is expensive) as long as the analysis is mainly local & dependent resources
avoid throwing uncaught exceptions (mostly NPEs), they are expensive & always caught behind the scenes
This is what happens when you trigger a clean/incremental build
We start with a set of resources.
We make an index of the resources in the global index phase
why? it’s impossible to keep the entire compilation graph in memory
the global index will help us find where certain elements are located without having to load those files
all files are processed, each file gets an entry in the global index
all data goes into a global index, this index stays in memory (at startup/shutdown it is persisted to the disk)
During linking:
all crossreferences are resolved or not in that case there is a linking error placed on the eclipse resource
validation follows directly after linking
Builder participants (done)
they do whatever they want
Understand where time is spent
This is a gross simplification of the build pipeline
GI -> L&V -> Part
GI: ResourceDescriptionStrategy
absolutely no linking here
walk parsed, unlinked AST, using ‘get*’ on a cross-reference will cause linking!!
used to find global names and to determine if the public interface exposed by a file has changed
linking: IScopeProvider => link cross-references, if it can’t be linked => create link-error
Validation: Abstract*Validator: create errors & warnings
Builder participant: can do virtually anything, there are ParallelBuilderParticipants
Memory consumption profile is basically your ResourceSet + the global index
fat arrow: => passing a resource set
normally everything is loaded once in the GI phase
loading means parsing, building the node model and building the emf ast => it’s expensive!
if you run out of memory => the resource set gets cleared & a lot of files will need to be reloaded
running out of memory is bad m’kay
in principle, once the GI is made, you can link & validate 1 resource with an empty resource set
small arrow: passing a resource => linking passes linked resource to validator
Performance profile
GI: first ast walk: minimize what you want to do in the global indexing phase (you normally don’t have to walk the entire model)
linking: second ast walk: depends on your scoping complexity
validation: 3rd ast walk: Your validation budget is pretty big (linking & GI is expensive) as long as the analysis is mainly local & dependent resources
avoid throwing uncaught exceptions (mostly NPEs), they are expensive & always caught behind the scenes
Now we’re going to look at EMF resource loading
if your memory is big enough
all resources loaded in global indexing phase
in this phase no linking should be done
pass RS to linking & validation
in linking phase everything gets linked
validation works with an already linked model
pass RS to builder participants
if it isn’t
when there is too much memory pressure the clusteringbuilder will clear the resourceset
may cause reloads during linking
may cause relinking of reloaded dependencies in validation & builder participants
you can hash subelements
Note: even with incomplete linking, you get OK Xtext support for rename, refactor, …
-> improve incrementally.
Nightly -> see progress, regressions
log also during usage -> detect unexpected use cases
cancellable => some xtext classes like outline have been retrofitted for this