Database
 sql >> Base de données >  >> RDS >> Database

Terminer SQL. Histoires de succès et d'échecs

Je travaille pour une entreprise qui développe des IDE pour l'interaction avec les bases de données depuis plus de cinq ans. Avant de commencer à écrire cet article, je n'avais aucune idée du nombre de contes de fantaisie qui m'attendaient.

Mon équipe développe et prend en charge les fonctionnalités du langage IDE, et l'auto-complétion du code est la principale. J'ai été confronté à de nombreuses choses passionnantes. Certaines choses que nous avons bien faites dès le premier essai, et d'autres ont échoué même après plusieurs tirs.

Analyse SQL et dialectes

SQL est une tentative de ressembler à un langage naturel, et la tentative est assez réussie, je dois dire. Selon le dialecte, il existe plusieurs milliers de mots-clés. Pour distinguer une déclaration d'une autre, vous devez souvent rechercher un ou deux mots (jetons) devant. Cette approche s'appelle une anticipation .

Il existe une classification des analyseurs en fonction de la distance qu'ils peuvent anticiper :LA(1), LA(2) ou LA(*), ce qui signifie qu'un analyseur peut regarder aussi loin que nécessaire pour définir la bonne bifurcation.

Parfois, la fin d'une clause facultative correspond au début d'une autre clause facultative. Ces situations rendent l'analyse beaucoup plus difficile à exécuter. T-SQL ne facilite pas les choses. De plus, certaines instructions SQL peuvent avoir, mais pas nécessairement, des fins qui peuvent entrer en conflit avec le début des instructions précédentes.

Vous ne le croyez pas ? Il existe un moyen de décrire les langages formels via la grammaire. Vous pouvez en générer un analyseur en utilisant tel ou tel outil. Les outils et les langages les plus remarquables qui décrivent la grammaire sont YACC et ANTLR.

YACC Les parseurs générés sont utilisés dans les moteurs MySQL, MariaDB et PostgreSQL. Nous pourrions essayer de les prendre directement à partir du code source et développer la complétion de code et d'autres fonctions basées sur l'analyse SQL utilisant ces analyseurs. De plus, ce produit recevrait des mises à jour de développement gratuites et l'analyseur se comporterait de la même manière que le moteur source.

Alors pourquoi utilisons-nous encore ANTLR ? ? Il prend fermement en charge C#/.NET, dispose d'une boîte à outils décente, sa syntaxe est beaucoup plus facile à lire et à écrire. La syntaxe ANTLR est devenue si pratique que Microsoft l'utilise désormais dans sa documentation officielle C#.

Mais revenons à la complexité SQL en matière d'analyse. Je voudrais comparer les tailles de grammaire des langues accessibles au public. Dans dbForge, nous utilisons nos morceaux de grammaire. Ils sont plus complets que les autres. Malheureusement, ils sont surchargés avec les insertions du code C# pour prendre en charge différentes fonctions.

Les tailles de grammaire pour les différentes langues sont les suivantes :

JS - 475 lignes d'analyseur + 273 lexers =748 lignes

Java - 615 lignes d'analyseur + 211 lexers =826 lignes

C # - 1159 lignes d'analyseur + 433 lexers =1592 lignes

С++ - 1933 lignes

MySQL - 2515 lignes d'analyseur + 1189 lexers =3704 lignes

T-SQL - 4035 lignes d'analyseur + 896 lexers =4931 lignes

PL SQL - 6719 lignes d'analyseur + 2366 lexers =9085 lignes

Les terminaisons de certains lexers comportent les listes des caractères Unicode disponibles dans la langue. Ces listes sont inutiles en ce qui concerne l'évaluation de la complexité du langage. Ainsi, le nombre de lignes que je prenais se terminait toujours avant ces listes.

L'évaluation de la complexité de l'analyse du langage en fonction du nombre de lignes dans la grammaire du langage est discutable. Néanmoins, je pense qu'il est important de montrer les chiffres qui présentent un énorme écart.

Ce n'est pas tout. Puisque nous développons un IDE, nous devons gérer les scripts incomplets ou invalides. Nous avons dû trouver de nombreuses astuces, mais les clients envoient encore de nombreux scénarios de travail avec des scripts inachevés. Nous devons résoudre ce problème.

Guerres de prédicats

Lors de l'analyse du code, le mot ne vous dit parfois pas laquelle des deux alternatives choisir. Le mécanisme qui résout ce type d'inexactitudes est anticipation dans l'ANTLR. La méthode d'analyse est la chaîne insérée de si , et chacun d'eux a une longueur d'avance. Voir l'exemple de la grammaire qui génère l'incertitude de ce genre :

