Skip to content

Commit e5f194f

Browse files
authored
Feature/state benchmark (#581)
* Move state migrations to shared * Add benchmark package * Update readmes * Make problem with duplicated RestoreError more obvious
1 parent d81c794 commit e5f194f

47 files changed

Lines changed: 4325 additions & 299 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslint/typescript.eslintrc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module.exports = {
1919
'no-console': [
2020
'warn',
2121
{
22-
allow: ['log', 'warn', 'error', 'assert'],
22+
allow: ['log', 'warn', 'error', 'assert', 'table'],
2323
},
2424
],
2525
'no-promise-executor-return': 'warn',

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ backend/coverage
2121

2222
shared/coverage
2323
shared/tsconfig.build.tsbuildinfo
24+
25+
benchmark/data/*
26+
!benchmark/data/*.permanent.json

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ backend/src/database/migrations/
55
docker-compose.yml
66
# same, but probably not needed
77
.env.example
8+
9+
# The states in here should not be touched by humans
10+
benchmark/data

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ If a test fails a new screenshot is taken and put in the `comparison` folder.
145145
If the new screenshot is the new desired result, then you only have to move it in the `baseline` folder and replace the old reference screenshot with the same name.
146146
In the `diff` folder you can see the changes between the baseline and the comparison screenshot.
147147

148+
## Benchmarking
149+
150+
You can run the benchmarks via `npm run benchmark` in the root folder.
151+
Look at the [benchmark readme](./benchmark/README.md) for more information.
152+
148153
## Styleguide
149154

150155
- names are never unique, ids are
@@ -172,7 +177,8 @@ This repository is a monorepo that consists of the following packages:
172177

173178
- [frontend](./frontend) the browser-based client application ([Angular](https://angular.io/))
174179
- [backend](./backend) the server-side application ([NodeJs](https://nodejs.org/))
175-
- [shared](./shared) the shared code that is used by both frontend and backend
180+
- [benchmark](./benchmark/) benchmarks and tests some parts of the application
181+
- [shared](./shared) the shared code that is used by the frontend, backend and the benchmark package
176182

177183
Each package has its own `README.md` file with additional documentation. Please check them out before you start working on the project.
178184

backend/README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,6 @@ If you want to, you can also disable the database.
9999
Set the environment variable `DFM_USE_DB` (in [`../.env`](../.env)) to `false` to achieve this.
100100
Note however that this results in a) all history being saved in memory instead of on disk, and b) once the backend exits, for whatever reason, all data is gone forever.
101101

102-
### Migrations
103-
104-
We use [state migrations](./src/database/state-migrations/) to convert outdated states to new versions.
105-
Look at [`migrations.ts`](./src/database/state-migrations/migrations.ts) for more information.
106-
107102
### Note on long term storage
108103

109104
The current setup when using a database is that no exercises get deleted unless anyone deletes them from the UI (or, more precisely, using the HTTP request `DELETE /api/exercises/:exerciseId`).
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { UUID } from 'digital-fuesim-manv-shared';
2+
import { applyMigrations } from 'digital-fuesim-manv-shared';
3+
import type { EntityManager } from 'typeorm';
4+
import { RestoreError } from '../utils/restore-error';
5+
import { ActionWrapperEntity } from './entities/action-wrapper.entity';
6+
import { ExerciseWrapperEntity } from './entities/exercise-wrapper.entity';
7+
8+
export async function migrateInDatabase(
9+
exerciseId: UUID,
10+
entityManager: EntityManager
11+
): Promise<void> {
12+
const exercise = await entityManager.findOne(ExerciseWrapperEntity, {
13+
where: { id: exerciseId },
14+
});
15+
if (exercise === null) {
16+
throw new RestoreError(
17+
'Cannot find exercise to convert in database',
18+
exerciseId
19+
);
20+
}
21+
const initialState = JSON.parse(exercise.initialStateString);
22+
const currentState = JSON.parse(exercise.currentStateString);
23+
const actions = (
24+
await entityManager.find(ActionWrapperEntity, {
25+
where: { exercise: { id: exerciseId } },
26+
select: { actionString: true },
27+
order: { index: 'ASC' },
28+
})
29+
).map((action) => JSON.parse(action.actionString));
30+
const newVersion = applyMigrations(exercise.stateVersion, {
31+
currentState,
32+
history: {
33+
initialState,
34+
actions,
35+
},
36+
});
37+
exercise.stateVersion = newVersion;
38+
// Save exercise wrapper
39+
const patch: Partial<ExerciseWrapperEntity> = {
40+
stateVersion: exercise.stateVersion,
41+
};
42+
patch.initialStateString = JSON.stringify(initialState);
43+
patch.currentStateString = JSON.stringify(currentState);
44+
await entityManager.update(
45+
ExerciseWrapperEntity,
46+
{ id: exerciseId },
47+
patch
48+
);
49+
// Save actions
50+
if (actions !== undefined) {
51+
let patchedActionsIndex = 0;
52+
const indicesToRemove: number[] = [];
53+
const actionsToUpdate: {
54+
previousIndex: number;
55+
newIndex: number;
56+
actionString: string;
57+
}[] = [];
58+
actions.forEach((action, i) => {
59+
if (action === null) {
60+
indicesToRemove.push(i);
61+
return;
62+
}
63+
actionsToUpdate.push({
64+
previousIndex: i,
65+
newIndex: patchedActionsIndex++,
66+
actionString: JSON.stringify(action),
67+
});
68+
});
69+
if (indicesToRemove.length > 0) {
70+
await entityManager
71+
.createQueryBuilder()
72+
.delete()
73+
.from(ActionWrapperEntity)
74+
// eslint-disable-next-line unicorn/string-content
75+
.where('index IN (:...ids)', { ids: indicesToRemove })
76+
.execute();
77+
}
78+
if (actionsToUpdate.length > 0) {
79+
await Promise.all(
80+
actionsToUpdate.map(
81+
async ({ previousIndex, newIndex, actionString }) =>
82+
entityManager.update(
83+
ActionWrapperEntity,
84+
{
85+
index: previousIndex,
86+
exercise: { id: exerciseId },
87+
},
88+
{ actionString, index: newIndex }
89+
)
90+
)
91+
);
92+
}
93+
}
94+
}

backend/src/database/state-migrations/impossible-migration.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

backend/src/database/state-migrations/migrations.ts

Lines changed: 0 additions & 227 deletions
This file was deleted.

0 commit comments

Comments
 (0)