sql >> Database >  >> RDS >> Database

SQL voltooien. Verhalen van succes en mislukking

Ik werk al meer dan vijf jaar voor een bedrijf dat IDE's ontwikkelt voor database-interactie. Voordat ik aan dit artikel begon, had ik geen idee hoeveel fantasieverhalen er in het verschiet zouden liggen.

Mijn team ontwikkelt en ondersteunt IDE-taalfuncties, en het automatisch aanvullen van code is de belangrijkste. Ik heb veel spannende dingen meegemaakt. Sommige dingen deden we vanaf de eerste poging geweldig, en andere faalden zelfs na verschillende schoten.

SQL en dialecten ontleden

SQL is een poging om eruit te zien als een natuurlijke taal, en de poging is behoorlijk succesvol, moet ik zeggen. Afhankelijk van het dialect zijn er enkele duizenden trefwoorden. Om de ene uitspraak van de andere te onderscheiden, moet je vaak vooruit zoeken naar een of twee woorden (tokens). Deze benadering wordt een vooruitblik genoemd .

Er is een classificatie van de parser, afhankelijk van hoe ver ze vooruit kunnen kijken:LA(1), LA(2) of LA(*), wat betekent dat een parser zo ver vooruit kan kijken als nodig is om de juiste fork te definiëren.

Soms komt het einde van een optionele clausule overeen met het begin van een andere optionele clausule. Deze situaties maken het parsen veel moeilijker om uit te voeren. T-SQL maakt het er niet makkelijker op. Ook kunnen sommige SQL-instructies, maar niet noodzakelijkerwijs, eindes hebben die in strijd kunnen zijn met het begin van eerdere instructies.

Geloof je het niet? Er is een manier om formele talen te beschrijven via grammatica. Je kunt er een parser van maken met deze of gene tool. De meest opvallende tools en talen die grammatica beschrijven zijn YACC en ANTLR.

YACC -gegenereerde parsers worden gebruikt in MySQL-, MariaDB- en PostgreSQL-engines. We zouden kunnen proberen ze rechtstreeks uit de broncode te halen en code-voltooiing te ontwikkelen en andere functies op basis van de SQL-analyse met behulp van deze parsers. Bovendien zou dit product gratis ontwikkelingsupdates ontvangen en zou de parser zich op dezelfde manier gedragen als de bronengine.

Dus waarom gebruiken we nog steeds ANTLR ? Het ondersteunt C#/.NET stevig, heeft een degelijke toolkit, de syntaxis is veel gemakkelijker te lezen en te schrijven. ANTLR-syntaxis werd zo handig dat Microsoft het nu gebruikt in zijn officiële C#-documentatie.

Maar laten we teruggaan naar de complexiteit van SQL als het gaat om parseren. Ik wil graag de grammatica van de openbaar beschikbare talen vergelijken. In dbForge gebruiken we onze stukjes grammatica. Ze zijn completer dan de andere. Helaas zijn ze overladen met de inserts van de C#-code voor het ondersteunen van verschillende functies.

De grammaticagroottes voor verschillende talen zijn als volgt:

JS – 475 rijen parser + 273 lexers =748 rijen

Java – 615 rijen parser + 211 lexers =826 rijen

C# – 1159 parserrijen + 433 lexers =1592 rijen

С++ – 1933 rijen

MySQL – 2515 parserrijen + 1189 lexers =3704 rijen

T-SQL – 4035 parserrijen + 896 lexers =4931 rijen

PL SQL – 6719 parserrijen + 2366 lexers =9085 rijen

De uitgangen van sommige lexers bevatten de lijsten van de Unicode-tekens die in de taal beschikbaar zijn. Die lijsten zijn nutteloos voor de evaluatie van taalcomplexiteit. Het aantal rijen dat ik nam eindigde dus altijd vóór deze lijsten.

Het evalueren van de complexiteit van taalparsing op basis van het aantal rijen in de taalgrammatica is discutabel. Toch denk ik dat het belangrijk is om de cijfers te tonen die een enorme discrepantie laten zien.

Dat is niet alles. Aangezien we een IDE aan het ontwikkelen zijn, moeten we omgaan met onvolledige of ongeldige scripts. We moesten veel trucjes bedenken, maar klanten sturen nog steeds veel werkende scenario's met onvoltooide scripts. We moeten dit oplossen.

Predikaatoorlogen

Tijdens het ontleden van de code vertelt het woord je soms niet welke van de twee alternatieven je moet kiezen. Het mechanisme dat dit soort onnauwkeurigheden oplost, is vooruitkijken in ANTLR. De parsermethode is de ingevoegde keten van if's , en elk van hen kijkt een stap vooruit. Zie het voorbeeld van de grammatica die dit soort onzekerheid genereert:

rule1:
  'a' rule2 | rule3
;

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

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

In het midden van de regel1, wanneer het token 'a' al is doorgegeven, kijkt de parser twee stappen vooruit om de te volgen regel te kiezen. Deze controle wordt nogmaals uitgevoerd, maar deze grammatica kan worden herschreven om de vooruitblik uit te sluiten . Het nadeel is dat dergelijke optimalisaties de structuur schaden, terwijl de prestatieverbetering vrij klein is.

Er zijn complexere manieren om dit soort onzekerheid op te lossen. Bijvoorbeeld het Syntaxispredikaat (SynPred) mechanisme in ANTLR3 . Het helpt als het optionele einde van een clausule het begin van de volgende optionele clausule kruist.

In termen van ANTLR3 is een predikaat een gegenereerde methode die een virtuele tekstinvoer uitvoert volgens een van de alternatieven . Als dit lukt, retourneert het de true waarde, en het predikaat voltooiing is succesvol. Als het een virtueel item is, wordt het een backtracking . genoemd modus invoer. Als een predikaat succesvol werkt, vindt de echte invoer plaats.

Het is alleen een probleem wanneer een predikaat binnen een ander predikaat begint. Dan kan een afstand honderden of duizenden keren worden overschreden.

Laten we een vereenvoudigd voorbeeld bekijken. Er zijn drie punten van onzekerheid:(A, B, C).

  1. De parser voert A in, onthoudt zijn positie in de tekst en start een virtuele invoer op niveau-1.
  2. De parser voert B in, onthoudt zijn positie in de tekst en start een virtuele invoer op niveau 2.
  3. De parser voert C in, onthoudt zijn positie in de tekst en start een virtuele invoer op niveau 3.
  4. De parser voltooit een virtuele invoer van niveau 3, keert terug naar niveau-2 en passeert nogmaals C.
  5. De parser voltooit een virtuele invoer van niveau-2, keert terug naar niveau-1 en passeert nogmaals B en C.
  6. De parser voltooit een virtuele invoer, keert terug en voert een echte invoer uit via A, B en C.

Hierdoor worden alle controles binnen C 4 keer uitgevoerd, binnen B – 3 keer, binnen A – 2 keer.

Maar wat als een passend alternatief op de tweede of derde plaats in de lijst staat? Dan zal een van de predikaatfasen mislukken. De positie in de tekst wordt teruggedraaid en een ander predikaat begint te lopen.

Bij het analyseren van redenen voor het bevriezen van de app, stuiten we vaak op het spoor van SynPred duizenden keren geëxecuteerd. SynPred s zijn vooral problematisch in recursieve regels. Helaas is SQL van nature recursief. De mogelijkheid om bijna overal subquery's te gebruiken, heeft zijn prijs. Het is echter mogelijk om de regel te manipuleren om een ​​predikaat te laten verdwijnen.

SynPred doet afbreuk aan de prestaties. Op een gegeven moment werd hun aantal onder strenge controle geplaatst. Maar het probleem is dat wanneer u grammaticacode schrijft, SynPred voor u onzichtbaar kan lijken. Meer nog, het wijzigen van een regel kan ertoe leiden dat SynPred in een andere regel verschijnt, en dat maakt controle over hen praktisch onmogelijk.

We hebben een eenvoudige reguliere uitdrukking . gemaakt tool voor het controleren van het aantal predikaten dat wordt uitgevoerd door de speciale MSBuild-taak . Als het aantal predikaten niet overeenkwam met het aantal dat in een bestand is opgegeven, mislukte de build onmiddellijk en werd gewaarschuwd voor een fout.

Bij het zien van de fout moet een ontwikkelaar de code van de regel meerdere keren herschrijven om de overbodige predikaten te verwijderen. Als men predikaten niet kan vermijden, zou de ontwikkelaar het toevoegen aan een speciaal bestand dat extra aandacht trekt voor de beoordeling.

In zeldzame gevallen hebben we onze predikaten zelfs met C# geschreven om de door ANTLR gegenereerde predikaten te vermijden. Gelukkig bestaat deze methode ook.

Grammatica overerving

Als er wijzigingen zijn in onze ondersteunde DBMS'en, moeten we deze in onze tools opnemen. Ondersteuning voor grammaticale syntaxisconstructies is altijd een startpunt.