rule1:
  'a' rule2 | rule3
;

rule2:
  'b' 'c' 'd'
;

rule3:
  'b' 'c' 'e'
;

Au milieu de la règle1, lorsque le jeton "a" est déjà passé, l'analyseur regardera deux pas en avant pour choisir la règle à suivre. Cette vérification sera effectuée une fois de plus, mais cette grammaire peut être réécrite pour exclure le lookahead . L'inconvénient est que de telles optimisations nuisent à la structure, alors que l'amélioration des performances est plutôt faible.

Il existe des moyens plus complexes de résoudre ce type d'incertitude. Par exemple, le prédicat de syntaxe (SynPred) mécanisme dans ANTLR3 . Cela aide lorsque la fin facultative d'une clause croise le début de la prochaine clause facultative.

En termes d'ANTLR3, un prédicat est une méthode générée qui effectue une saisie de texte virtuelle selon l'une des alternatives . En cas de succès, il renvoie le true valeur, et l'achèvement du prédicat est réussi. Lorsqu'il s'agit d'une entrée virtuelle, cela s'appelle un retour en arrière entrée de mode. Si un prédicat fonctionne avec succès, la véritable entrée se produit.

C'est seulement un problème quand un prédicat commence à l'intérieur d'un autre prédicat. Ensuite, une distance peut être franchie des centaines ou des milliers de fois.

Reprenons un exemple simplifié. Il y a trois points d'incertitude :(A, B, C).

  1. L'analyseur entre A, se souvient de sa position dans le texte, commence une entrée virtuelle de niveau 1.
  2. L'analyseur saisit B, se souvient de sa position dans le texte, démarre une entrée virtuelle de niveau 2.
  3. L'analyseur entre C, se souvient de sa position dans le texte, commence une entrée virtuelle de niveau 3.
  4. L'analyseur effectue une entrée virtuelle de niveau 3, revient au niveau 2 et passe à nouveau C.
  5. L'analyseur effectue une entrée virtuelle de niveau 2, revient au niveau 1 et passe à nouveau B et C.
  6. L'analyseur complète une entrée virtuelle, renvoie et effectue une entrée réelle via A, B et C.

En conséquence, toutes les vérifications dans C seront effectuées 4 fois, dans B - 3 fois, dans A - 2 fois.

Mais que se passe-t-il si une alternative appropriée se trouve dans la deuxième ou la troisième de la liste ? Ensuite, l'une des étapes de prédicat échouera. Sa position dans le texte sera annulée et un autre prédicat commencera à s'exécuter.

Lors de l'analyse des raisons du blocage de l'application, nous tombons souvent sur la trace de SynPred exécuté plusieurs milliers de fois. SynPred s sont particulièrement problématiques dans les règles récursives. Malheureusement, SQL est récursif par nature. La possibilité d'utiliser des sous-requêtes presque partout a un prix. Cependant, il est possible de manipuler la règle pour faire disparaître un prédicat.

SynPred nuit aux performances. À un moment donné, leur nombre a été placé sous contrôle strict. Mais le problème est que lorsque vous écrivez du code de grammaire, SynPred peut vous sembler non évident. Plus encore, la modification d'une règle peut faire apparaître SynPred dans une autre règle, ce qui rend le contrôle sur celles-ci pratiquement impossible.

Nous avons créé une simple expression régulière outil pour contrôler le nombre de prédicats exécutés par la tâche MSBuild spéciale . Si le nombre de prédicats ne correspondait pas au nombre spécifié dans un fichier, la tâche échouait immédiatement la génération et avertissait d'une erreur.

Lorsqu'il voit l'erreur, un développeur doit réécrire le code de la règle plusieurs fois pour supprimer les prédicats redondants. Si l'on ne peut pas éviter les prédicats, le développeur l'ajoutera à un fichier spécial qui attirera une attention supplémentaire pour la révision.

En de rares occasions, nous avons même écrit nos prédicats en utilisant C # juste pour éviter ceux générés par ANTLR. Heureusement, cette méthode existe aussi.

Héritage de la grammaire

Lorsque des modifications sont apportées à nos SGBD pris en charge, nous devons les intégrer dans nos outils. La prise en charge des constructions de syntaxe grammaticale est toujours un point de départ.

Nous créons une grammaire spéciale pour chaque dialecte SQL. Cela permet une certaine répétition de code, mais c'est plus facile que d'essayer de trouver ce qu'ils ont en commun.

Nous avons opté pour l'écriture de notre propre préprocesseur de grammaire ANTLR qui gère l'héritage de la grammaire.

