Caches
You have parsed a document and you would like to execute some computation on the AST. But you don’t want to do this every time you see a certain node. You want to do it once for the lifetime of a document. Where to save it?
For data that depends on the lifetime of a document or even the entire workspace, Langium has several kinds of caches:
- the document cache saves key-value-pairs of given types
K
andV
for each document. If the document gets changed or deleted the cache gets cleared automatically for the single files - the workspace cache also saves key-value-pairs of given types
K
andV
, but gets cleared entirely when something in the workspace gets changed
Besides those specific caches, Langium also provides:
- a simple cache that can be used for any kind of key-value-data
- a context cache that stores a simple cache for each context object. The document cache and workspace cache are implemented using the context cache
Here we will use the HelloWorld
example from the learning section. Let’s keep it simple and just list people in a document, which will come from a comic book.
We will have a computation for each person that determines from which publisher it comes from.
Let’s build a “publisher inferer service”. First let’s create a small database of known publishers and known persons:
type KnownPublisher = 'DC' | 'Marvel' | 'Egmont';
const KnownPersonNames: Record<KnownPublisher, string[]> = {
DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'],
Marvel: ['Spiderman', 'Wolverine', 'Deadpool'],
Egmont: ['Asterix', 'Obelix']
};
For our service we define an interface:
export interface InferPublisherService {
inferPublisher(person: Person): KnownPublisher | undefined;
}
Now we implement the service:
class UncachedInferPublisherService implements InferPublisherService {
inferPublisher(person: Person): KnownPublisher | undefined {
for (const [publisher, persons] of Object.entries(KnownPersonNames)) {
if (persons.includes(person.name)) {
return publisher as KnownPublisher;
}
}
return undefined;
}
}
Now we want to cache the results of the inferPublisher
method. We can use the DocumentCache
for this. We will reuse the uncached service as base class and override the inferPublisher
method:
export class CachedInferPublisherService extends UncachedInferPublisherService {
private readonly cache: DocumentCache<Person, KnownPublisher | undefined>;
constructor(services: HelloWorldServices) {
super();
this.cache = new DocumentCache(services.shared);
}
override inferPublisher(person: Person): KnownPublisher | undefined {
const documentUri = AstUtils.getDocument(person).uri;
//get cache entry for the documentUri and the person
//if it does not exist, calculate the value and store it
return this.cache.get(documentUri, person, () => super.inferPublisher(person));
}
}
To use this service, let’s create a validator that checks if the publisher of a person is known. Go to the hello-world-validator.ts
file and add the following code:
import type { ValidationAcceptor, ValidationChecks } from 'langium';
import type { HelloWorldAstType, Person } from './generated/ast.js';
import type { HelloWorldServices } from './hello-world-module.js';
import { InferPublisherService } from './infer-publisher-service.js';
/**
* Register custom validation checks.
*/
export function registerValidationChecks(services: HelloWorldServices) {
const registry = services.validation.ValidationRegistry;
const validator = services.validation.HelloWorldValidator;
const checks: ValidationChecks<HelloWorldAstType> = {
Person: validator.checkPersonIsFromKnownPublisher
};
registry.register(checks, validator);
}
/**
* Implementation of custom validations.
*/
export class HelloWorldValidator {
private readonly inferPublisherService: InferPublisherService;
constructor(services: HelloWorldServices) {
this.inferPublisherService = services.utilities.inferPublisherService;
}
checkPersonIsFromKnownPublisher(person: Person, accept: ValidationAcceptor): void {
if (!this.inferPublisherService.inferPublisher(person)) {
accept('warning', `"${person.name}" is not from a known publisher.`, {
node: person
});
}
}
}
Finally, we need to register the service in the module. Go to the hello-world-module.ts
file and add the following code:
export type HelloWorldAddedServices = {
utilities: {
inferPublisherService: InferPublisherService
},
validation: {
HelloWorldValidator: HelloWorldValidator
}
}
//...
export const HelloWorldModule: Module<HelloWorldServices, PartialLangiumServices & HelloWorldAddedServices> = {
utilities: {
inferPublisherService: (services) => new CachedInferPublisherService(services)
},
validation: {
//add `services` parameter here
HelloWorldValidator: (services) => new HelloWorldValidator(services)
}
};
Start the extension and create a .hello
file with several persons, like this one:
person Wonderwoman
person Spiderman
person Homer //warning: unknown publisher!!
person Obelix
Caching can improve the performance of your language server. It is especially useful for computations that are expensive to calculate. The DocumentCache
and WorkspaceCache
are the most common caches to use. The ContextCache
is useful if you need to store data for a specific context object. If you only need a key-value store, you can use the SimpleCache
.
All of these caches are disposable compared to a simple Map<K, V>
. If you dispose them by calling dispose()
the entries will be removed and the memory will be freed. Plus, from the moment you have called dispose()
, the cache will not react to changes in the workspace anymore.
Full implementation
import { AstUtils, DocumentCache } from "langium";
import { Person } from "./generated/ast.js";
import { HelloWorldServices } from "./hello-world-module.js";
type KnownPublisher = 'DC' | 'Marvel' | 'Egmont';
const KnownPersonNames: Record<KnownPublisher, string[]> = {
DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'],
Marvel: ['Spiderman', 'Wolverine', 'Deadpool'],
Egmont: ['Asterix', 'Obelix']
};
export interface InferPublisherService {
inferPublisher(person: Person): KnownPublisher | undefined;
}
class UncachedInferPublisherService implements InferPublisherService {
inferPublisher(person: Person): KnownPublisher | undefined {
for (const [publisher, persons] of Object.entries(KnownPersonNames)) {
if (persons.includes(person.name)) {
return publisher as KnownPublisher;
}
}
return undefined;
}
}
export class CachedInferPublisherService extends UncachedInferPublisherService {
private readonly cache: DocumentCache<Person, KnownPublisher | undefined>;
constructor(services: HelloWorldServices) {
super();
this.cache = new DocumentCache(services.shared);
}
override inferPublisher(person: Person): KnownPublisher | undefined {
const documentUri = AstUtils.getDocument(person).uri;
return this.cache.get(documentUri, person, () => super.inferPublisher(person));
}
}