We creëren een speciale grammatica voor elk SQL-dialect. Het maakt wat herhaling van de code mogelijk, maar het is gemakkelijker dan proberen te vinden wat ze gemeen hebben.

We gingen voor het schrijven van onze eigen ANTLR grammatica-preprocessor die de grammatica-overerving doet.

Het werd ook duidelijk dat we een mechanisme voor polymorfisme nodig hadden - het vermogen om niet alleen de regel in de afstammeling opnieuw te definiëren, maar ook de basisregel aan te roepen. We willen ook graag de positie bepalen bij het aanroepen van de basisregel.

Tools zijn zeker een pluspunt als we ANTLR vergelijken met andere taalherkenningstools, Visual Studio en ANTLRWorks. En u wilt dit voordeel niet verliezen tijdens het uitvoeren van de erfenis. De oplossing was het specificeren van basisgrammatica in een overgeërfde grammatica in een ANTLR-commentaarformaat. Voor ANTLR-tools is het slechts een opmerking, maar we kunnen er alle benodigde informatie uit halen.

We schreven een MsBuild-taak die als pre-build-actie in het hele build-systeem was ingebed. De taak was om het werk van een preprocessor voor ANTLR-grammatica te doen door de resulterende grammatica te genereren uit de basis en geërfde peers. De resulterende grammatica is door ANTLR zelf verwerkt.

ANTLR-nabewerking

In veel programmeertalen kunnen trefwoorden niet worden gebruikt als onderwerpnamen. Afhankelijk van het dialect kunnen er 800 tot 3000 trefwoorden in SQL zijn. De meeste zijn gekoppeld aan de context in databases. Dus het verbieden ervan als objectnamen zou gebruikers frustreren. Daarom heeft SQL gereserveerde en niet-gereserveerde zoekwoorden.

U kunt uw object geen naam geven als het gereserveerde woord (SELECT, FROM, etc.) zonder het te citeren, maar u kunt dit doen met een niet-gereserveerd woord (CONVERSATION, AVAILABILITY, etc.). Deze interactie maakt de ontwikkeling van de parser moeilijker.

Tijdens de lexicale analyse is de context onbekend, maar een parser vereist al verschillende getallen voor de identifier en het trefwoord. Daarom hebben we nog een nabewerking toegevoegd aan de ANTLR-parser. Het verving alle voor de hand liggende identificatiecontroles door een speciale methode aan te roepen.

Deze methode heeft een meer gedetailleerde controle. Als het item een ​​identifier aanroept, en we verwachten dat aan de identifier wordt voldaan, dan is het allemaal goed. Maar als een niet-gereserveerd woord een invoer is, moeten we het dubbel controleren. Deze extra controle beoordeelt de branchezoekopdracht in de huidige context waarin dit niet-gereserveerde trefwoord een trefwoord kan zijn. Als er geen vertakkingen zijn, kan deze als identificatie worden gebruikt.

Technisch gezien zou dit probleem kunnen worden opgelost door middel van ANTLR, maar deze beslissing is niet optimaal. De ANTLR-manier is om een ​​regel te maken die alle niet-gereserveerde trefwoorden en een lexeme-ID weergeeft. Verderop zal een speciale regel dienen in plaats van een lexeme-ID. Deze oplossing zorgt ervoor dat een ontwikkelaar niet vergeet het trefwoord toe te voegen waar het wordt gebruikt en in de speciale regel. Het optimaliseert ook de bestede tijd.

Fouten in syntaxisanalyse zonder bomen

De syntaxisstructuur is meestal het resultaat van parserwerk. Het is een gegevensstructuur die de programmatekst weerspiegelt door middel van formele grammatica. Als u een code-editor wilt implementeren met de taal automatisch aanvullen, krijgt u hoogstwaarschijnlijk het volgende algoritme:

  1. Ontleed de tekst in de editor. Dan krijg je een syntaxisboom.
  2. Zoek een knoop onder de wagen en vergelijk deze met de grammatica.
  3. Ontdek welke trefwoorden en objecttypes beschikbaar zullen zijn bij de Point.

In dit geval is de grammatica gemakkelijk voor te stellen als een grafiek of een toestandsmachine.

Helaas was pas de derde versie van ANTLR beschikbaar toen de dbForge IDE met zijn ontwikkeling was begonnen. Het was echter niet zo wendbaar en hoewel je ANTLR kon vertellen hoe je een boom moest bouwen, was het gebruik niet soepel.

Bovendien suggereerden veel artikelen over dit onderwerp om het 'acties'-mechanisme te gebruiken voor het uitvoeren van code wanneer de parser de regel passeerde. Dit mechanisme is erg handig, maar het heeft geleid tot architecturale problemen en heeft het ondersteunen van nieuwe functionaliteit complexer gemaakt.

Het punt is dat een enkel grammaticabestand begon met het verzamelen van 'acties' vanwege het grote aantal functionaliteiten die liever naar verschillende builds hadden gedistribueerd. We zijn erin geslaagd om actie-handlers naar verschillende builds te distribueren en voor die maatregel een stiekeme patroonvariatie voor abonnee-meldingen te maken.

ANTLR3 werkt volgens onze metingen 6 keer sneller dan ANTLR4. Ook kon de syntaxisstructuur voor grote scripts te veel RAM in beslag nemen, wat geen goed nieuws was, dus moesten we werken binnen de 32-bits adresruimte van Visual Studio en SQL Management Studio.

ANTLR-parser-nabewerking

Bij het werken met strings is een van de meest kritieke momenten de fase van lexicale analyse waarin we het script in afzonderlijke woorden verdelen.

ANTLR neemt als invoergrammatica die de taal specificeert en voert een parser uit in een van de beschikbare talen. Op een gegeven moment groeide de gegenereerde parser zo sterk dat we bang waren om deze te debuggen. Als u tijdens het debuggen op F11 drukt (instappen) en naar het parserbestand gaat, crasht Visual Studio gewoon.

Het bleek dat het mislukte vanwege een OutOfMemory-uitzondering bij het analyseren van het parserbestand. Dit bestand bevat meer dan 200.000 regels code.

Maar het debuggen van de parser is een essentieel onderdeel van het werkproces en je kunt het niet weglaten. Met behulp van gedeeltelijke C#-klassen hebben we de gegenereerde parser geanalyseerd met behulp van reguliere expressies en deze in een paar bestanden verdeeld. Visual Studio werkte er perfect mee.

Lexicale analyse zonder subtekenreeks vóór Span API

De belangrijkste taak van lexicale analyse is classificatie - het definiëren van de grenzen van de woorden en deze vergelijken met een woordenboek. Als het woord wordt gevonden, geeft de lexer zijn index terug. Als dit niet het geval is, wordt het woord beschouwd als een object-ID. Dit is een vereenvoudigde beschrijving van het algoritme.

Achtergrond lexing tijdens het openen van bestanden

Syntaxisaccentuering is gebaseerd op lexicale analyse. Deze bewerking kost gewoonlijk veel meer tijd in vergelijking met het lezen van tekst van de schijf. Wat is het addertje onder het gras? In de ene thread wordt de tekst uit het bestand gelezen, terwijl de lexicale analyse in een andere thread wordt uitgevoerd.

De lexer leest de tekst rij voor rij voor. Als het een rij vraagt ​​die niet bestaat, stopt het en wacht.

BlockingCollection van BCL werkt op een vergelijkbare basis en het algoritme omvat een typische toepassing van een gelijktijdig Producer-Consumer-patroon. De editor die in de hoofdthread werkt, vraagt ​​om gegevens over de eerste gemarkeerde regel en als deze niet beschikbaar is, stopt deze en wacht. In onze editor hebben we het producer-consumer-patroon en blocking-collection twee keer gebruikt:

  1. Lezen uit een bestand is een Producer, terwijl Lexer een Consument is.
  2. Lexer is al een producent en de teksteditor is een consument.

Met deze reeks trucs kunnen we de tijd die wordt besteed aan het openen van grote bestanden aanzienlijk verkorten. De eerste pagina van het document wordt zeer snel weergegeven, maar het document kan vastlopen als gebruikers binnen de eerste paar seconden naar het einde van het bestand proberen te gaan. Het gebeurt omdat de achtergrondlezer en lexer het einde van het document moeten bereiken. Als de gebruiker echter langzaam van het begin van het document naar het einde beweegt, zal er geen merkbare bevriezing zijn.

Ambigue optimalisatie:gedeeltelijke lexicale analyse

De syntactische analyse is meestal verdeeld in twee niveaus:

  • de invoertekenstroom wordt verwerkt om lexemen (tokens) te krijgen op basis van de taalregels - dit wordt lexicale analyse genoemd
  • de parser verbruikt tokenstroom en controleert deze volgens de formele grammaticaregels en bouwt vaak een syntaxisboom.