Il est également devenu évident que nous avions besoin d'un mécanisme de polymorphisme - la capacité non seulement de redéfinir la règle dans le descendant, mais aussi d'appeler la règle de base. Nous aimerions également contrôler la position lors de l'appel de la règle de base.

Les outils sont un plus indéniable lorsque nous comparons ANTLR à d'autres outils de reconnaissance de langage, Visual Studio et ANTLRWorks. Et vous ne voulez pas perdre cet avantage lors de la mise en œuvre de l'héritage. La solution consistait à spécifier la grammaire de base dans une grammaire héritée dans un format de commentaire ANTLR. Pour les outils ANTLR, il s'agit simplement d'un commentaire, mais nous pouvons en extraire toutes les informations requises.

Nous avons écrit une tâche MsBuild qui a été intégrée dans le système de construction complet en tant qu'action de pré-construction. La tâche consistait à faire le travail d'un préprocesseur pour la grammaire ANTLR en générant la grammaire résultante à partir de ses pairs de base et hérités. La grammaire résultante a été traitée par ANTLR lui-même.

Post-traitement ANTLR

Dans de nombreux langages de programmation, les mots-clés ne peuvent pas être utilisés comme noms de sujet. Il peut y avoir de 800 à 3000 mots clés en SQL selon le dialecte. La plupart d'entre eux sont liés au contexte à l'intérieur des bases de données. Ainsi, les interdire en tant que noms d'objets frustrerait les utilisateurs. C'est pourquoi SQL a des mots clés réservés et non réservés.

Vous ne pouvez pas nommer votre objet en tant que mot réservé (SELECT, FROM, etc.) sans le citer, mais vous pouvez le faire avec un mot non réservé (CONVERSATION, DISPONIBILITÉ, etc.). Cette interaction rend le développement de l'analyseur plus difficile.

Lors de l'analyse lexicale, le contexte est inconnu, mais un parseur nécessite déjà des numéros différents pour l'identifiant et le mot-clé. C'est pourquoi nous avons ajouté un autre post-traitement à l'analyseur ANTLR. Il a remplacé toutes les vérifications d'identifiant évidentes par l'appel d'une méthode spéciale.

Cette méthode a une vérification plus détaillée. Si l'entrée appelle un identifiant et que nous nous attendons à ce que l'identifiant soit rencontré, alors tout va bien. Mais si un mot non réservé est une entrée, nous devons le revérifier. Cette vérification supplémentaire examine la recherche de branche dans le contexte actuel où ce mot-clé non réservé peut être un mot-clé. S'il n'y a pas de telles branches, il peut être utilisé comme identifiant.

Techniquement, ce problème pourrait être résolu par le biais de l'ANTLR mais cette décision n'est pas optimale. La méthode ANTLR consiste à créer une règle qui répertorie tous les mots-clés non réservés et un identifiant de lexème. Plus loin, une règle spéciale servira à la place d'un identifiant de lexème. Cette solution permet à un développeur de ne pas oublier d'ajouter le mot-clé là où il est utilisé et dans la règle spéciale. De plus, cela optimise le temps passé.

Erreurs d'analyse de syntaxe sans arbres

L'arbre de syntaxe est généralement le résultat d'un travail d'analyseur. C'est une structure de données qui reflète le texte du programme à travers la grammaire formelle. Si vous souhaitez implémenter un éditeur de code avec l'auto-complétion du langage, vous obtiendrez très probablement l'algorithme suivant :

  1. Analyser le texte dans l'éditeur. Ensuite, vous obtenez un arbre de syntaxe.
  2. Trouvez un nœud sous le chariot et associez-le à la grammaire.
  3. Découvrez quels mots-clés et types d'objets seront disponibles au Point.

Dans ce cas, la grammaire est facile à imaginer comme un graphe ou une machine à états.

Malheureusement, seule la troisième version d'ANTLR était disponible lorsque l'IDE dbForge a commencé son développement. Cependant, il n'était pas aussi agile et bien que vous puissiez dire à ANTLR comment construire un arbre, l'utilisation n'était pas fluide.

De plus, de nombreux articles sur ce sujet suggéraient d'utiliser le mécanisme "actions" pour exécuter du code lorsque l'analyseur passait par la règle. Ce mécanisme est très pratique, mais il a entraîné des problèmes d'architecture et rendu plus complexe la prise en charge de nouvelles fonctionnalités.

Le fait est qu'un seul fichier de grammaire a commencé à accumuler des "actions" en raison du grand nombre de fonctionnalités qui auraient plutôt dû être distribuées à différentes versions. Nous avons réussi à distribuer des gestionnaires d'actions à différentes versions et à créer une variation sournoise du modèle abonné-notificateur pour cette mesure.

