Jeg har arbejdet for en virksomhed, der udvikler IDE'er til databaseinteraktion i mere end fem år. Før jeg begyndte at skrive denne artikel, havde jeg ingen idé om, hvor mange fancy historier der ville ligge forude.
Mit team udvikler og understøtter IDE-sprogfunktioner, og autofuldførelse af kode er den vigtigste. Jeg stod over for mange spændende ting, der skete. Nogle ting gjorde vi godt fra første forsøg, og nogle andre mislykkedes selv efter flere skud.
SQL og dialekter parsing
SQL er et forsøg på at ligne et naturligt sprog, og forsøget er ganske vellykket, må jeg sige. Afhængigt af dialekten er der flere tusinde søgeord. For at skelne et udsagn fra et andet skal du ofte lede efter et eller to ord (tokens) forude. Denne tilgang kaldes et lookahead .
Der er en parser-klassifikation afhængigt af hvor langt de kan se frem:LA(1), LA(2) eller LA(*), hvilket betyder, at en parser kan se så langt frem som nødvendigt for at definere den rigtige fork.
Nogle gange matcher en valgfri klausul, der slutter, starten på en anden valgfri klausul. Disse situationer gør parsing meget sværere at køre. T-SQL gør ikke tingene nemmere. Nogle SQL-sætninger kan også have, men ikke nødvendigvis, slutninger, der kan være i konflikt med begyndelsen af tidligere sætninger.
tror du ikke på det? Der er en måde at beskrive formelle sprog på via grammatik. Du kan generere en parser ud af det ved at bruge dette eller hint værktøj. De mest bemærkelsesværdige værktøjer og sprog, der beskriver grammatik, er YACC og ANTLR.
YACC -genererede parsere bruges i MySQL-, MariaDB- og PostgreSQL-motorer. Vi kunne prøve at tage dem direkte fra kildekoden og udvikle kodefuldførelse og andre funktioner baseret på SQL-analysen ved hjælp af disse parsere. Desuden ville dette produkt modtage gratis udviklingsopdateringer, og parseren ville opføre sig på samme måde som kildemotoren.
Så hvorfor bruger vi stadig ANTLR ? Det understøtter C#/.NET, har et anstændigt værktøjssæt, dets syntaks er meget lettere at læse og skrive. ANTLR-syntaks blev så praktisk, at Microsoft nu bruger den i sin officielle C#-dokumentation.
Men lad os gå tilbage til SQL-kompleksitet, når det kommer til parsing. Jeg vil gerne sammenligne grammatikstørrelserne på de offentligt tilgængelige sprog. I dbForge bruger vi vores grammatikstykker. De er mere komplette end de andre. Desværre er de overbelastet med indsættelserne af C#-koden til at understøtte forskellige funktioner.
Grammatikstørrelserne for forskellige sprog er som følger:
JS – 475 parserrækker + 273 lexere =748 rækker
Java – 615 parserrækker + 211 lexere =826 rækker
C# – 1159 parserrækker + 433 lexere =1592 rækker
С++ – 1933 rækker
MySQL – 2515 parserrækker + 1189 lexere =3704 rækker
T-SQL – 4035 parserrækker + 896 lexere =4931 rækker
PL SQL – 6719 parserrækker + 2366 lexere =9085 rækker
Endelserne på nogle lexers indeholder lister over Unicode-tegn, der er tilgængelige på sproget. Disse lister er ubrugelige med hensyn til evaluering af sprogets kompleksitet. Således sluttede antallet af rækker, jeg tog, altid før disse lister.
Det kan diskuteres at vurdere kompleksiteten af sprogparsing baseret på antallet af rækker i sproggrammatikken. Alligevel mener jeg, at det er vigtigt at vise de tal, der viser en stor uoverensstemmelse.
Det er ikke alt. Da vi udvikler en IDE, bør vi håndtere ufuldstændige eller ugyldige scripts. Vi skulle finde på mange tricks, men kunderne sender stadig mange arbejdsscenarier med ufærdige scripts. Vi er nødt til at løse dette.
Prdikatkrige
Under kodeparsingen fortæller ordet nogle gange dig ikke, hvilket af de to alternativer du skal vælge. Mekanismen, der løser denne type unøjagtigheder, er lookahead i ANTLR. Parsermetoden er den indsatte kæde af hvis , og hver af dem ser et skridt foran. Se eksemplet på den grammatik, der genererer denne usikkerhed:
rule1:
'a' rule2 | rule3
;
rule2:
'b' 'c' 'd'
;
rule3:
'b' 'c' 'e'
;
I midten af reglen1, når tokenet 'a' allerede er bestået, vil parseren se to trin frem for at vælge den regel, der skal følges. Denne kontrol vil blive udført igen, men denne grammatik kan omskrives for at udelukke lookahead . Ulempen er, at sådanne optimeringer skader strukturen, mens ydelsesforøgelsen er ret lille.
Der er mere komplekse måder at løse denne form for usikkerhed på. For eksempel Syntax-prædikatet (SynPred) mekanisme i ANTLR3 . Det hjælper, når den valgfri slutning af en klausul krydser begyndelsen af den næste valgfri klausul.
Med hensyn til ANTLR3 er et prædikat en genereret metode, der udfører en virtuel tekstindtastning i henhold til et af alternativerne . Når det lykkes, returnerer det den sande værdi, og prædikatafslutningen er vellykket. Når det er en virtuel indgang, kaldes det en backtracking tilstandsindtastning. Hvis et prædikat fungerer med succes, sker den rigtige indtastning.
Det er kun et problem, når et prædikat starter inde i et andet prædikat. Så kan én distance blive krydset hundredvis eller tusindvis af gange.
Lad os gennemgå et forenklet eksempel. Der er tre usikkerhedsmomenter:(A, B, C).
- Parseren indtaster A, husker sin position i teksten, starter en virtuel indtastning på niveau 1.
- Parseren indtaster B, husker sin position i teksten, starter en virtuel indtastning på niveau 2.
- Parseren indtaster C, husker sin position i teksten, starter en virtuel indtastning på niveau 3.
- Parseren fuldfører en virtuel indtastning på niveau-3, vender tilbage til niveau-2 og består C igen.
- Parseren fuldfører en virtuel indtastning på niveau-2, vender tilbage til niveau-1 og består B og C igen.
- Parseren fuldfører en virtuel indtastning, returnerer og udfører en reel indtastning gennem A, B og C.
Som et resultat vil alle kontroller inden for C blive udført 4 gange, inden for B – 3 gange, inden for A – 2 gange.
Men hvad hvis et passende alternativ er på anden eller tredjeplads på listen? Så vil et af prædikatstadierne mislykkes. Dens position i teksten vil rulle tilbage, og et andet prædikat begynder at køre.
Når vi analyserer årsagerne til, at appen fryser, støder vi ofte på sporet af SynPred henrettet flere tusinde gange. SynPred s er især problematiske i rekursive regler. Desværre er SQL rekursiv i sin natur. Muligheden for at bruge underforespørgsler næsten overalt har sin pris. Det er dog muligt at manipulere reglen for at få et prædikat til at forsvinde.
SynPred skader ydeevnen. På et tidspunkt blev deres antal sat under hård kontrol. Men problemet er, at når du skriver grammatikkode, kan SynPred se ud til at være uoplagt for dig. Mere så, ændring af en regel kan få SynPred til at blive vist i en anden regel, og det gør kontrol over dem praktisk talt umulig.
Vi oprettede et simpelt regulært udtryk værktøj til at kontrollere antallet af prædikater, der køres af den særlige MSBuild Task . Hvis antallet af prædikater ikke stemte overens med det angivne antal i en fil, mislykkedes opgaven med det samme bygningen og advarede om en fejl.
Når en udvikler ser fejlen, bør han omskrive reglens kode flere gange for at fjerne de overflødige prædikater. Hvis man ikke kan undgå prædikater, vil udvikleren tilføje det til en speciel fil, der trækker ekstra opmærksomhed til anmeldelsen.
I sjældne tilfælde skrev vi endda vores prædikater ved hjælp af C# bare for at undgå de ANTLR-genererede. Heldigvis findes denne metode også.
Grammatikarv
Når der er ændringer i vores understøttede DBMS'er, skal vi opfylde dem i vores værktøjer. Understøttelse af grammatiske syntakskonstruktioner er altid et udgangspunkt.
Vi laver en speciel grammatik for hver SQL-dialekt. Det muliggør en vis kodegentagelse, men det er nemmere end at prøve at finde, hvad de har til fælles.
Vi gik efter at skrive vores egen ANTLR grammatik preprocessor, der arver grammatik.
Det blev også indlysende, at vi havde brug for en mekanisme for polymorfi – evnen til ikke kun at omdefinere reglen i efterkommeren, men også at kalde den grundlæggende. Vi vil også gerne kontrollere positionen, når vi kalder basisreglen.
Værktøjer er et klart plus, når vi sammenligner ANTLR med andre sproggenkendelsesværktøjer, Visual Studio og ANTLRWorks. Og du ønsker ikke at miste denne fordel, mens du implementerer arven. Løsningen var at specificere grundlæggende grammatik i en nedarvet grammatik i et ANTLR-kommentarformat. For ANTLR-værktøjer er det kun en kommentar, men vi kan udtrække alle nødvendige oplysninger fra den.
Vi skrev en MsBuild-opgave, der var indlejret i hele-build-systemet som pre-build-handling. Opgaven var at udføre arbejdet som en præprocessor til ANTLR-grammatik ved at generere den resulterende grammatik fra dens base og nedarvede peers. Den resulterende grammatik blev behandlet af ANTLR selv.
ANTLR-efterbehandling
I mange programmeringssprog kan søgeord ikke bruges som emnenavne. Der kan være fra 800 til 3000 nøgleord i SQL afhængigt af dialekten. De fleste af dem er knyttet til konteksten i databaser. At forbyde dem som objektnavne ville derfor frustrere brugerne. Det er derfor, SQL har reserverede og ureserverede søgeord.
Du kan ikke navngive dit objekt som det reserverede ord (SELECT, FROM osv.) uden at citere det, men du kan gøre dette til et ureserveret ord (SAMTALE, TILGÆNGELIGHED osv.). Denne interaktion gør udviklingen af parseren sværere.
Under den leksikalske analyse er konteksten ukendt, men en parser kræver allerede forskellige tal for identifikatoren og nøgleordet. Det er derfor, vi tilføjede endnu en efterbehandling til ANTLR-parseren. Det erstattede alle de åbenlyse identifikationskontrol med at kalde en speciel metode.
Denne metode har en mere detaljeret kontrol. Hvis indtastningen kalder en identifikator, og vi forventer, at identifikatoren er opfyldt fremover, så er det alt godt. Men hvis et uforbeholdent ord er en post, bør vi dobbelttjekke det. Denne ekstra kontrol gennemgår grensøgningen i den aktuelle kontekst, hvor dette uforbeholdne nøgleord kan være et nøgleord. Hvis der ikke er sådanne grene, kan det bruges som en identifikator.
Teknisk set kunne dette problem løses ved hjælp af ANTLR, men denne beslutning er ikke optimal. ANTLR-måden er at oprette en regel, der viser alle ureserverede søgeord og en lexemidentifikator. Længere fremme vil en særlig regel tjene i stedet for en lexem identifier. Denne løsning gør, at en udvikler ikke glemmer at tilføje søgeordet, hvor det er brugt og i specialreglen. Desuden optimerer det tidsforbruget.
Fejl i syntaksanalyse uden træer
Syntakstræet er normalt et resultat af parserarbejde. Det er en datastruktur, der afspejler programteksten gennem formel grammatik. Hvis du vil implementere en kodeeditor med sprogets autofuldførelse, vil du højst sandsynligt få følgende algoritme:
- Parse teksten i editoren. Så får du et syntakstræ.
- Find en node under vognen, og match den med grammatikken.
- Find ud af, hvilke søgeord og objekttyper der vil være tilgængelige på punktet.
I dette tilfælde er grammatikken let at forestille sig som en graf eller en statsmaskine.
Desværre var kun den tredje version af ANTLR tilgængelig, da dbForge IDE havde startet sin udvikling. Det var dog ikke så smidigt, og selvom du kunne fortælle ANTLR, hvordan man bygger et træ, var brugen ikke jævn.
Desuden foreslog mange artikler om dette emne at bruge 'handlings'-mekanismen til at køre kode, når parseren passerede gennem reglen. Denne mekanisme er meget praktisk, men den har ført til arkitektoniske problemer og gjort understøttelse af ny funktionalitet mere kompleks.
Sagen er, at en enkelt grammatikfil begyndte at akkumulere 'handlinger' på grund af det store antal funktionaliteter, der hellere skulle have været distribueret til forskellige builds. Vi formåede at distribuere handlingsbehandlere til forskellige builds og lave en lusket abonnent-notifier-mønstervariation til den foranstaltning.
ANTLR3 virker 6 gange hurtigere end ANTLR4 ifølge vores målinger. Syntakstræet for store scripts kunne også tage for meget RAM, hvilket ikke var gode nyheder, så vi var nødt til at operere inden for 32-bit adresserummet i Visual Studio og SQL Management Studio.
ANTLR-parser-efterbehandling
Når man arbejder med strenge, er et af de mest kritiske øjeblikke det stadie af leksikalsk analyse, hvor vi opdeler scriptet i separate ord.
ANTLR tager som input grammatik, der specificerer sproget og udsender en parser på et af de tilgængelige sprog. På et tidspunkt voksede den genererede parser i en sådan grad, at vi var bange for at fejlsøge den. Skulle du trykke på F11 (træde ind) under fejlretningen og gå til parserfilen, ville Visual Studio bare gå ned.
Det viste sig, at det fejlede på grund af en OutOfMemory-undtagelse ved analyse af parserfilen. Denne fil indeholdt mere end 200.000 linjer kode.
Men fejlretning af parseren er en væsentlig del af arbejdsprocessen, og du kan ikke udelade den. Ved hjælp af C# partial-klasser analyserede vi den genererede parser ved hjælp af regulære udtryk og opdelte den i nogle få filer. Visual Studio fungerede perfekt med det.
Leksikal analyse uden understreng før Span API
Den leksikalske analyses hovedopgave er klassifikation - at definere ordenes grænser og kontrollere dem mod en ordbog. Hvis ordet findes, vil lexeren returnere sit indeks. Hvis ikke, anses ordet for at være en objektidentifikator. Dette er en forenklet beskrivelse af algoritmen.
Baggrundstekst under filåbning
Syntaksfremhævning er baseret på leksikalsk analyse. Denne operation tager normalt meget længere tid sammenlignet med at læse tekst fra disken. Hvad er fangsten? I en tråd læses teksten fra filen, mens den leksikalske analyse udføres i en anden tråd.
Lexeren læser teksten række for række. Hvis den anmoder om en række, der ikke eksisterer, stopper den og venter.
BlockingCollection
- At læse fra en fil er en Producer, mens lexer er en Forbruger.
- Lexer er allerede en producent, og teksteditoren er en forbruger.
Dette sæt tricks giver os mulighed for betydeligt at forkorte den tid, der bruges på at åbne store filer. Den første side af dokumentet vises meget hurtigt, dog kan dokumentet fryse, hvis brugere forsøger at flytte til slutningen af filen inden for de første få sekunder. Det sker, fordi baggrundslæseren og lexeren skal nå slutningen af dokumentet. Men hvis brugeren arbejder, bevæger sig langsomt fra begyndelsen af dokumentet mod slutningen, vil der ikke være nogen mærkbare frysninger.
Tvetydig optimering:delvis leksikalsk analyse
Den syntaktiske analyse er normalt opdelt i to niveauer:
- input-tegnstrømmen behandles for at få lexemes (tokens) baseret på sprogreglerne – dette kaldes leksikalsk analyse
- parseren bruger tokenstrøm ved at kontrollere den i henhold til de formelle grammatikregler og bygger ofte et syntakstræ.
Strengebehandling er en dyr operation. For at optimere den besluttede vi ikke at udføre en fuld leksikalsk analyse af teksten hver gang, men kun at genanalysere den del, der blev ændret. Men hvordan skal man håndtere multiline-konstruktioner som blokkommentarer eller linjer? Vi gemte en linjesluttilstand for hver linje:"ingen multiline tokens" =0, "begyndelsen af en blokkommentar" =1, "begyndelsen af en multiline streng bogstavelig" =2. Den leksikalske analyse starter fra den ændrede sektion og slutter, når linjesluttilstanden er lig med den gemte.
Der var et problem med denne løsning:det er ekstremt ubelejligt at overvåge linjenumre i sådanne strukturer, mens linjenummer er en påkrævet attribut for et ANTLR-token, fordi når en linje indsættes eller slettes, skal nummeret på den næste linje opdateres i overensstemmelse hermed. Vi løste det ved at sætte et linjenummer med det samme, inden vi afleverede tokenet til parseren. De test, vi udførte senere, har vist, at ydeevnen blev forbedret med 15-25%. Den faktiske forbedring var endnu større.
Mængden af RAM, der kræves til alt dette, viste sig at være meget mere, end vi havde forventet. Et ANTLR-token bestod af:et begyndelsespunkt – 8 bytes, et slutpunkt – 8 bytes, et link til ordets tekst – 4 eller 8 bytes (uden selve strengen), et link til dokumentets tekst – 4 eller 8 bytes, og en token-type – 4 bytes.
Så hvad kan vi konkludere? Vi fokuserede på ydeevne og fik for stort forbrug af RAM et sted, vi ikke havde forventet. Vi antog ikke, at dette ville ske, fordi vi forsøgte at bruge lette strukturer i stedet for klasser. Ved at erstatte dem med tunge genstande gik vi bevidst efter yderligere hukommelsesudgifter for at få bedre ydeevne. Heldigvis lærte dette os en vigtig lektie, så nu ender hver ydelsesoptimering med profilering af hukommelsesforbrug og omvendt.
Dette er en historie med en moral. Nogle funktioner begyndte at virke næsten øjeblikkeligt, og andre bare en smule hurtigere. Det ville trods alt være umuligt at udføre tricket til baggrundsleksikalsk analyse, hvis der ikke var et objekt, hvor en af trådene kunne opbevare tokens.
Alle yderligere problemer udspiller sig i forbindelse med desktopudvikling på .NET-stakken.
32-bit-problemet
Nogle brugere vælger at bruge selvstændige versioner af vores produkter. Andre holder sig til at arbejde i Visual Studio og SQL Server Management Studio. Der er udviklet mange udvidelser til dem. En af disse udvidelser er SQL Complete. For at præcisere, det giver flere kræfter og funktioner end standard Code Completion SSMS og VS for SQL.
SQL-parsing er en meget bekostelig proces, både hvad angår CPU- og RAM-ressourcer. For at vise listen over objekter i brugerscripts, uden unødvendige kald til serveren, gemmer vi objektcachen i RAM. Ofte fylder det ikke meget, men nogle af vores brugere har databaser, der indeholder op til en kvart million objekter.
At arbejde med SQL er meget anderledes end at arbejde med andre sprog. I C# er der praktisk talt ingen filer, selv med tusind linjer kode. I mellemtiden kan en udvikler i SQL arbejde med en database-dump bestående af flere millioner linjer kode. Der er intet usædvanligt ved det.
DLL-helvede inde i VS
Der er et praktisk værktøj til at udvikle plugins i .NET Framework, det er et applikationsdomæne. Alt udføres på en isoleret måde. Det er muligt at læsse af. For det meste er implementeringen af udvidelser måske hovedårsagen til, at applikationsdomæner blev introduceret.
Der er også MAF Framework, som blev designet af MS til at løse problemet med at skabe tilføjelser til programmet. Det isolerer disse tilføjelser i en sådan grad, at det kan sende dem til en separat proces og overtage al kommunikation. Helt ærligt er denne løsning for besværlig og har ikke vundet meget popularitet.
Desværre implementerer Microsoft Visual Studio og SQL Server Management Studio på det, udvidelsessystemet anderledes. Det forenkler adgangen til hostingapplikationer for plugins, men det tvinger dem til at passe sammen inden for én proces og domæne med en anden.
Ligesom enhver anden applikation i det 21. århundrede har vores mange afhængigheder. De fleste af dem er velkendte, gennemprøvede og populære biblioteker i .NET-verdenen.
Trække beskeder i en lås
Det er ikke almindeligt kendt, at .NET Framework vil pumpe Windows Message Queue ind i hver WaitHandle. For at placere den i hver lås, kan enhver behandler af enhver hændelse i en applikation kaldes, hvis denne lås har tid til at skifte til kernetilstand, og den ikke frigives under spin-vente-fasen.
Dette kan resultere i genindtræden nogle meget uventede steder. Et par gange førte det til problemer som "Samlingen blev ændret under opregning" og forskellige ArgumentOutOfRangeException.
Tilføjelse af en samling til en løsning ved hjælp af SQL
Når projektet vokser, udvikler opgaven med at tilføje samlinger, som er enkel i starten, til et dusin af komplicerede trin. En gang skulle vi tilføje et dusin af forskellige samlinger til løsningen, vi udførte en stor refaktorisering. Næsten 80 løsninger, inklusive produkt- og testløsninger, blev skabt baseret på omkring 300 .NET-projekter.
Baseret på produktløsninger skrev vi Inno Setup-filer. De inkluderede lister over samlinger pakket i installationen, som brugeren downloadede. Algoritmen til at tilføje et projekt var som følger:
- Opret et nyt projekt.
- Føj et certifikat til det. Konfigurer tagget for build.
- Tilføj en versionsfil.
- Genkonfigurer stierne, hvor projektet skal hen.
- Omdøb mappen, så den matcher den interne specifikation.
- Tilføj projektet til løsningen igen.
- Tilføj et par samlinger, som alle projekter skal have links til.
- Tilføj buildet til alle nødvendige løsninger:test og produkt.
- For alle produktløsninger skal du tilføje samlingerne til installationen.
Disse 9 trin skulle gentages cirka 10 gange. Trin 8 og 9 er ikke så trivielle, og det er nemt at glemme at tilføje builds overalt.
Stillet over for en så stor og rutinemæssig opgave vil enhver normal programmør gerne automatisere den. Det var præcis, hvad vi ønskede at gøre. Men hvordan angiver vi, hvilke løsninger og installationer der præcist skal tilføjes til det nyoprettede projekt? Der er så mange scenarier, og hvad mere er, det er svært at forudsige nogle af dem.
Vi fik en skør idé. Løsninger hænger sammen med projekter som mange-til-mange, projekter med installationer på samme måde, og SQL kan løse præcis den slags opgaver, som vi havde.
Vi har lavet en .Net Core Console-app, der scanner alle .sln-filer i kildemappen, henter listen over projekter fra dem ved hjælp af DotNet CLI og lægger den i SQLite-databasen. Programmet har nogle få tilstande:
- Ny – opretter et projekt og alle nødvendige mapper, tilføjer et certifikat, opsætter et tag, tilføjer en version, minimum væsentlige samlinger.
- Tilføj-projekt – tilføjer projektet til alle løsninger, der opfylder den SQL-forespørgsel, der vil blive givet som en af parametrene. For at tilføje projektet til løsningen bruger programmet indeni DotNet CLI.
- Add-ISS – tilføjer projektet til alle installationer, der opfylder SQL-forespørgsler.
Selvom ideen om at angive listen over løsninger gennem SQL-forespørgslen kan virke besværlig, lukkede den fuldstændig alle eksisterende sager og højst sandsynligt alle mulige sager i fremtiden.
Lad mig demonstrere scenariet. Opret et projekt "A" og tilføje det til alle løsninger, hvor projekter “B” bruges:
dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"
Et problem med LiteDB
For et par år siden fik vi til opgave at udvikle en baggrundsfunktion til lagring af brugerdokumenter. Den havde to hovedapplikationsflows:evnen til øjeblikkeligt at lukke IDE'en og forlade den, og ved at vende tilbage til start hvor du slap, og evnen til at genoprette i presserende situationer som blackouts eller programnedbrud.
For at gennemføre denne opgave var det nødvendigt at gemme indholdet af filerne et sted på siden og gøre det ofte og hurtigt. Udover indholdet var det nødvendigt at gemme nogle metadata, hvilket gjorde direkte lagring i filsystemet ubelejligt.
På det tidspunkt fandt vi LiteDB-biblioteket, som imponerede os med dets enkelhed og ydeevne. LiteDB er en hurtig letvægts indlejret database, som udelukkende er skrevet i C#. Hurtigheden og den overordnede enkelhed vandt os.
I løbet af udviklingsprocessen var hele teamet tilfredse med arbejdet med LiteDB. De største problemer startede dog efter udgivelsen.
Den officielle dokumentation garanterede, at databasen sikrede korrekt arbejde med samtidig adgang fra flere tråde samt flere processer. Aggressive syntetiske test viste, at databasen ikke fungerer korrekt i et multithreaded miljø.
For hurtigt at løse problemet synkroniserede vi processerne ved hjælp af den selvskrevne interproces ReadWriteLock. Nu, efter næsten tre år, fungerer LiteDB meget bedre.
StreamStringList
Dette problem er det modsatte af tilfældet med den partielle leksikalske analyse. Når vi arbejder med en tekst, er det mere bekvemt at arbejde med den som en strengliste. Strenge kan anmodes om i tilfældig rækkefølge, men en vis hukommelsesadgangstæthed er stadig til stede. På et tidspunkt var det nødvendigt at køre flere opgaver for at behandle meget store filer uden fuld hukommelsesbelastning. Ideen var som følger:
- At læse filen linje for linje. Husk forskydninger i filen.
- På forespørgsel skal du udsende den næste linjesæt en påkrævet offset og returnere dataene.
Hovedopgaven er afsluttet. Denne struktur fylder ikke meget i forhold til filstørrelsen. På teststadiet tjekker vi hukommelsesfodaftrykket grundigt for store og meget store filer. Store filer blev behandlet i lang tid, og små vil blive behandlet med det samme.
Der var ingen reference til at kontrollere tidspunktet for eksekvering . RAM kaldes Random Access Memory - det er dens konkurrencefordel i forhold til SSD og især over HDD. Disse drivere begynder at fungere dårligt for tilfældig adgang. Det viste sig, at denne tilgang bremsede arbejdet med næsten 40 gange, sammenlignet med at indlæse en fil fuldstændigt i hukommelsen. Desuden læser vi filen 2,5 -10 fulde gange afhængigt af konteksten.
Løsningen var enkel, og forbedring var nok til, at operationen kun ville tage lidt længere tid, end når filen er fuldt indlæst i hukommelsen.
Ligeledes var RAM-forbruget også ubetydeligt. Vi fandt inspiration i princippet om at indlæse data fra RAM til en cache-processor. Når du får adgang til et array-element, kopierer processoren snesevis af tilstødende elementer til sin cache, fordi de nødvendige elementer ofte er i nærheden.
Mange datastrukturer bruger denne processoroptimering til at opnå topydelse. Det er på grund af denne ejendommelighed, at tilfældig adgang til array-elementer er meget langsommere end sekventiel adgang. Vi implementerede en lignende mekanisme:vi læste et sæt på tusind strenge og huskede deres forskydninger. Når vi får adgang til den 1001. streng, dropper vi de første 500 strenge og indlæser de næste 500. Hvis vi har brug for nogen af de første 500 linjer, så går vi til den separat, fordi vi allerede har offset.
Programmøren behøver ikke nødvendigvis omhyggeligt at formulere og kontrollere ikke-funktionelle krav. Som et resultat huskede vi for fremtidige sager, at vi skal arbejde sekventielt med vedvarende hukommelse.
Analyse af undtagelserne
Du kan nemt indsamle brugeraktivitetsdata på nettet. Det er dog ikke tilfældet med at analysere desktop-applikationer. Der er ikke noget sådant værktøj, der er i stand til at give et utroligt sæt målinger og visualiseringsværktøjer som Google Analytics. Hvorfor? Her er mine antagelser:
- Gennem størstedelen af historien om desktopapplikationsudvikling havde de ingen stabil og permanent adgang til internettet.
- Der er mange udviklingsværktøjer til desktop-applikationer. Derfor er det umuligt at bygge et multifunktionelt brugerdataindsamlingsværktøj til alle UI-rammer og teknologier.
Et centralt aspekt ved indsamling af data er at spore undtagelser. For eksempel indsamler vi data om nedbrud. Tidligere skulle vores brugere selv skrive til kundesupport-e-mail og tilføje en Stack Trace af en fejl, som blev kopieret fra et specielt appvindue. Få brugere fulgte alle disse trin. Indsamlede data er fuldstændig anonymiseret, hvilket fratager os muligheden for at finde ud af reproduktionstrin eller enhver anden information fra brugeren.
På den anden side er fejldata i Postgres-databasen, og dette baner vejen for en øjeblikkelig kontrol af snesevis af hypoteser. Du kan med det samme få svarene ved blot at lave SQL-forespørgsler til databasen. Det er ofte uklart ud fra en enkelt stak eller undtagelsestype, hvordan undtagelsen opstod, og derfor er al denne information afgørende for at studere problemet.
Udover det har du mulighed for at analysere alle indsamlede data og finde de mest problematiske moduler og klasser. Baseret på resultaterne af analysen kan du planlægge refaktorisering eller yderligere test for at dække disse dele af programmet.
Stakafkodningstjeneste
.NET builds indeholder IL-kode, som nemt kan konverteres tilbage til C#-kode, nøjagtigt for operatøren, ved hjælp af flere specielle programmer. En af måderne at beskytte programkoden på er dens tilsløring. Programmer kan omdøbes; metoder, variabler og klasser kan erstattes; kode kan erstattes med tilsvarende, men det er virkelig uforståeligt.
Nødvendigheden af at sløre kildekoden viser sig, når du distribuerer dit produkt på en måde, der antyder, at brugeren får builds af din applikation. Desktop-applikationer er disse tilfælde. Alle builds, inklusive mellemliggende builds til testere, er omhyggeligt sløret.
Vores kvalitetssikringsenhed bruger afkodningsstackværktøjer fra obfuscator-udvikleren. For at starte afkodningen skal de køre programmet, finde deobfuscation maps udgivet af CI for en specifik build og indsætte undtagelsesstakken i inputfeltet.
Forskellige versioner og editorer blev sløret på en anden måde, hvilket gjorde det svært for en udvikler at studere problemet eller endda kunne bringe ham på det forkerte spor. Det var indlysende, at denne proces skulle automatiseres.
Deobfuscation-kortformatet viste sig at være ret ligetil. Vi fjernede det nemt og skrev et stak-afkodningsprogram. Kort før det blev en web-UI udviklet til at gengive undtagelser efter produktversioner og gruppere dem efter stakken. Det var et .NET Core-websted med en database i SQLite.
SQLite er et smart værktøj til små løsninger. Vi forsøgte også at placere deobfuscation-kort der. Hver build genererede cirka 500.000 krypterings- og dekrypteringspar. SQLite kunne ikke håndtere en så aggressiv indsættelseshastighed.
Mens data på én build blev indsat i databasen, blev to mere tilføjet til køen. Ikke længe før det problem lyttede jeg til en rapport om Clickhouse og var ivrig efter at prøve det. Det viste sig at være fremragende, indsættelseshastigheden blev øget med mere end 200 gange.
Når det er sagt, blev stakafkodningen (læsning fra databasen) bremset med næsten 50 gange, men da hver stak tog mindre end 1 ms, var det omkostningseffektivt at bruge tid på at studere dette problem.
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.