Stringverwerking is een kostbare operatie. Om het te optimaliseren, hebben we besloten om niet elke keer een volledige lexicale analyse van de tekst uit te voeren, maar alleen het gedeelte dat is gewijzigd opnieuw te analyseren. Maar hoe om te gaan met constructies met meerdere regels, zoals blokopmerkingen of regels? We hebben voor elke regel een regeleindstatus opgeslagen:"geen tokens met meerdere regels" =0, "het begin van een blokcommentaar" =1, "het begin van een letterlijke tekenreeks met meerdere regels" =2. De lexicale analyse begint bij de gewijzigde sectie en eindigt wanneer de regel-eindtoestand gelijk is aan de opgeslagen toestand.

Er was één probleem met deze oplossing:het is buitengewoon onhandig om regelnummers in dergelijke structuren te controleren, terwijl regelnummer een vereist attribuut is van een ANTLR-token, omdat wanneer een regel wordt ingevoegd of verwijderd, het nummer van de volgende regel dienovereenkomstig moet worden bijgewerkt. We hebben het opgelost door direct een regelnummer in te stellen, voordat we het token aan de parser overhandigen. De tests die we later hebben uitgevoerd, hebben aangetoond dat de prestaties met 15-25% verbeterden. De daadwerkelijke verbetering was zelfs nog groter.

De hoeveelheid RAM die voor dit alles nodig was, bleek veel meer te zijn dan we hadden verwacht. Een ANTLR-token bestond uit:een beginpunt - 8 bytes, een eindpunt - 8 bytes, een link naar de tekst van het woord - 4 of 8 bytes (zonder de string zelf te noemen), een link naar de tekst van het document - 4 of 8 bytes, en een tokentype – 4 bytes.

Dus wat kunnen we concluderen? We concentreerden ons op prestaties en kregen overmatig RAM-verbruik op een plek die we niet hadden verwacht. We gingen er niet vanuit dat dit zou gebeuren omdat we probeerden lichtgewicht structuren te gebruiken in plaats van klassen. Door ze te vervangen door zware voorwerpen, gingen we bewust voor extra geheugenkosten om betere prestaties te krijgen. Gelukkig heeft dit ons een belangrijke les geleerd, dus nu eindigt elke prestatie-optimalisatie met het profileren van geheugenverbruik en vice versa.

Dit is een verhaal met een moraal. Sommige functies begonnen bijna onmiddellijk te werken en andere net iets sneller. Het zou immers onmogelijk zijn om de truc met lexicale analyse op de achtergrond uit te voeren als er geen object was waar een van de threads tokens zou kunnen opslaan.

Alle verdere problemen ontvouwen zich in de context van desktopontwikkeling op de .NET-stack.

Het 32-bits probleem

Sommige gebruikers kiezen ervoor om stand-alone versies van onze producten te gebruiken. Anderen blijven werken in Visual Studio en SQL Server Management Studio. Er zijn veel extensies voor ontwikkeld. Een van deze extensies is SQL Complete. Ter verduidelijking, het biedt meer bevoegdheden en functies dan de standaard Code Completion SSMS en VS voor SQL.

SQL-parsing is een zeer kostbaar proces, zowel wat betreft CPU- als RAM-bronnen. Om de lijst met objecten in gebruikersscripts op te vragen, zonder onnodige oproepen naar de server, slaan we de objectcache op in RAM. Vaak neemt het niet veel ruimte in beslag, maar sommige van onze gebruikers hebben databases die tot een kwart miljoen objecten bevatten.

Werken met SQL is heel anders dan werken met andere talen. In C# zijn er praktisch geen bestanden, zelfs niet met duizend regels code. Ondertussen kan een ontwikkelaar in SQL werken met een databasedump die bestaat uit enkele miljoenen regels code. Er is niets ongewoons aan.

DLL-Hell in VS

Er is een handige tool om plug-ins te ontwikkelen in .NET Framework, het is een applicatiedomein. Alles wordt geïsoleerd uitgevoerd. Het is mogelijk om te lossen. Voor het grootste deel is de implementatie van extensies misschien wel de belangrijkste reden waarom applicatiedomeinen zijn geïntroduceerd.

Er is ook het MAF Framework, dat door MS is ontworpen om het probleem van het maken van add-ons voor het programma op te lossen. Het isoleert deze add-ons zodanig dat het ze naar een apart proces kan sturen en alle communicatie kan overnemen. Eerlijk gezegd is deze oplossing te omslachtig en heeft niet veel populariteit gewonnen.

Helaas hebben Microsoft Visual Studio en SQL Server Management Studio daarop voortgebouwd en het uitbreidingssysteem op een andere manier geïmplementeerd. Het vereenvoudigt de toegang tot hostingapplicaties voor plug-ins, maar dwingt ze om binnen het ene proces en domein bij een ander proces te passen.

Net als elke andere toepassing in de 21e eeuw, heeft de onze veel afhankelijkheden. De meeste van hen zijn bekende, beproefde en populaire bibliotheken in de .NET-wereld.

Berichten in een slot halen

Het is niet algemeen bekend dat .NET Framework Windows Message Queue in elke WaitHandle pompt. Om het in elke vergrendeling te plaatsen, kan elke handler van een gebeurtenis in een toepassing worden aangeroepen als deze vergrendeling tijd heeft om over te schakelen naar de kernelmodus en deze niet wordt vrijgegeven tijdens de spin-wait-fase.

Dit kan leiden tot herintreding op een aantal zeer onverwachte plaatsen. Een paar keer leidde het tot problemen zoals "Verzameling is gewijzigd tijdens opsomming" en verschillende ArgumentOutOfRangeException.

Een assembly toevoegen aan een oplossing met behulp van SQL

Wanneer het project groeit, ontwikkelt de taak van het toevoegen van assemblages, in het begin eenvoudig, zich tot een tiental ingewikkelde stappen. Toen we een keer een tiental verschillende samenstellingen aan de oplossing moesten toevoegen, voerden we een grote refactoring uit. Bijna 80 oplossingen, waaronder product- en testoplossingen, zijn gemaakt op basis van ongeveer 300 .NET-projecten.

Op basis van productoplossingen hebben we Inno Setup-bestanden geschreven. Ze bevatten lijsten met assemblages die zijn verpakt in de installatie die de gebruiker heeft gedownload. Het algoritme voor het toevoegen van een project was als volgt:

  1. Maak een nieuw project.
  2. Voeg er een certificaat aan toe. Stel de tag van de build in.
  3. Voeg een versiebestand toe.
  4. Configureer de paden waar het project naartoe gaat opnieuw.
  5. Hernoem de map zodat deze overeenkomt met de interne specificatie.
  6. Voeg het project nogmaals toe aan de oplossing.
  7. Voeg een aantal samenstellingen toe waarnaar alle projecten moeten worden gelinkt.
  8. Voeg de build toe aan alle benodigde oplossingen:test en product.
  9. Voeg voor alle productoplossingen de assemblages toe aan de installatie.

Deze 9 stappen moesten ongeveer 10 keer worden herhaald. Stappen 8 en 9 zijn niet zo triviaal en het is gemakkelijk om te vergeten overal builds toe te voegen.

Geconfronteerd met zo'n grote en routinematige taak, zou elke normale programmeur het willen automatiseren. Dat is precies wat we wilden doen. Maar hoe geven we aan welke oplossingen en installaties precies moeten worden toegevoegd aan het nieuw aangemaakte project? Er zijn zoveel scenario's en bovendien is het moeilijk om sommige ervan te voorspellen.

We kwamen op een gek idee. Oplossingen zijn verbonden met projecten zoals veel-op-veel, projecten met installaties op dezelfde manier, en SQL kan precies het soort taken oplossen dat we hadden.

We hebben een .Net Core Console-app gemaakt die alle .sln-bestanden in de bronmap scant, de lijst met projecten eruit haalt met behulp van DotNet CLI en deze in de SQLite-database plaatst. Het programma heeft een paar modi:

  • Nieuw – maakt een project en alle benodigde mappen, voegt een certificaat toe, stelt een tag in, voegt een versie toe, minimaal essentiële assemblages.
  • Add-Project – voegt het project toe aan alle oplossingen die voldoen aan de SQL-query die als een van de parameters wordt gegeven. Om het project aan de oplossing toe te voegen, gebruikt het programma binnenin DotNet CLI.
  • Add-ISS – voegt het project toe aan alle installaties die voldoen aan SQL-query's.

Hoewel het idee om de lijst met oplossingen via de SQL-query aan te geven misschien omslachtig lijkt, heeft het alle bestaande gevallen en hoogstwaarschijnlijk alle mogelijke gevallen in de toekomst volledig gesloten.

Laat me het scenario demonstreren. Maak een project “A” en voeg het toe aan alle oplossingen waar projecten “B” wordt gebruikt:

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

Een probleem met LiteDB

Een paar jaar geleden kregen we de opdracht om een ​​achtergrondfunctie te ontwikkelen voor het opslaan van gebruikersdocumenten. Het had twee belangrijke applicatiestromen:de mogelijkheid om de IDE onmiddellijk te sluiten en te verlaten, en bij terugkeer te beginnen waar je was gebleven, en de mogelijkheid om te herstellen in urgente situaties zoals black-outs of programmacrashes.

Om deze taak uit te voeren, was het noodzakelijk om de inhoud van de bestanden ergens aan de zijkant op te slaan, en dit vaak en snel te doen. Afgezien van de inhoud was het nodig om enkele metadata op te slaan, wat directe opslag in het bestandssysteem onhandig maakte.

Op dat moment vonden we de LiteDB-bibliotheek, die indruk op ons maakte met zijn eenvoud en prestaties. LiteDB is een snelle lichtgewicht embedded database, die volledig is geschreven in C#. De snelheid en algehele eenvoud hebben ons overtuigd.

In de loop van het ontwikkelproces was het hele team tevreden over de samenwerking met LiteDB. De belangrijkste problemen begonnen echter na de release.

De officiële documentatie garandeerde dat de database goed werkte met gelijktijdige toegang vanuit meerdere threads en verschillende processen. Agressieve synthetische tests toonden aan dat de database niet correct werkt in een omgeving met meerdere threads.

Om het probleem snel op te lossen, hebben we de processen gesynchroniseerd met behulp van het zelfgeschreven interprocess ReadWriteLock. Nu, bijna drie jaar later, werkt LiteDB veel beter.

StringList

Dit probleem is het tegenovergestelde van het geval met de partiële lexicale analyse. Als we met een tekst werken, is het handiger om ermee te werken als een stringlijst. Strings kunnen in willekeurige volgorde worden aangevraagd, maar er is nog steeds een bepaalde geheugentoegangsdichtheid. Op een gegeven moment was het nodig om verschillende taken uit te voeren om zeer grote bestanden te verwerken zonder volledige geheugenbelasting. Het idee was als volgt:

  1. Om het bestand regel voor regel te lezen. Onthoud offsets in het bestand.
  2. Voer op verzoek de volgende regel uit, stel een vereiste offset in en retourneer de gegevens.

De hoofdtaak is voltooid. Deze structuur neemt niet veel ruimte in beslag in vergelijking met de bestandsgrootte. In de testfase controleren we grondig de geheugenvoetafdruk voor grote en zeer grote bestanden. Grote bestanden werden lange tijd verwerkt en kleine bestanden zullen onmiddellijk worden verwerkt.

Er was geen referentie voor het controleren van de uitvoering tijd . RAM wordt Random Access Memory genoemd - het is het concurrentievoordeel ten opzichte van SSD en vooral ten opzichte van HDD. Deze stuurprogramma's beginnen slecht te werken voor willekeurige toegang. Het bleek dat deze aanpak het werk bijna 40 keer vertraagde, vergeleken met het volledig in het geheugen laden van een bestand. Bovendien lezen we het bestand 2,5 -10 keer volledig, afhankelijk van de context.

De oplossing was eenvoudig, en verbetering was genoeg zodat de operatie maar iets langer zou duren dan wanneer het bestand volledig in het geheugen is geladen.

Evenzo was het RAM-verbruik ook onbeduidend. We vonden inspiratie in het principe van het laden van gegevens uit RAM in een cacheprocessor. Wanneer u een array-element opent, kopieert de processor tientallen aangrenzende elementen naar zijn cache omdat de benodigde elementen vaak dichtbij zijn.

Veel datastructuren gebruiken deze processoroptimalisatie om topprestaties te behalen. Vanwege deze eigenaardigheid is willekeurige toegang tot array-elementen veel langzamer dan sequentiële toegang. We hebben een soortgelijk mechanisme geïmplementeerd:we lazen een set van duizend snaren en onthielden hun offsets. Wanneer we toegang krijgen tot de 1001e string, laten we de eerste 500 strings vallen en laden de volgende 500. Als we een van de eerste 500 regels nodig hebben, gaan we er apart naar toe, omdat we de offset al hebben.

De programmeur hoeft niet per se niet-functionele eisen zorgvuldig te formuleren en te controleren. Als gevolg hiervan herinnerden we ons voor toekomstige gevallen dat we sequentieel moeten werken met persistent geheugen.

De uitzonderingen analyseren

U kunt eenvoudig gegevens over gebruikersactiviteit verzamelen op internet. Dit is echter niet het geval bij het analyseren van desktop-applicaties. Er is geen dergelijke tool die een ongelooflijke reeks statistieken en visualisatietools zoals Google Analytics kan geven. Waarom? Hier zijn mijn veronderstellingen:

  1. Gedurende het grootste deel van de geschiedenis van de ontwikkeling van desktopapplicaties hadden ze geen stabiele en permanente toegang tot het web.
  2. Er zijn veel ontwikkeltools voor desktopapplicaties. Daarom is het onmogelijk om een ​​multifunctioneel hulpmiddel voor het verzamelen van gebruikersgegevens te bouwen voor alle UI-frameworks en technologieën.

Een belangrijk aspect van het verzamelen van gegevens is het bijhouden van uitzonderingen. We verzamelen bijvoorbeeld gegevens over crashes. Voorheen moesten onze gebruikers zelf naar de e-mail van de klantenondersteuning schrijven en een Stack Trace van een fout toevoegen, die werd gekopieerd uit een speciaal app-venster. Weinig gebruikers volgden al deze stappen. De verzamelde gegevens worden volledig geanonimiseerd, wat ons de mogelijkheid ontneemt om reproductiestappen of andere informatie van de gebruiker te achterhalen.

Aan de andere kant bevinden foutgegevens zich in de Postgres-database en dit maakt de weg vrij voor een onmiddellijke controle van tientallen hypothesen. U kunt onmiddellijk de antwoorden krijgen door eenvoudig SQL-query's naar de database te maken. Van slechts één stack of type uitzondering is vaak niet duidelijk hoe de uitzondering is ontstaan, daarom is al deze informatie van cruciaal belang om het probleem te bestuderen.

Daarnaast heb je de mogelijkheid om alle verzamelde gegevens te analyseren en de meest problematische modules en klassen te vinden. Op basis van de resultaten van de analyse kunt u refactoring of aanvullende tests plannen om deze onderdelen van het programma te dekken.

Stapeldecoderingsservice

.NET-builds bevatten IL-code, die gemakkelijk kan worden teruggezet in C#-code, nauwkeurig voor de operator, met behulp van verschillende speciale programma's. Een van de manieren om de programmacode te beschermen is de verduistering ervan. Programma's kunnen worden hernoemd; methoden, variabelen en klassen kunnen worden vervangen; code kan worden vervangen door zijn equivalent, maar het is echt onbegrijpelijk.

De noodzaak om de broncode te verdoezelen verschijnt wanneer u uw product distribueert op een manier die suggereert dat de gebruiker de builds van uw toepassing krijgt. Desktop-applicaties zijn die gevallen. Alle builds, inclusief tussentijdse builds voor testers, zijn zorgvuldig verdoezeld.

Onze Quality Assurance Unit gebruikt decoderingsstacktools van de ontwikkelaar van de obfuscator. Om te beginnen met decoderen, moeten ze de toepassing uitvoeren, deobfuscatiekaarten vinden die door CI zijn gepubliceerd voor een specifieke build en de uitzonderingsstapel in het invoerveld invoegen.

Verschillende versies en editors werden op een andere manier versluierd, waardoor het voor een ontwikkelaar moeilijk was om het probleem te bestuderen of hem zelfs op het verkeerde been kon zetten. Het was duidelijk dat dit proces geautomatiseerd moest worden.

Het formaat van de deobfuscatiekaart bleek vrij eenvoudig te zijn. We hebben het gemakkelijk ontleed en een programma voor het decoderen van stapels geschreven. Kort daarvoor werd een web-UI ontwikkeld om uitzonderingen per productversie te maken en te groeperen op stapel. Het was een .NET Core-website met een database in SQLite.

SQLite is een handige tool voor kleine oplossingen. We hebben geprobeerd om daar ook deobfuscatiekaarten te plaatsen. Elke build genereerde ongeveer 500 duizend coderings- en decoderingsparen. SQLite kon zo'n agressieve invoegsnelheid niet aan.

Terwijl gegevens over één build in de database werden ingevoegd, werden er nog twee aan de wachtrij toegevoegd. Niet lang voor dat probleem luisterde ik naar een reportage over Clickhouse en wilde het graag uitproberen. Het bleek uitstekend te zijn, de invoegsnelheid werd meer dan 200 keer versneld.

Dat gezegd hebbende, het decoderen van de stapel (lezen uit de database) werd bijna 50 keer langzamer, maar aangezien elke stapel minder dan 1 ms duurde, was het niet rendabel om tijd te besteden aan het bestuderen van dit probleem.

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.


  1. Wat is de maximale lengte van gegevens die ik in een BLOB-kolom in MySQL kan plaatsen?

  2. Tussentijds gelijktijdige gebeurtenissen in een database zoeken

  3. Een relatie maken in SQL Server 2017

  4. Hoe SQLCipher te implementeren bij gebruik van SQLiteOpenHelper