ANTLR3 fonctionne 6 fois plus vite que ANTLR4 selon nos mesures. De plus, l'arborescence de syntaxe des gros scripts pouvait prendre trop de RAM, ce qui n'était pas une bonne nouvelle. Nous devions donc opérer dans l'espace d'adressage 32 bits de Visual Studio et de SQL Management Studio.

Post-traitement de l'analyseur ANTLR

Lorsque vous travaillez avec des chaînes, l'un des moments les plus critiques est l'étape de l'analyse lexicale où nous divisons le script en mots séparés.

ANTLR prend en entrée une grammaire qui spécifie la langue et génère un analyseur dans l'une des langues disponibles. À un moment donné, l'analyseur généré a tellement grossi que nous avons eu peur de le déboguer. Si vous appuyez sur F11 (entrez dans) lors du débogage et que vous accédez au fichier d'analyseur, Visual Studio planterait simplement.

Il s'est avéré qu'il a échoué en raison d'une exception OutOfMemory lors de l'analyse du fichier d'analyseur. Ce fichier contenait plus de 200 000 lignes de code.

Mais le débogage de l'analyseur est une partie essentielle du processus de travail, et vous ne pouvez pas l'omettre. À l'aide de classes partielles C #, nous avons analysé l'analyseur généré à l'aide d'expressions régulières et l'avons divisé en quelques fichiers. Visual Studio a parfaitement fonctionné avec.

Analyse lexicale sans sous-chaîne avant l'API Span

La tâche principale de l'analyse lexicale est la classification - définir les limites des mots et les comparer à un dictionnaire. Si le mot est trouvé, l'analyseur renverra son index. Sinon, le mot est considéré comme un identifiant d'objet. Ceci est une description simplifiée de l'algorithme.

Lexique en arrière-plan lors de l'ouverture du fichier

La coloration syntaxique est basée sur l'analyse lexicale. Cette opération prend généralement beaucoup plus de temps que la lecture de texte à partir du disque. Quel est le piège? Dans un fil, le texte est lu à partir du fichier, tandis que l'analyse lexicale est effectuée dans un fil différent.

Le lexer lit le texte ligne par ligne. S'il demande une ligne qui n'existe pas, il s'arrêtera et attendra.

BlockingCollection de BCL fonctionne sur une base similaire, et l'algorithme comprend une application typique d'un modèle producteur-consommateur simultané. L'éditeur travaillant dans le thread principal demande des données sur la première ligne en surbrillance, et s'il n'est pas disponible, il s'arrêtera et attendra. Dans notre éditeur, nous avons utilisé deux fois le modèle producteur-consommateur et la collection bloquante :

  1. La lecture d'un fichier est un Producteur, tandis que le lexer est un Consommateur.
  2. Lexer est déjà un Producteur et l'éditeur de texte est un Consommateur.

Cet ensemble d'astuces nous permet de raccourcir considérablement le temps passé à ouvrir des fichiers volumineux. La première page du document s'affiche très rapidement, cependant, le document peut se figer si les utilisateurs essaient d'aller à la fin du fichier dans les premières secondes. Cela se produit parce que le lecteur en arrière-plan et le lexer doivent atteindre la fin du document. Cependant, si l'utilisateur travaille lentement du début du document vers la fin, il n'y aura pas de blocage notable.

Optimisation ambiguë :analyse lexicale partielle

L'analyse syntaxique est généralement divisée en deux niveaux :

  • le flux de caractères d'entrée est traité pour obtenir des lexèmes (jetons) basés sur les règles de la langue - c'est ce qu'on appelle l'analyse lexicale
  • l'analyseur consomme le flux de jetons en le vérifiant conformément aux règles de grammaire formelles et construit souvent un arbre de syntaxe.

Le traitement des chaînes est une opération coûteuse. Pour l'optimiser, nous avons décidé de ne pas effectuer une analyse lexicale complète du texte à chaque fois mais de ne réanalyser que la partie qui a été modifiée. Mais comment gérer les constructions multilignes comme les commentaires de bloc ou les lignes ? Nous avons stocké un état de fin de ligne pour chaque ligne :"pas de jetons multilignes" =0, "le début d'un commentaire de bloc" =1, "le début d'un littéral de chaîne multiligne" =2. L'analyse lexicale commence à partir de la section modifiée et se termine lorsque l'état de fin de ligne est égal à celui stocké.

Il y avait un problème avec cette solution :il est extrêmement peu pratique de surveiller les numéros de ligne dans de telles structures alors que le numéro de ligne est un attribut obligatoire d'un jeton ANTLR, car lorsqu'une ligne est insérée ou supprimée, le numéro de la ligne suivante doit être mis à jour en conséquence. Nous l'avons résolu en définissant un numéro de ligne immédiatement, avant de remettre le jeton à l'analyseur. Les tests que nous avons effectués plus tard ont montré que les performances s'amélioraient de 15 à 25 %. L'amélioration réelle était encore plus importante.

La quantité de RAM requise pour tout cela s'est avérée bien supérieure à ce à quoi nous nous attendions. Un jeton ANTLR consistait en :un point de départ - 8 octets, un point final - 8 octets, un lien vers le texte du mot - 4 ou 8 octets (sans mentionner la chaîne elle-même), un lien vers le texte du document - 4 ou 8 octets, et un type de jeton - 4 octets.

Alors que pouvons-nous conclure ? Nous nous sommes concentrés sur les performances et avons obtenu une consommation excessive de RAM dans un endroit auquel nous ne nous attendions pas. Nous ne pensions pas que cela se produirait car nous avons essayé d'utiliser des structures légères au lieu de classes. En les remplaçant par des objets lourds, nous avons sciemment opté pour des dépenses mémoire supplémentaires pour obtenir de meilleures performances. Heureusement, cela nous a appris une leçon importante, donc maintenant chaque optimisation des performances se termine par le profilage de la consommation de mémoire et vice versa.

C'est une histoire avec une morale. Certaines fonctionnalités ont commencé à fonctionner presque instantanément et d'autres un peu plus rapidement. Après tout, il serait impossible d'effectuer l'astuce d'analyse lexicale en arrière-plan s'il n'y avait pas un objet où l'un des threads pourrait stocker des jetons.

Tous les autres problèmes se déroulent dans le contexte du développement de bureau sur la pile .NET.

Le problème 32 bits

Certains utilisateurs choisissent d'utiliser des versions autonomes de nos produits. D'autres s'en tiennent à travailler dans Visual Studio et SQL Server Management Studio. De nombreuses extensions sont développées pour eux. L'une de ces extensions est SQL Complete. Pour clarifier, il fournit plus de pouvoirs et de fonctionnalités que le standard SSMS et VS pour SQL.

L'analyse SQL est un processus très coûteux, à la fois en termes de ressources CPU et RAM. Pour demander la liste des objets dans les scripts utilisateur, sans appels inutiles au serveur, nous stockons le cache d'objets dans la RAM. Souvent, cela ne prend pas beaucoup de place, mais certains de nos utilisateurs ont des bases de données contenant jusqu'à un quart de million d'objets.

Travailler avec SQL est assez différent de travailler avec d'autres langages. En C#, il n'y a pratiquement pas de fichiers même avec mille lignes de code. Pendant ce temps, en SQL, un développeur peut travailler avec un vidage de base de données composé de plusieurs millions de lignes de code. Il n'y a rien d'inhabituel à cela.

DLL-Hell dans VS

Il existe un outil pratique pour développer des plugins dans .NET Framework, c'est un domaine d'application. Tout est exécuté de manière isolée. Il est possible de décharger. Dans la plupart des cas, la mise en œuvre d'extensions est peut-être la principale raison pour laquelle les domaines d'application ont été introduits.

En outre, il existe le cadre MAF, qui a été conçu par MS pour résoudre le problème de la création de modules complémentaires au programme. Il isole ces add-ons à tel point qu'il peut les envoyer à un processus séparé et prendre en charge toutes les communications. Franchement, cette solution est trop lourde et n'a pas gagné beaucoup de popularité.

Malheureusement, Microsoft Visual Studio et SQL Server Management Studio construits dessus, implémentent le système d'extension différemment. Cela simplifie l'accès aux applications d'hébergement pour les plugins, mais les oblige à s'intégrer dans un processus et un domaine avec un autre.

Comme toute autre application du 21ème siècle, la nôtre a beaucoup de dépendances. La majorité d'entre elles sont des bibliothèques bien connues, éprouvées et populaires dans le monde .NET.

Insérer des messages dans une serrure

Il n'est pas largement connu que .NET Framework pompera la file d'attente de messages Windows à l'intérieur de chaque WaitHandle. Pour le mettre à l'intérieur de chaque verrou, n'importe quel gestionnaire d'événement dans une application peut être appelé si ce verrou a le temps de passer en mode noyau et qu'il n'est pas libéré pendant la phase d'attente.

Cela peut entraîner une réentrée dans certains endroits très inattendus. Quelques fois, cela a conduit à des problèmes tels que "La collection a été modifiée lors de l'énumération" et diverses ArgumentOutOfRangeException.

Ajout d'un assembly à une solution à l'aide de SQL

Lorsque le projet grandit, la tâche d'ajouter des assemblages, simple au début, se transforme en une douzaine d'étapes compliquées. Une fois, nous avons dû ajouter une dizaine d'assemblages différents à la solution, nous avons effectué un gros refactoring. Près de 80 solutions, y compris des produits et des tests, ont été créées sur la base d'environ 300 projets .NET.

Sur la base des solutions produits, nous avons écrit les fichiers Inno Setup. Ils incluaient des listes d'assemblys empaquetés dans l'installation que l'utilisateur avait téléchargés. L'algorithme d'ajout d'un projet était le suivant :

  1. Créer un nouveau projet.
  2. Ajoutez-y un certificat. Configurez la balise de la construction.
  3. Ajouter un fichier de version.
  4. Reconfigurer les chemins d'accès au projet.
  5. Renommer le dossier pour qu'il corresponde à la spécification interne.
  6. Ajoutez à nouveau le projet à la solution.
  7. Ajoutez quelques assemblys vers lesquels tous les projets ont besoin de liens.
  8. Ajoutez la version à toutes les solutions nécessaires :test et produit.
  9. Pour toutes les solutions produit, ajoutez les assemblages à l'installation.

Ces 9 étapes ont dû être répétées environ 10 fois. Les étapes 8 et 9 ne sont pas si triviales, et il est facile d'oublier d'ajouter des builds partout.

Confronté à une tâche aussi importante et routinière, tout programmeur normal voudrait l'automatiser. C'est exactement ce que nous voulions faire. Mais comment indiquer exactement quelles solutions et installations ajouter au projet nouvellement créé ? Il y a tellement de scénarios et de plus, il est difficile de prévoir certains d'entre eux.

Nous avons eu une idée folle. Les solutions sont connectées à des projets comme plusieurs à plusieurs, des projets avec des installations de la même manière, et SQL peut résoudre exactement le type de tâches que nous avions.

Nous avons créé une application .Net Core Console qui analyse tous les fichiers .sln dans le dossier source, en récupère la liste des projets à l'aide de DotNet CLI et la place dans la base de données SQLite. Le programme a quelques modes :

  • Nouveau :crée un projet et tous les dossiers nécessaires, ajoute un certificat, configure une balise, ajoute une version, un minimum d'assemblages essentiels.
  • Add-Project - ajoute le projet à toutes les solutions qui satisfont la requête SQL qui sera donnée comme l'un des paramètres. Pour ajouter le projet à la solution, le programme à l'intérieur utilise DotNet CLI.
  • Add-ISS :ajoute le projet à toutes les installations, qui satisfont les requêtes SQL.

Bien que l'idée d'indiquer la liste des solutions via la requête SQL puisse sembler lourde, elle a complètement fermé tous les cas existants et très probablement tous les cas possibles à l'avenir.

Laissez-moi vous montrer le scénario. Créer un projet "A" et ajoutez-le à toutes les solutions où les projets "B" est utilisé :

dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"

Un problème avec LiteDB

Il y a quelques années, nous avons eu pour tâche de développer une fonction d'arrière-plan pour enregistrer les documents des utilisateurs. Il comportait deux flux d'application principaux :la possibilité de fermer instantanément l'IDE et de partir, et en revenant à partir de là où vous vous étiez arrêté, et la possibilité de restaurer dans des situations urgentes telles que des pannes ou des plantages de programme.

Pour mettre en œuvre cette tâche, il était nécessaire de sauvegarder le contenu des fichiers quelque part sur le côté, et de le faire souvent et rapidement. Outre le contenu, il était nécessaire de sauvegarder certaines métadonnées, ce qui rendait le stockage direct dans le système de fichiers gênant.

À ce stade, nous avons trouvé la bibliothèque LiteDB, qui nous a impressionnés par sa simplicité et ses performances. LiteDB est une base de données embarquée légère et rapide, qui a été entièrement écrite en C#. La rapidité et la simplicité générale nous ont conquis.

Au cours du processus de développement, toute l'équipe était satisfaite de travailler avec LiteDB. Les principaux problèmes, cependant, ont commencé après la sortie.

La documentation officielle garantissait que la base de données assurait un bon fonctionnement avec un accès simultané à partir de plusieurs threads ainsi que de plusieurs processus. Des tests synthétiques agressifs ont montré que la base de données ne fonctionnait pas correctement dans un environnement multithread.

Pour résoudre rapidement le problème, nous avons synchronisé les processus à l'aide de l'interprocessus auto-écrit ReadWriteLock. Maintenant, après presque trois ans, LiteDB fonctionne beaucoup mieux.

StreamStringList

Ce problème est à l'opposé du cas de l'analyse lexicale partielle. Lorsque nous travaillons avec un texte, il est plus pratique de l'utiliser sous forme de liste de chaînes. Les chaînes peuvent être demandées dans un ordre aléatoire, mais une certaine densité d'accès à la mémoire est toujours présente. À un moment donné, il était nécessaire d'exécuter plusieurs tâches pour traiter de très gros fichiers sans charge de mémoire complète. L'idée était la suivante :

  1. Pour lire le fichier ligne par ligne. Mémoriser les décalages dans le fichier.
  2. Sur demande, émettez la ligne suivante, définissez un décalage requis et renvoyez les données.

La tâche principale est terminée. Cette structure ne prend pas beaucoup de place par rapport à la taille du fichier. Lors de la phase de test, nous vérifions minutieusement l'empreinte mémoire des fichiers volumineux et très volumineux. Les gros fichiers ont été traités pendant longtemps et les petits seront traités immédiatement.

Il n'y avait aucune référence pour vérifier l'heure d'exécution . La RAM est appelée Random Access Memory - c'est son avantage concurrentiel par rapport au SSD et surtout au disque dur. Ces pilotes commencent à mal fonctionner pour un accès aléatoire. Il s'est avéré que cette approche ralentissait le travail de près de 40 fois, par rapport au chargement complet d'un fichier en mémoire. De plus, nous lisons le fichier 2,5 à 10 fois selon le contexte.

La solution était simple et l'amélioration était suffisante pour que l'opération ne prenne qu'un peu plus de temps que lorsque le fichier est entièrement chargé en mémoire.

De même, la consommation de RAM était également insignifiante. Nous nous sommes inspirés du principe de chargement des données de la RAM dans un processeur de cache. Lorsque vous accédez à un élément du tableau, le processeur copie des dizaines d'éléments voisins dans son cache car les éléments nécessaires sont souvent à proximité.

De nombreuses structures de données utilisent cette optimisation du processeur pour obtenir des performances optimales. C'est à cause de cette particularité que l'accès aléatoire aux éléments du tableau est beaucoup plus lent que l'accès séquentiel. Nous avons implémenté un mécanisme similaire :nous avons lu un ensemble de mille chaînes et nous nous sommes souvenus de leurs décalages. Lorsque nous accédons à la 1001e chaîne, nous supprimons les 500 premières chaînes et chargeons les 500 suivantes. Si nous avons besoin de l'une des 500 premières lignes, nous y allons séparément, car nous avons déjà le décalage.

Le programmeur n'a pas nécessairement besoin de formuler et de vérifier soigneusement les exigences non fonctionnelles. En conséquence, nous nous sommes souvenus pour les cas futurs que nous devions travailler séquentiellement avec la mémoire persistante.

Analyser les exceptions

Vous pouvez facilement collecter des données sur l'activité des utilisateurs sur le Web. Cependant, ce n'est pas le cas avec l'analyse des applications de bureau. Il n'existe aucun outil de ce type capable de fournir un ensemble incroyable de mesures et d'outils de visualisation tels que Google Analytics. Pourquoi? Voici mes hypothèses :

  1. Pendant la majeure partie de l'histoire du développement d'applications de bureau, ils n'ont eu aucun accès stable et permanent au Web.
  2. Il existe de nombreux outils de développement pour les applications de bureau. Par conséquent, il est impossible de créer un outil de collecte de données utilisateur polyvalent pour tous les cadres et technologies d'interface utilisateur.

Un aspect clé de la collecte de données est le suivi des exceptions. Par exemple, nous recueillons des données sur les accidents. Auparavant, nos utilisateurs devaient écrire eux-mêmes à l'e-mail du support client, en ajoutant une Stack Trace d'une erreur, qui était copiée à partir d'une fenêtre d'application spéciale. Peu d'utilisateurs ont suivi toutes ces étapes. Les données collectées sont totalement anonymisées, ce qui nous prive de la possibilité de connaître les étapes de reproduction ou toute autre information de l'utilisateur.

D'autre part, les données d'erreur se trouvent dans la base de données Postgres, ce qui ouvre la voie à une vérification instantanée de dizaines d'hypothèses. Vous pouvez obtenir immédiatement les réponses en faisant simplement des requêtes SQL à la base de données. Il n'est souvent pas clair d'un seul type de pile ou d'exception comment l'exception s'est produite, c'est pourquoi toutes ces informations sont essentielles pour étudier le problème.

En plus de cela, vous avez la possibilité d'analyser toutes les données collectées et de trouver les modules et les classes les plus problématiques. En vous appuyant sur les résultats de l'analyse, vous pouvez planifier une refactorisation ou des tests supplémentaires pour couvrir ces parties du programme.

Service de décodage de pile

Les versions .NET contiennent du code IL, qui peut être facilement reconverti en code C #, précis pour l'opérateur, à l'aide de plusieurs programmes spéciaux. L'un des moyens de protéger le code du programme est son obscurcissement. Les programmes peuvent être renommés; les méthodes, les variables et les classes peuvent être remplacées ; le code peut être remplacé par son équivalent, mais c'est vraiment incompréhensible.

La nécessité d'obscurcir le code source apparaît lorsque vous distribuez votre produit d'une manière suggérant que l'utilisateur obtient les versions de votre application. Les applications de bureau sont ces cas. Toutes les versions, y compris les versions intermédiaires pour les testeurs, sont soigneusement masquées.

Notre unité d'assurance qualité utilise les outils de pile de décodage du développeur de l'obfuscateur. Pour commencer le décodage, ils doivent exécuter l'application, rechercher les cartes de désobscurcissement publiées par CI pour une version spécifique et insérer la pile d'exceptions dans le champ de saisie.

Différentes versions et éditeurs étaient obscurcis de manière différente, ce qui rendait difficile pour un développeur d'étudier le problème ou pouvait même le mettre sur la mauvaise voie. Il était évident que ce processus devait être automatisé.

Le format de la carte de désobscurcissement s'est avéré assez simple. Nous l'avons facilement déparsé et avons écrit un programme de décodage de pile. Peu de temps avant cela, une interface utilisateur Web a été développée pour restituer les exceptions par version de produit et les regrouper par pile. Il s'agissait d'un site Web .NET Core avec une base de données en SQLite.

SQLite est un outil soigné pour les petites solutions. Nous avons également essayé d'y mettre des cartes de désobscurcissement. Chaque version a généré environ 500 000 paires de chiffrement et de déchiffrement. SQLite ne pouvait pas gérer un taux d'insertion aussi agressif.

Alors que les données d'une version ont été insérées dans la base de données, deux autres ont été ajoutées à la file d'attente. Peu de temps avant ce problème, j'écoutais un reportage sur Clickhouse et j'avais hâte de l'essayer. Il s'est avéré excellent, le taux d'insertion a été multiplié par plus de 200.

Cela dit, le décodage de la pile (lecture à partir de la base de données) a été ralenti de près de 50 fois, mais comme chaque pile prenait moins de 1 ms, il était peu rentable de passer du temps à étudier ce problème.

ML.NET for classification of exceptions

On the subject of the automatic processing of exceptions, we made a few more enhancements.

We already had the Web-UI for a convenient review of exceptions grouped by stacks. We had a Grafana for high-level analysis of product stability at the level of versions and product lines. But a programmer’s eye, constantly craving optimization, caught another aspect of this process.

Historically, dbForge line development was divided among 4 teams. Each team had its own functionality to work on, though the borderline was not always obvious. Our technical support team, relying on their experience, read the stack and assigned it to this or that team. They managed it quite well, yet, in some cases, mistakes occurred. The information on errors from analytics came to Jira on its own, but the support team still needed to classify tasks by team.

In the meantime, Microsoft introduced a new library – ML.NET. And we still had this classification task. A library of that kind was exactly what we needed. It extracted stacks of all resolved exceptions from Redmine, a project management system that we used earlier, and Jira, which we use at present.

We obtained a data array that contained some 5 thousand pairs of Exception StackTrace and command. We put 100 exceptions aside and used the rest of the exceptions to teach a model. The accuracy was about 75%. Again, we had 4 teams, hence, random and round-robin would only attain 25%. It sufficiently saved up their time.

To my way of thinking, if we considerably clean up incoming data array, make a thorough examination of the ML.NET library, and theoretical foundation in machine learning, on the whole, we can improve these results. At the same time, I was impressed with the simplicity of this library:with no special knowledge in AI and ML, we managed to gain real cost-benefits in less than an hour.

Conclusion

Hopefully, some of the readers happen to be users of the products I describe in this article, and some lines shed light on the reasons why this or that function was implemented this way.

And now, let me conclude:

We should make decisions based on data and not assumptions. It is about behavior analytics and insights that we can obtain from it.

We ought to constantly invest in tools. There is nothing wrong if we need to develop something for it. In the next few months, it will save us a lot of time and rid us of routine. Routine on top of time expenditure can be very demotivating.

When we develop some internal tools, we get a super chance to try out new technologies, which can be applied in production solutions later on.

There are infinitely many tools for data analysis. Still, we managed to extract some crucial information using SQL tools. This is the most powerful tool to formulate a question to data and receive an answer in a structured form.