Merged PR 212: Optimize FindChildrenById from O(n) to O(1)
Optimize FindChildrenById from O(n) to O(1): - Deprecate FindContainerByIdDFS - Container: Replace Children to string[] - Add HashMap to IHistoryState that contains all containers To access a container by id now cost O(1) without any additional cost + Implement CICD for SVGLibs
This commit is contained in:
parent
466ef2b08b
commit
c256a76e01
45 changed files with 775 additions and 450 deletions
217
docs/DataStructure.md
Normal file
217
docs/DataStructure.md
Normal file
|
@ -0,0 +1,217 @@
|
|||
# Préface
|
||||
|
||||
Ce document explique la structure de données utilisée pour les conteneurs.
|
||||
|
||||
|
||||
# Graphe
|
||||
|
||||
Avant de parler de la structure principale il faut comprendre ce qu'est un graphe.
|
||||
|
||||
Pourquoi ? Parce que la plupart des algorithmes utilisés dans ce projets découle de la théorie des graphes.
|
||||
|
||||
> Definition : Un graphe est une structure composée d'objets dans laquelle certaines paries d'objets sont en relation. (...) On distingue les graphes non orientés et les graphes orientés. (src: wikipedia)
|
||||
|
||||
Ces objets sont appelé Container dans notre projet. Mais dans cette documentation, nous appelerons cela un *Noeud*
|
||||
|
||||
En programmation un graphe peut être représenter par des divers structure de données (Vector/List/Array, Dictionaries, Tree, Pointer etc.).
|
||||
|
||||
Dans notre projet, nous utilisons les pointeurs, les listes et les dictionnaires.
|
||||
|
||||
# Structures de données
|
||||
|
||||
## Pointeur
|
||||
|
||||
Un graphe représenté par des pointeurs peut être représenté de cette manière.
|
||||
|
||||
Un exemple de graphe peut être représenté de la manière suivante :
|
||||
```
|
||||
type Node = {
|
||||
child: Node
|
||||
}
|
||||
|
||||
A: Node = {
|
||||
child: B
|
||||
}
|
||||
|
||||
B: Node = {
|
||||
child: A
|
||||
}
|
||||
```
|
||||
|
||||
Donc le graphe est simplement `A <-> B`
|
||||
|
||||
Ceci est un graphe cyclique que nous ne verrons pas souvent dans ce projet.
|
||||
En effet, nous avons plutôt quelque chose comme ça:
|
||||
|
||||
```
|
||||
type Node = {
|
||||
parent: Node
|
||||
child: Node
|
||||
}
|
||||
|
||||
A: Node = {
|
||||
parent: null
|
||||
child: B
|
||||
}
|
||||
|
||||
B: Node = {
|
||||
parent: A
|
||||
child: null
|
||||
}
|
||||
```
|
||||
|
||||
Cela permet de représenter un graphe avec deux liens au lieu d'un seul et d'avoir une information de **hiérarchie**.
|
||||
On le représente donc comme cela: `A -> B`. Donc par définition, un graphe orienté.
|
||||
|
||||
|
||||
## Liste
|
||||
|
||||
Un noeud dans un graphe peut avoir plusieurs voisins/enfants. On peut simplement représenter cela par une *liste* de pointeurs.
|
||||
|
||||
Reprenons l'exemple précédent :
|
||||
|
||||
```
|
||||
type Node = {
|
||||
parent: Node
|
||||
children: Node[]
|
||||
}
|
||||
|
||||
A: Node {
|
||||
parent: null,
|
||||
children: [
|
||||
B,
|
||||
C,
|
||||
]
|
||||
}
|
||||
|
||||
B: Node {
|
||||
parent: A
|
||||
children: []
|
||||
}
|
||||
|
||||
C: Node {
|
||||
parent: A
|
||||
children: []
|
||||
}
|
||||
```
|
||||
|
||||
Ici, A contient donc deux enfants dans une liste: B et C.
|
||||
On peut représenter cela de la manière suivante:
|
||||
|
||||
```
|
||||
A -> B
|
||||
A -> C
|
||||
```
|
||||
|
||||
A partir d'ici vous pouvez voir ce qu'on appelle un *arbre*. Cette structure de données d'arbre est la base fondamentale dont repose les Conteneurs.
|
||||
|
||||
Pour des examples réels, vous pouvez voir qu'un livre peut être représenté comme des arbres :
|
||||
|
||||
> Un livre contiennent des pages, et des pages contiennent des lignes, des lignes contiennent des lettres, etc.
|
||||
|
||||
|
||||
## Dictionnaire
|
||||
|
||||
### Contexte
|
||||
|
||||
Normalement l'arbre devrait être suffisant pour développer SVGLayoutDesigner mais on va s'en rendre compte que ce n'est pas la meilleure solution.
|
||||
|
||||
Depuis le début du projet, la structure utilisée était celle d'un arbre avec relation parent-enfant comme précédemment montré.
|
||||
|
||||
Mon on a remarqué tardivement que cela commençait à avoir un coût très important sur toutes les opérations d'arbres. En effet, car toutes les opérations d'arbre repose sur une fonction principale: la recherche.
|
||||
|
||||
Pour trouver un noeud dans un arbre, il faut parcours l'arbre grâce à des algorithme de parcours d'arbre appelé Depth-First Search et Breath-First Search, qui sont d'excellent algorithme de recherche dans un graphe (oui! car ça ne se limite pas aux arbres).
|
||||
|
||||
Cependant, cela possède un coût au pire cas de O(N). Il est possible que le noeud que le cherche se trouve tout à la fin de l'arbre et ces deux algorithmes n'aident pas du tout dans ce cas là. Imaginez 1000 conteneurs, et on veut juste changer la position d'un seul conteneur. Cela veut dire qu'il faudrait parcourir les 1000 conteneurs !
|
||||
|
||||
Et c'est ce qui c'est passé jusqu'au commit `9f4796d0` (10 oct 2022). Faites une recherche globale de `FindContainerById`, cela montrera toutes les opérations impactées par l'arbre. Au jour de l'écriture de ce document, il y a environ 60 fonctions utilisant cette méthode utilisée dans divers opérations actives ou passives. C'est très COUTEUX !
|
||||
|
||||
Réduisons cela à O(1), le meilleur cas. Pour cela nous allons utiliser ce qu'on appelle un dictionnaire ou aussi appelé aussi HashMap.
|
||||
|
||||
|
||||
### Qu'est-ce qu'un dictionnaire ?
|
||||
|
||||
> Un dictionnaire est une structure de données qui implémente un tableau associatif. C'est un type de données abstrait qui permet l'association de clé-valeur.
|
||||
|
||||
Exemples :
|
||||
|
||||
```
|
||||
Dict {
|
||||
key: value
|
||||
...
|
||||
}
|
||||
|
||||
Dict = [
|
||||
[key, value],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Mise en pratique
|
||||
|
||||
On veut accéder aux conteneurs rapidement, nous allons sauvegarder chaque conteneur dans le dictionnaire. Donc dès que l'ont crée un conteneur, on ajoute son id pour clé et le conteneur en tant que valeur. Dès qu'on le supprime, on supprime sa clé.
|
||||
|
||||
Cependant pour éviter la duplication de données, il faut aussi changer comment on représente l'arbre. Il n'est plus nécessaire de sauvegarder la référence de l'enfant en tant qu'enfant, on peut juste utiliser son id.
|
||||
|
||||
Ainsi on obtient la structure suivante utilisée dans le projet :
|
||||
|
||||
```
|
||||
type Conteneur = {
|
||||
parent: string
|
||||
children: string[]
|
||||
}
|
||||
|
||||
const dict = new Map<string, Conteneur>();
|
||||
|
||||
dict = {
|
||||
"conteneur1": Conteneur {
|
||||
parent: null,
|
||||
children: [
|
||||
"conteneur2",
|
||||
"conteneur3"
|
||||
]
|
||||
},
|
||||
"conteneur3": Conteneur {
|
||||
parent: "conteneur1",
|
||||
children: []
|
||||
},
|
||||
"conteneur2": Conteneur {
|
||||
parent: "conteneur1",
|
||||
children: []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ainsi, `FindContainerById` utilisant précédemment depth-first search, peut être refactoré par une seule ligne :
|
||||
|
||||
```ts
|
||||
function FindContainerById(
|
||||
containers: Map<string, Conteneur>,
|
||||
id: string
|
||||
): Conteneur {
|
||||
return containers.get(id)
|
||||
}
|
||||
```
|
||||
|
||||
Et maintenant déplacer un seul conteneur ne coûte plus aussi cher pas vrai ?
|
||||
|
||||
# FAQ
|
||||
|
||||
Pourquoi est-ce important d'utiliser un dictionnaire dans notre cas ?
|
||||
|
||||
> Cela permet d'accéder un conteneur directement par une clé par exemple son id
|
||||
|
||||
|
||||
Pourquoi ne pas l'utiliser tout le temps ?
|
||||
|
||||
> Car ce n'est pas très intuitif, on aime voir les arbres comme des arbres et pas comme des listes. Le coût est parfois suffisamment mineur pour ignorer cela.
|
||||
|
||||
|
||||
Pourquoi ne pas utiliser l'arbre avec le dictionnaire en même temps ?
|
||||
|
||||
> Car dans notre projet, la sérialisation des données ne permet pas d'avoir deux instances à deux endroits différents. C'est pourquoi nous utilisons un *replacer*, pour supprimer les références de parents. Mais il serait difficile de faire cela pour tous les enfants, il est plus simple de supprimer entièrement l'arbre et de juste conserver le dictionnaire. Et puis, pourquoi dupliquer les données alors que l'on l'accéder avec un coût minimal O(1) avec juste le dictionnaire sans aucun coût supplémentaire ?
|
||||
|
||||
|
||||
Et si je veux itérer sur tout les chassis ?
|
||||
|
||||
> Depth-first search et Breath-first search sont toujours valables. Il faut juste adapter légèrement l'algorithme pour qu'il lit le dictionnaire à chaque fois que l'on veut accéder aux enfants. Voir `MakeDFSIterator` ou `MakeBFSIterator` dans `src/utils/itertools.ts` pour l'exemple.
|
|
@ -26,18 +26,18 @@ Il y deux manières de récupérer les builds du projets:
|
|||
|
||||
Customiser le build du projet permet de modifier les urls de l'API et de personnaliser des fonctionnalités.
|
||||
|
||||
## Configurer les options de build
|
||||
## Configurer l'API
|
||||
|
||||
Il y a deux fichiers principaux à configurer :
|
||||
- `.env.production.local`: pour configurer les URLs d'API
|
||||
- `src/assets/svgld-settings.js`: pour configurer les URLs d'API
|
||||
- `src/utils/default.ts`: pour configurer les fonctionnalités
|
||||
|
||||
Copiez `.env.production` vers `.env.production.local` et modifiez-le comme bon vous semble :
|
||||
Modifiez `public/svgld-settings.js`
|
||||
|
||||
```
|
||||
VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
|
||||
VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList
|
||||
VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback
|
||||
```js
|
||||
export const API_FETCH_URL = 'http://localhost:5000';
|
||||
export const API_SET_CONTAINER_LIST_URL = 'http://localhost:5000/SetContainerList';
|
||||
export const API_GET_FEEDBACK_URL = 'http://localhost:5000/GetFeedback';
|
||||
```
|
||||
|
||||
Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas.
|
||||
|
@ -92,7 +92,7 @@ Cliquez sur le menu hamburger et cliquez sur **Download Artifacts** :
|
|||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue