Skip to content

Commit 7f098d8

Browse files
Lotesmontymxb
andauthored
Recipe: Caches (#241)
Co-authored-by: Benjamin Friedman Wilson <[email protected]>
1 parent 4dfaa9b commit 7f098d8

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Performance"
3+
weight: 175
4+
---
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
title: "Caches"
3+
weight: 0
4+
---
5+
6+
## What is the problem?
7+
8+
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?
9+
10+
## How to solve it?
11+
12+
For data that depends on the lifetime of a document or even the entire workspace, Langium has several kinds of caches:
13+
14+
* the document cache saves key-value-pairs of given types `K` and `V` for each document. If the document gets changed or deleted the cache gets cleared automatically for the single files
15+
* the workspace cache also saves key-value-pairs of given types `K` and `V`, but gets cleared entirely when something in the workspace gets changed
16+
17+
Besides those specific caches, Langium also provides:
18+
19+
* a simple cache that can be used for any kind of key-value-data
20+
* a context cache that stores a simple cache for each context object. The document cache and workspace cache are implemented using the context cache
21+
22+
## How to use it?
23+
24+
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.
25+
26+
We will have a computation for each person that determines from which publisher it comes from.
27+
28+
### Add a database
29+
30+
Let's build a "publisher inferer service". First let's create a small database of known publishers and known persons:
31+
32+
```typescript
33+
type KnownPublisher = 'DC' | 'Marvel' | 'Egmont';
34+
const KnownPersonNames: Record<KnownPublisher, string[]> = {
35+
DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'],
36+
Marvel: ['Spiderman', 'Wolverine', 'Deadpool'],
37+
Egmont: ['Asterix', 'Obelix']
38+
};
39+
```
40+
41+
### Define the computation service
42+
43+
For our service we define an interface:
44+
45+
```typescript
46+
export interface InferPublisherService {
47+
inferPublisher(person: Person): KnownPublisher | undefined;
48+
}
49+
```
50+
51+
Now we implement the service:
52+
53+
```typescript
54+
class UncachedInferPublisherService implements InferPublisherService {
55+
inferPublisher(person: Person): KnownPublisher | undefined {
56+
for (const [publisher, persons] of Object.entries(KnownPersonNames)) {
57+
if (persons.includes(person.name)) {
58+
return publisher as KnownPublisher;
59+
}
60+
}
61+
return undefined;
62+
}
63+
}
64+
```
65+
66+
### Add a cache
67+
68+
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:
69+
70+
```typescript
71+
export class CachedInferPublisherService extends UncachedInferPublisherService {
72+
private readonly cache: DocumentCache<Person, KnownPublisher | undefined>;
73+
constructor(services: HelloWorldServices) {
74+
super();
75+
this.cache = new DocumentCache(services.shared);
76+
}
77+
override inferPublisher(person: Person): KnownPublisher | undefined {
78+
const documentUri = AstUtils.getDocument(person).uri;
79+
//get cache entry for the documentUri and the person
80+
//if it does not exist, calculate the value and store it
81+
return this.cache.get(documentUri, person, () => super.inferPublisher(person));
82+
}
83+
}
84+
```
85+
86+
### Use the service
87+
88+
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:
89+
90+
```typescript
91+
import type { ValidationAcceptor, ValidationChecks } from 'langium';
92+
import type { HelloWorldAstType, Person } from './generated/ast.js';
93+
import type { HelloWorldServices } from './hello-world-module.js';
94+
import { InferPublisherService } from './infer-publisher-service.js';
95+
96+
/**
97+
* Register custom validation checks.
98+
*/
99+
export function registerValidationChecks(services: HelloWorldServices) {
100+
const registry = services.validation.ValidationRegistry;
101+
const validator = services.validation.HelloWorldValidator;
102+
const checks: ValidationChecks<HelloWorldAstType> = {
103+
Person: validator.checkPersonIsFromKnownPublisher
104+
};
105+
registry.register(checks, validator);
106+
}
107+
108+
/**
109+
* Implementation of custom validations.
110+
*/
111+
export class HelloWorldValidator {
112+
private readonly inferPublisherService: InferPublisherService;
113+
114+
constructor(services: HelloWorldServices) {
115+
this.inferPublisherService = services.utilities.inferPublisherService;
116+
}
117+
118+
checkPersonIsFromKnownPublisher(person: Person, accept: ValidationAcceptor): void {
119+
if (!this.inferPublisherService.inferPublisher(person)) {
120+
accept('warning', `"${person.name}" is not from a known publisher.`, {
121+
node: person
122+
});
123+
}
124+
}
125+
126+
}
127+
```
128+
129+
### Register the service
130+
131+
Finally, we need to register the service in the module. Go to the `hello-world-module.ts` file and add the following code:
132+
133+
```typescript
134+
export type HelloWorldAddedServices = {
135+
utilities: {
136+
inferPublisherService: InferPublisherService
137+
},
138+
validation: {
139+
HelloWorldValidator: HelloWorldValidator
140+
}
141+
}
142+
//...
143+
export const HelloWorldModule: Module<HelloWorldServices, PartialLangiumServices & HelloWorldAddedServices> = {
144+
utilities: {
145+
inferPublisherService: (services) => new CachedInferPublisherService(services)
146+
},
147+
validation: {
148+
//add `services` parameter here
149+
HelloWorldValidator: (services) => new HelloWorldValidator(services)
150+
}
151+
};
152+
```
153+
154+
### Test the result
155+
156+
Start the extension and create a `.hello` file with several persons, like this one:
157+
158+
```plaintext
159+
person Wonderwoman
160+
person Spiderman
161+
person Homer //warning: unknown publisher!!
162+
person Obelix
163+
```
164+
165+
## Last words
166+
167+
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`.
168+
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.
169+
170+
## Appendix
171+
172+
<details>
173+
<summary>Full implementation</summary>
174+
175+
```typescript
176+
import { AstUtils, DocumentCache } from "langium";
177+
import { Person } from "./generated/ast.js";
178+
import { HelloWorldServices } from "./hello-world-module.js";
179+
180+
type KnownPublisher = 'DC' | 'Marvel' | 'Egmont';
181+
const KnownPersonNames: Record<KnownPublisher, string[]> = {
182+
DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'],
183+
Marvel: ['Spiderman', 'Wolverine', 'Deadpool'],
184+
Egmont: ['Asterix', 'Obelix']
185+
};
186+
187+
export interface InferPublisherService {
188+
inferPublisher(person: Person): KnownPublisher | undefined;
189+
}
190+
191+
class UncachedInferPublisherService implements InferPublisherService {
192+
inferPublisher(person: Person): KnownPublisher | undefined {
193+
for (const [publisher, persons] of Object.entries(KnownPersonNames)) {
194+
if (persons.includes(person.name)) {
195+
return publisher as KnownPublisher;
196+
}
197+
}
198+
return undefined;
199+
}
200+
}
201+
202+
export class CachedInferPublisherService extends UncachedInferPublisherService {
203+
private readonly cache: DocumentCache<Person, KnownPublisher | undefined>;
204+
constructor(services: HelloWorldServices) {
205+
super();
206+
this.cache = new DocumentCache(services.shared);
207+
}
208+
override inferPublisher(person: Person): KnownPublisher | undefined {
209+
const documentUri = AstUtils.getDocument(person).uri;
210+
return this.cache.get(documentUri, person, () => super.inferPublisher(person));
211+
}
212+
}
213+
```
214+
215+
</details>

0 commit comments

Comments
 (0)