From 64c6f62c93c51eb6fd1d6c33a103566cdbd4936f Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Thu, 20 Jun 2024 11:24:52 +0200 Subject: [PATCH 01/10] Add a recipe --- hugo/content/docs/recipes/utilities/_index.md | 4 + hugo/content/docs/recipes/utilities/caches.md | 223 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 hugo/content/docs/recipes/utilities/_index.md create mode 100644 hugo/content/docs/recipes/utilities/caches.md diff --git a/hugo/content/docs/recipes/utilities/_index.md b/hugo/content/docs/recipes/utilities/_index.md new file mode 100644 index 00000000..ad57ad67 --- /dev/null +++ b/hugo/content/docs/recipes/utilities/_index.md @@ -0,0 +1,4 @@ +--- +title: "Utilities" +weight: 175 +--- \ No newline at end of file diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md new file mode 100644 index 00000000..77350606 --- /dev/null +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -0,0 +1,223 @@ +--- +title: "Caches" +weight: 0 +--- + +## What is the problem? + +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? + +## How to solve 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` and `V` for each document. If the document gets changed or deleted the cache gets cleared automatically for the single files +* the workspace cache also save key-value-pairs of given types `K` and `V` 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. + +## How to use it? + +Here we will use the `HelloWorld` example from the learning section. Let's keep it simple and just list persons in the document, that come from a comic book. + +We will have a computation for each person that determines from which publisher it comes from. + +### Add a database + +Let's build a "publisher inferer service". First let's create a small database of known publishers and known persons: + +```typescript +type KnownPublisher = 'DC'|'Marvel'|'Egmont'; +const KnownPersonNames: Record = { + DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'], + Marvel: ['Spiderman', 'Wolverine', 'Deadpool'], + Egmont: ['Asterix', 'Obelix'] +}; +``` + +### Define the computation service + +For our service we define an interface: + +```typescript +export interface InferPublisherService { + inferPublisher(person: Person): KnownPublisher|undefined; +} +``` + +Now we implement the service: + +```typescript +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; + } +} +``` + +### Add a cache + +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: + +```typescript +export class CachedInferPublisherService extends UncachedInferPublisherService { + private readonly cache: DocumentCache; + constructor(services: HelloWorldServices) { + super(); + this.cache = new DocumentCache(services.shared); + } + override inferPublisher(person: Person): KnownPublisher|undefined { + const documentUri = AstUtils.getDocument(person).uri; + if(this.cache.has(documentUri, person)) { + return this.cache.get(documentUri, person)!; + } + const publisher = super.inferPublisher(person); + this.cache.set(documentUri, person, publisher); + return publisher; + } +} +``` + +### Use the service + +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: + +```typescript +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 = { + 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) === undefined) { + accept('warning', `"${person.name}" is not from a known publisher.`, { + node: person + }); + } + } + +} +``` + +### Register the service + +Finally, we need to register the service in the module. Go to the `hello-world-module.ts` file and add the following code: + +```typescript +export type HelloWorldAddedServices = { + utilities: { + inferPublisherService: InferPublisherService + }, + validation: { + HelloWorldValidator: HelloWorldValidator + } +} +//... +export const HelloWorldModule: Module = { + utilities: { + inferPublisherService: (services) => new CachedInferPublisherService(services) + }, + validation: { + //add `services` parameter here + HelloWorldValidator: (services) => new HelloWorldValidator(services) + } +}; +``` + +### Test the result + +Start the extension and create a `.hello` file with several persons, like this one: + +```plaintext +person Wonderwoman +person Spiderman +person Homer //warning: unknown publisher!! +person Obelix +``` + +## Last words + +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 them are disposable compared to a simple `Map`. If you dispose them by calling `dispose()` the entries will be removed and the memory will be freed. + +## Appendix + +
+Ful implementation + +```typescript +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 = { + 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; + constructor(services: HelloWorldServices) { + super(); + this.cache = new DocumentCache(services.shared); + } + override inferPublisher(person: Person): KnownPublisher|undefined { + const documentUri = AstUtils.getDocument(person).uri; + if(this.cache.has(documentUri, person)) { + return this.cache.get(documentUri, person)!; + } + const publisher = super.inferPublisher(person); + this.cache.set(documentUri, person, publisher); + return publisher; + } +} +``` + +
From ce1d9d6c1d33aa77c7273e22cc8a65b61ab8481a Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Thu, 20 Jun 2024 12:02:55 +0200 Subject: [PATCH 02/10] Use a shorter cache lookup strategy --- hugo/content/docs/recipes/utilities/caches.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index 77350606..93cf7edf 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -76,12 +76,9 @@ export class CachedInferPublisherService extends UncachedInferPublisherService { } override inferPublisher(person: Person): KnownPublisher|undefined { const documentUri = AstUtils.getDocument(person).uri; - if(this.cache.has(documentUri, person)) { - return this.cache.get(documentUri, person)!; - } - const publisher = super.inferPublisher(person); - this.cache.set(documentUri, person, publisher); - return publisher; + //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)); } } ``` @@ -210,12 +207,7 @@ export class CachedInferPublisherService extends UncachedInferPublisherService { } override inferPublisher(person: Person): KnownPublisher|undefined { const documentUri = AstUtils.getDocument(person).uri; - if(this.cache.has(documentUri, person)) { - return this.cache.get(documentUri, person)!; - } - const publisher = super.inferPublisher(person); - this.cache.set(documentUri, person, publisher); - return publisher; + return this.cache.get(documentUri, person, () => super.inferPublisher(person)); } } ``` From abb5ba1e4d447fa3d48301cd5188f7727262277a Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 08:55:44 +0200 Subject: [PATCH 03/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index 93cf7edf..2128e923 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -12,7 +12,7 @@ You have parsed a document and you would like to execute some computation on the 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` and `V` for each document. If the document gets changed or deleted the cache gets cleared automatically for the single files -* the workspace cache also save key-value-pairs of given types `K` and `V` but gets cleared entirely when something in the workspace gets changed. +* 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 Besides those specific caches, Langium also provides From 0cd2c12fbb678861aa5065d21689f9655149064d Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 08:55:56 +0200 Subject: [PATCH 04/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index 2128e923..e8b837d7 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -14,7 +14,7 @@ For data that depends on the lifetime of a document or even the entire workspace * 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 * 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 -Besides those specific caches, Langium also provides +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. From a55dafc9f38b5646239cd992e75495d8d962eeba Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 08:56:13 +0200 Subject: [PATCH 05/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index e8b837d7..c6ff8186 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -9,7 +9,7 @@ You have parsed a document and you would like to execute some computation on the ## How to solve it? -For data that depends on the lifetime of a document or even the entire workspace, Langium has several kinds of caches. +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` and `V` 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` and `V`, but gets cleared entirely when something in the workspace gets changed From bc555bedb4b1722a89608ae978acedd842399793 Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 08:56:31 +0200 Subject: [PATCH 06/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index c6ff8186..c4174dbe 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -17,7 +17,7 @@ For data that depends on the lifetime of a document or even the entire workspace 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. +* a context cache that stores a simple cache for each context object. The document cache and workspace cache are implemented using the context cache ## How to use it? From aab190e916a3458967b654e2f969b1f5482f8026 Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 08:56:59 +0200 Subject: [PATCH 07/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index c4174dbe..cee71714 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -165,7 +165,7 @@ person Obelix ## Last words 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 them are disposable compared to a simple `Map`. If you dispose them by calling `dispose()` the entries will be removed and the memory will be freed. +All of these caches are disposable compared to a simple `Map`. If you dispose them by calling `dispose()` the entries will be removed and the memory will be freed. ## Appendix From c2bfb1d3d1dd81be395d99bd0fd3a35ed22104e4 Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 09:04:55 +0200 Subject: [PATCH 08/10] Update hugo/content/docs/recipes/utilities/caches.md Co-authored-by: Benjamin Friedman Wilson --- hugo/content/docs/recipes/utilities/caches.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/utilities/caches.md index cee71714..e23c01b0 100644 --- a/hugo/content/docs/recipes/utilities/caches.md +++ b/hugo/content/docs/recipes/utilities/caches.md @@ -21,7 +21,7 @@ Besides those specific caches, Langium also provides: ## How to use it? -Here we will use the `HelloWorld` example from the learning section. Let's keep it simple and just list persons in the document, that come from a comic book. +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. From 8f53d12df901bb0655c409dbdd3dd0487ffb8c52 Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 7 Aug 2024 09:05:45 +0200 Subject: [PATCH 09/10] Move to cateegory performance --- hugo/content/docs/recipes/performance/_index.md | 4 ++++ .../content/docs/recipes/{utilities => performance}/caches.md | 0 hugo/content/docs/recipes/utilities/_index.md | 4 ---- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 hugo/content/docs/recipes/performance/_index.md rename hugo/content/docs/recipes/{utilities => performance}/caches.md (100%) delete mode 100644 hugo/content/docs/recipes/utilities/_index.md diff --git a/hugo/content/docs/recipes/performance/_index.md b/hugo/content/docs/recipes/performance/_index.md new file mode 100644 index 00000000..5f2a4082 --- /dev/null +++ b/hugo/content/docs/recipes/performance/_index.md @@ -0,0 +1,4 @@ +--- +title: "Performance" +weight: 175 +--- \ No newline at end of file diff --git a/hugo/content/docs/recipes/utilities/caches.md b/hugo/content/docs/recipes/performance/caches.md similarity index 100% rename from hugo/content/docs/recipes/utilities/caches.md rename to hugo/content/docs/recipes/performance/caches.md diff --git a/hugo/content/docs/recipes/utilities/_index.md b/hugo/content/docs/recipes/utilities/_index.md deleted file mode 100644 index ad57ad67..00000000 --- a/hugo/content/docs/recipes/utilities/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Utilities" -weight: 175 ---- \ No newline at end of file From 60b0980eeb534702cbe3a489d6eb8e7aa43ae76b Mon Sep 17 00:00:00 2001 From: Markus Rudolph Date: Wed, 28 Aug 2024 14:34:49 +0200 Subject: [PATCH 10/10] Resolved review comments by Mark --- .../docs/recipes/performance/caches.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/hugo/content/docs/recipes/performance/caches.md b/hugo/content/docs/recipes/performance/caches.md index e23c01b0..f2a7a1b6 100644 --- a/hugo/content/docs/recipes/performance/caches.md +++ b/hugo/content/docs/recipes/performance/caches.md @@ -30,7 +30,7 @@ We will have a computation for each person that determines from which publisher Let's build a "publisher inferer service". First let's create a small database of known publishers and known persons: ```typescript -type KnownPublisher = 'DC'|'Marvel'|'Egmont'; +type KnownPublisher = 'DC' | 'Marvel' | 'Egmont'; const KnownPersonNames: Record = { DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'], Marvel: ['Spiderman', 'Wolverine', 'Deadpool'], @@ -44,7 +44,7 @@ For our service we define an interface: ```typescript export interface InferPublisherService { - inferPublisher(person: Person): KnownPublisher|undefined; + inferPublisher(person: Person): KnownPublisher | undefined; } ``` @@ -52,9 +52,9 @@ Now we implement the service: ```typescript class UncachedInferPublisherService implements InferPublisherService { - inferPublisher(person: Person): KnownPublisher|undefined { + inferPublisher(person: Person): KnownPublisher | undefined { for (const [publisher, persons] of Object.entries(KnownPersonNames)) { - if(persons.includes(person.name)) { + if (persons.includes(person.name)) { return publisher as KnownPublisher; } } @@ -69,12 +69,12 @@ Now we want to cache the results of the `inferPublisher` method. We can use the ```typescript export class CachedInferPublisherService extends UncachedInferPublisherService { - private readonly cache: DocumentCache; + private readonly cache: DocumentCache; constructor(services: HelloWorldServices) { super(); this.cache = new DocumentCache(services.shared); } - override inferPublisher(person: Person): KnownPublisher|undefined { + 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 @@ -116,7 +116,7 @@ export class HelloWorldValidator { } checkPersonIsFromKnownPublisher(person: Person, accept: ValidationAcceptor): void { - if(this.inferPublisherService.inferPublisher(person) === undefined) { + if (!this.inferPublisherService.inferPublisher(person)) { accept('warning', `"${person.name}" is not from a known publisher.`, { node: person }); @@ -165,19 +165,19 @@ person Obelix ## Last words 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`. If you dispose them by calling `dispose()` the entries will be removed and the memory will be freed. +All of these caches are disposable compared to a simple `Map`. 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. ## Appendix
-Ful implementation +Full implementation ```typescript import { AstUtils, DocumentCache } from "langium"; import { Person } from "./generated/ast.js"; import { HelloWorldServices } from "./hello-world-module.js"; -type KnownPublisher = 'DC'|'Marvel'|'Egmont'; +type KnownPublisher = 'DC' | 'Marvel' | 'Egmont'; const KnownPersonNames: Record = { DC: ['Superman', 'Batman', 'Aquaman', 'Wonderwoman', 'Flash'], Marvel: ['Spiderman', 'Wolverine', 'Deadpool'], @@ -185,13 +185,13 @@ const KnownPersonNames: Record = { }; export interface InferPublisherService { - inferPublisher(person: Person): KnownPublisher|undefined; + inferPublisher(person: Person): KnownPublisher | undefined; } class UncachedInferPublisherService implements InferPublisherService { - inferPublisher(person: Person): KnownPublisher|undefined { + inferPublisher(person: Person): KnownPublisher | undefined { for (const [publisher, persons] of Object.entries(KnownPersonNames)) { - if(persons.includes(person.name)) { + if (persons.includes(person.name)) { return publisher as KnownPublisher; } } @@ -200,12 +200,12 @@ class UncachedInferPublisherService implements InferPublisherService { } export class CachedInferPublisherService extends UncachedInferPublisherService { - private readonly cache: DocumentCache; + private readonly cache: DocumentCache; constructor(services: HelloWorldServices) { super(); this.cache = new DocumentCache(services.shared); } - override inferPublisher(person: Person): KnownPublisher|undefined { + override inferPublisher(person: Person): KnownPublisher | undefined { const documentUri = AstUtils.getDocument(person).uri; return this.cache.get(documentUri, person, () => super.inferPublisher(person)); }