sql >> Databasteknik >  >> RDS >> Database

Slutför SQL. Berättelser om framgång och misslyckande

Jag har arbetat för ett företag som utvecklar IDE:er för databasinteraktion i mer än fem år. Innan jag började skriva den här artikeln hade jag ingen aning om hur många tjusiga berättelser som skulle ligga framför mig.

Mitt team utvecklar och stöder IDE-språkfunktioner, och autokomplettering av kod är den viktigaste. Jag mötte många spännande saker som hände. Vissa saker gjorde vi bra från första försöket, och andra misslyckades även efter flera skott.

SQL och dialektanalys

SQL är ett försök att se ut som ett naturligt språk, och försöket är ganska lyckat, måste jag säga. Beroende på dialekt finns det flera tusen nyckelord. För att skilja ett påstående från ett annat behöver du ofta leta efter ett eller två ord (tokens) framåt. Detta tillvägagångssätt kallas en blick framåt .

Det finns en parserklassificering beroende på hur långt de kan se framåt:LA(1), LA(2) eller LA(*), vilket innebär att en parser kan titta så långt fram som behövs för att definiera rätt gaffel.

Ibland matchar en valfri klausul som slutar början på en annan valfri klausul. Dessa situationer gör analysen mycket svårare att köra. T-SQL gör inte saker enklare. Vissa SQL-satser kan också ha, men inte nödvändigtvis, ändelser som kan komma i konflikt med början av tidigare satser.

tror du inte på det? Det finns ett sätt att beskriva formella språk via grammatik. Du kan skapa en parser av den med hjälp av det eller det verktyget. De mest anmärkningsvärda verktygen och språken som beskriver grammatik är YACC och ANTLR.

YACC -genererade parsers används i MySQL-, MariaDB- och PostgreSQL-motorer. Vi kan försöka ta dem direkt från källkoden och utveckla kodkomplettering och andra funktioner baserade på SQL-analysen som använder dessa parsers. Dessutom skulle den här produkten få gratis utvecklingsuppdateringar, och parsern skulle bete sig på samma sätt som källmotorn gör.

Så varför använder vi fortfarande ANTLR ? Den stöder C#/.NET, har en anständig verktygslåda, dess syntax är mycket lättare att läsa och skriva. ANTLR-syntax kom att vara så praktisk att Microsoft nu använder den i sin officiella C#-dokumentation.

Men låt oss gå tillbaka till SQL-komplexitet när det kommer till analys. Jag skulle vilja jämföra grammatikstorlekarna för de allmänt tillgängliga språken. I dbForge använder vi våra grammatikbitar. De är mer kompletta än de andra. Tyvärr är de överbelastade med insatserna i C#-koden för att stödja olika funktioner.

Grammatikstorlekarna för olika språk är följande:

JS – 475 parserrader + 273 lexers =748 rader

Java – 615 parserrader + 211 lexers =826 rader

C# – 1159 parserrader + 433 lexers =1592 rader

С++ – 1933 rader

MySQL – 2515 parserrader + 1189 lexers =3704 rader

T-SQL – 4035 parserrader + 896 lexers =4931 rader

PL SQL – 6719 parserrader + 2366 lexers =9085 rader

Sluten på vissa lexers innehåller listorna över Unicode-tecken som finns tillgängliga på språket. Dessa listor är värdelösa när det gäller utvärderingen av språkets komplexitet. Antalet rader jag tog slutade alltså alltid före dessa listor.

Att utvärdera komplexiteten i språkanalys baserat på antalet rader i språkgrammatiken är diskutabelt. Ändå tror jag att det är viktigt att visa siffrorna som visar på en enorm diskrepans.

Det är inte allt. Eftersom vi utvecklar en IDE bör vi hantera ofullständiga eller ogiltiga skript. Vi var tvungna att hitta på många knep, men kunderna skickar fortfarande många fungerande scenarier med oavslutade skript. Vi måste lösa detta.

Predikatkrig

Under kodtolkningen berättar ordet ibland inte vilket av de två alternativen du ska välja. Mekanismen som löser denna typ av felaktigheter är lookahead i ANTLR. Parsermetoden är den infogade kedjan av om , och var och en av dem ser ett steg före. Se exemplet på grammatiken som genererar osäkerheten av detta slag:

rule1:
  'a' rule2 | rule3
;

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

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

I mitten av regel1, när token 'a' redan har passerats, kommer analysatorn att titta två steg framåt för att välja regeln att följa. Denna kontroll kommer att utföras en gång till, men denna grammatik kan skrivas om för att utesluta framåtblick . Nackdelen är att sådana optimeringar skadar strukturen, medan prestandaökningen är ganska liten.

Det finns mer komplexa sätt att lösa den här typen av osäkerhet. Till exempel, Syntaxpredikatet (SynPred) mekanism i ANTLR3 . Det hjälper när det valfria slutet på en sats korsar början av nästa valfria sats.

När det gäller ANTLR3 är ett predikat en genererad metod som utför en virtuell textinmatning enligt ett av alternativen . När det lyckas returneras true värde och slutförandet av predikatet är framgångsrikt. När det är en virtuell post kallas det en backtracking lägesingång. Om ett predikat fungerar framgångsrikt sker det verkliga inträdet.

Det är bara ett problem när ett predikat börjar inuti ett annat predikat. Då kan en sträcka korsas hundratals eller tusentals gånger.

Låt oss granska ett förenklat exempel. Det finns tre osäkerhetspunkter:(A, B, C).

  1. Parseraren anger A, kommer ihåg sin position i texten, startar en virtuell post på nivå 1.
  2. Parseraren anger B, kommer ihåg sin position i texten, startar en virtuell post på nivå 2.
  3. Parseraren anger C, kommer ihåg sin position i texten, startar en virtuell post på nivå 3.
  4. Parseraren slutför en virtuell post på nivå 3, återgår till nivå 2 och klarar C igen.
  5. Parseraren slutför en virtuell post på nivå 2, återgår till nivå 1 och klarar B och C igen.
  6. Parseraren slutför en virtuell inmatning, returnerar och utför en riktig inmatning genom A, B och C.

Som ett resultat kommer alla kontroller inom C att göras 4 gånger, inom B – 3 gånger, inom A – 2 gånger.

Men vad händer om ett passande alternativ finns på andra eller tredje plats i listan? Då kommer ett av predikatstadierna att misslyckas. Dess position i texten kommer att rullas tillbaka, och ett annat predikat börjar köras.

När vi analyserar orsakerna till att appen fryser, stöter vi ofta på spåret av SynPred avrättad flera tusen gånger. SynPred s är särskilt problematiska i rekursiva regler. Tyvärr är SQL rekursiv till sin natur. Möjligheten att använda subqueries nästan överallt har sitt pris. Det är dock möjligt att manipulera regeln för att få ett predikat att försvinna.

SynPred skadar prestandan. Vid något tillfälle sattes deras antal under hård kontroll. Men problemet är att när du skriver grammatikkod kan SynPred verka otydligt för dig. Ändra en regel kan göra att SynPred visas i en annan regel, och det gör kontroll över dem praktiskt taget omöjlig.

Vi skapade ett enkelt reguljärt uttryck verktyg för att kontrollera antalet predikat som körs av den speciella MSBuild Task . Om antalet predikat inte överensstämde med antalet angivna i en fil, misslyckades uppgiften omedelbart i bygget och varnade för ett fel.

När en utvecklare ser felet bör han skriva om koden för regeln flera gånger för att ta bort de överflödiga predikaten. Om man inte kan undvika predikat, skulle utvecklaren lägga till det i en speciell fil som drar extra uppmärksamhet för granskningen.

Vid sällsynta tillfällen skrev vi till och med våra predikat med C# bara för att undvika de ANTLR-genererade. Som tur är finns den här metoden också.

Grammatikärvning

När det finns några ändringar i våra stödda DBMS:er måste vi möta dem i våra verktyg. Stöd för grammatiska syntaxkonstruktioner är alltid en utgångspunkt.

Vi skapar en speciell grammatik för varje SQL-dialekt. Det möjliggör viss kodupprepning, men det är lättare än att försöka hitta vad de har gemensamt.

Vi valde att skriva vår egen ANTLR grammatikförbehandlare som ärvs av grammatik.

Det blev också uppenbart att vi behövde en mekanism för polymorfism – förmågan att inte bara omdefiniera regeln i efterkommande utan också kalla den grundläggande. Vi skulle också vilja kontrollera positionen när vi anropar basregeln.

Verktyg är ett klart plus när vi jämför ANTLR med andra verktyg för språkigenkänning, Visual Studio och ANTLRWorks. Och du vill inte förlora denna fördel när du implementerar arvet. Lösningen var att specificera grundläggande grammatik i en ärvd grammatik i ett ANTLR-kommentarformat. För ANTLR-verktyg är det bara en kommentar, men vi kan extrahera all nödvändig information från den.

Vi skrev en MsBuild Task som var inbäddad i hela byggsystemet som pre-build-action. Uppgiften var att göra jobbet som en förprocessor för ANTLR-grammatik genom att generera den resulterande grammatiken från dess bas och ärvda kamrater. Den resulterande grammatiken bearbetades av ANTLR själv.

ANTLR-efterbehandling

I många programmeringsspråk kan nyckelord inte användas som ämnesnamn. Det kan finnas från 800 till 3000 nyckelord i SQL beroende på dialekt. De flesta av dem är knutna till sammanhanget i databaser. Att förbjuda dem som objektnamn skulle därför frustrera användarna. Det är därför SQL har reserverade och oreserverade sökord.

Du kan inte namnge ditt objekt som det reserverade ordet (SELECT, FROM, etc.) utan att citera det, men du kan göra detta till ett oreserverat ord (CONVERSATION, AVAILABILITY, etc.). Denna interaktion gör analysens utveckling svårare.

Under den lexikaliska analysen är sammanhanget okänt, men en parser kräver redan olika nummer för identifieraren och nyckelordet. Det är därför vi har lagt till ytterligare en efterbehandling till ANTLR-parsern. Den ersatte alla uppenbara identifierarkontroller med att anropa en speciell metod.

Denna metod har en mer detaljerad kontroll. Om posten anropar en identifierare, och vi förväntar oss att identifieraren uppfylls och framåt, så är allt bra. Men om ett oreserverat ord är en post bör vi dubbelkolla det. Denna extra kontroll granskar grensökningen i det aktuella sammanhanget där detta oreserverade nyckelord kan vara ett nyckelord. Om det inte finns några sådana grenar kan den användas som en identifierare.

Tekniskt sett skulle detta problem kunna lösas med hjälp av ANTLR men detta beslut är inte optimalt. ANTLR-sättet är att skapa en regel som listar alla oreserverade nyckelord och en lexemidentifierare. Längre fram kommer en specialregel att fungera istället för en lexemidentifierare. Denna lösning gör att en utvecklare inte glömmer att lägga till nyckelordet där det används och i specialregeln. Dessutom optimerar det tidsåtgången.

Fel i syntaxanalys utan träd

Syntaxträdet är vanligtvis ett resultat av analysarbete. Det är en datastruktur som speglar programtexten genom formell grammatik. Om du vill implementera en kodredigerare med språkets automatiska komplettering kommer du troligen att få följande algoritm:

  1. Parse texten i redigeraren. Då får du ett syntaxträd.
  2. Hitta en nod under vagnen och matcha den mot grammatiken.
  3. Ta reda på vilka nyckelord och objekttyper som kommer att vara tillgängliga vid punkten.

I det här fallet är grammatiken lätt att föreställa sig som en graf eller en tillståndsmaskin.

Tyvärr var bara den tredje versionen av ANTLR tillgänglig när dbForge IDE hade startat sin utveckling. Det var dock inte lika smidigt och även om du kunde berätta för ANTLR hur man bygger ett träd, var användningen inte smidig.

Dessutom föreslog många artiklar om detta ämne att man skulle använda "actions"-mekanismen för att köra kod när parsern gick igenom regeln. Denna mekanism är väldigt praktisk, men den har lett till arkitektoniska problem och gjort stödet till ny funktionalitet mer komplext.

Saken är att en enda grammatikfil började samla "åtgärder" på grund av det stora antalet funktioner som snarare borde ha distribuerats till olika byggnader. Vi lyckades distribuera åtgärdshanterare till olika versioner och skapa en lömsk variant av mönster för prenumerant-aviseringar för den åtgärden.

ANTLR3 fungerar 6 gånger snabbare än ANTLR4 enligt våra mått. Syntaxträdet för stora skript kunde också ta för mycket RAM-minne, vilket inte var goda nyheter, så vi behövde arbeta inom 32-bitars adressutrymmet i Visual Studio och SQL Management Studio.

ANTLR-parserefterbehandling

När man arbetar med strängar är ett av de mest kritiska ögonblicken det skede av lexikal analys där vi delar in manuset i separata ord.

ANTLR tar som input grammatik som specificerar språket och matar ut en parser på ett av de tillgängliga språken. Vid något tillfälle växte den genererade parsern till en sådan grad att vi var rädda för att felsöka den. Skulle du trycka på F11 (steg in i) när du felsöker och gå till parserfilen, skulle Visual Studio bara krascha.

Det visade sig att det misslyckades på grund av ett OutOfMemory-undantag vid analys av parserfilen. Den här filen innehöll mer än 200 000 rader kod.

Men att felsöka parsern är en viktig del av arbetsprocessen, och du kan inte utelämna den. Med hjälp av partiella C#-klasser analyserade vi den genererade parsern med hjälp av reguljära uttryck och delade upp den i några filer. Visual Studio fungerade perfekt med det.

Lexikal analys utan delsträng före Span API

Huvuduppgiften för lexikal analys är klassificering – definiera gränserna för orden och kontrollera dem mot en ordbok. Om ordet hittas, skulle lexern returnera sitt index. Om inte, anses ordet vara en objektidentifierare. Detta är en förenklad beskrivning av algoritmen.

Lexning i bakgrunden under filöppning

Syntaxmarkering är baserad på lexikal analys. Denna operation tar vanligtvis mycket längre tid jämfört med att läsa text från disken. Vad är haken? I en tråd läses texten från filen, medan den lexikala analysen utförs i en annan tråd.

Lexaren läser texten rad för rad. Om den begär en rad som inte finns kommer den att stanna och vänta.

BlockingCollection från BCL fungerar på liknande sätt, och algoritmen omfattar en typisk tillämpning av ett samtidigt Producer-Consumer-mönster. Redaktören som arbetar i huvudtråden begär data om den första markerade raden, och om den inte är tillgänglig kommer den att stanna och vänta. I vår editor har vi använt producent-konsumentmönster och blockeringssamling två gånger:

  1. Att läsa från en fil är en producent, medan lexer är en konsument.
  2. Lexer är redan en producent och textredigeraren är en konsument.

Denna uppsättning knep gör att vi kan avsevärt förkorta tiden som ägnas åt att öppna stora filer. Den första sidan av dokumentet visas mycket snabbt, dock kan dokumentet frysa om användare försöker flytta till slutet av filen inom de första sekunderna. Det händer för att bakgrundsläsaren och lexern behöver nå slutet av dokumentet. Men om användaren arbetar långsamt från början av dokumentet mot slutet kommer det inte att finnas några märkbara frysningar.

Tvetydig optimering:partiell lexikal analys

Den syntaktiska analysen är vanligtvis uppdelad i två nivåer:

  • indatateckenströmmen bearbetas för att få lexem (tokens) baserat på språkreglerna – detta kallas lexikal analys
  • parsern förbrukar tokenström och kontrollerar den enligt de formella grammatikreglerna och bygger ofta ett syntaxträd.

Strängbearbetning är en kostsam operation. För att optimera den bestämde vi oss för att inte utföra en fullständig lexikal analys av texten varje gång utan bara analysera om den del som ändrades. Men hur hanterar man flerradskonstruktioner som blockkommentarer eller linjer? Vi lagrade ett radsluttillstånd för varje rad:"inga flerradssymboler" =0, "början av en blockkommentar" =1, "början av en flerradssträng bokstavlig" =2. Den lexikala analysen börjar från det ändrade avsnittet och slutar när linjesluttillståndet är lika med det lagrade.

Det fanns ett problem med den här lösningen:det är extremt obekvämt att övervaka linjenummer i sådana strukturer medan linjenummer är ett obligatoriskt attribut för en ANTLR-token, eftersom när en linje infogas eller raderas bör numret på nästa rad uppdateras i enlighet med detta. Vi löste det genom att sätta ett radnummer direkt, innan vi lämnade över token till parsern. Testerna vi utförde senare har visat att prestandan förbättrades med 15-25%. Den faktiska förbättringen var ännu större.

Mängden RAM som krävdes för allt detta visade sig vara mycket mer än vi förväntade oss. En ANTLR-token bestod av:en startpunkt – 8 byte, en slutpunkt – 8 byte, en länk till ordets text – 4 eller 8 byte (utan att nämna själva strängen), en länk till dokumentets text – 4 eller 8 byte, och en tokentyp – 4 byte.

Så vad kan vi dra slutsatsen? Vi fokuserade på prestanda och fick överdriven förbrukning av RAM på en plats vi inte förväntade oss. Vi antog inte att detta skulle hända eftersom vi försökte använda lätta strukturer istället för klasser. Genom att ersätta dem med tunga föremål gick vi medvetet på ytterligare minneskostnader för att få bättre prestanda. Lyckligtvis lärde detta oss en viktig läxa, så nu slutar varje prestandaoptimering med att profilera minnesförbrukningen och vice versa.

Det här är en berättelse med en moral. Vissa funktioner började fungera nästan omedelbart och andra bara lite snabbare. När allt kommer omkring skulle det vara omöjligt att utföra tricket för bakgrundslexikalanalys om det inte fanns ett objekt där en av trådarna kunde lagra tokens.

Alla ytterligare problem uppstår i samband med skrivbordsutveckling på .NET-stacken.

32-bitarsproblemet

Vissa användare väljer att använda fristående versioner av våra produkter. Andra fortsätter att arbeta i Visual Studio och SQL Server Management Studio. Många tillägg har utvecklats för dem. En av dessa tillägg är SQL Complete. För att förtydliga, ger den fler krafter och funktioner än standardkodkompletterings-SSMS och VS för SQL.

SQL-analys är en mycket kostsam process, både när det gäller CPU- och RAM-resurser. För att visa listan över objekt i användarskript, utan onödiga anrop till servern, lagrar vi objektcachen i RAM. Ofta tar det inte mycket utrymme, men vissa av våra användare har databaser som innehåller upp till en kvarts miljon objekt.

Att arbeta med SQL skiljer sig ganska mycket från att arbeta med andra språk. I C# finns det praktiskt taget inga filer även med tusen rader kod. Under tiden kan en utvecklare i SQL arbeta med en databasdump bestående av flera miljoner rader kod. Det är inget ovanligt med det.

DLL-Helvet i VS

Det finns ett praktiskt verktyg för att utveckla plugins i .NET Framework, det är en applikationsdomän. Allt utförs på ett isolerat sätt. Det går att lossa. För det mesta är implementeringen av tillägg kanske den främsta anledningen till att applikationsdomäner introducerades.

Det finns också MAF Framework, som designades av MS för att lösa problemet med att skapa tillägg till programmet. Den isolerar dessa tillägg till en sådan grad att den kan skicka dem till en separat process och ta över all kommunikation. Uppriktigt sagt är den här lösningen för besvärlig och har inte vunnit mycket popularitet.

Tyvärr implementerar Microsoft Visual Studio och SQL Server Management Studio på det förlängningssystemet på olika sätt. Det förenklar åtkomsten av värdapplikationer för plugins, men det tvingar dem att passa ihop inom en process och domän med en annan.

Precis som alla andra applikationer på 2000-talet har vår många beroenden. Majoriteten av dem är välkända, beprövade och populära bibliotek i .NET-världen.

Dra meddelanden i ett lås

Det är inte allmänt känt att .NET Framework kommer att pumpa Windows Message Queue in i varje WaitHandle. För att placera det i varje lås, kan vilken som helst hanterare av vilken händelse som helst i en applikation anropas om detta lås har tid att byta till kärnläge och det inte släpps under spin-wait-fasen.

Detta kan resultera i återinträde på vissa mycket oväntade platser. Några gånger ledde det till problem som "Samlingen modifierades under uppräkningen" och olika ArgumentOutOfRangeException.

Lägga till en sammansättning till en lösning med SQL

När projektet växer utvecklas uppgiften att lägga till sammansättningar, enkla till en början, till ett dussin komplicerade steg. En gång var vi tvungna att lägga till ett dussin olika sammansättningar till lösningen, vi utförde en stor refaktorisering. Nästan 80 lösningar, inklusive produkt- och testlösningar, skapades baserat på cirka 300 .NET-projekt.

Baserat på produktlösningar skrev vi Inno Setup-filer. De inkluderade listor över sammansättningar förpackade i installationen som användaren laddade ner. Algoritmen för att lägga till ett projekt var följande:

  1. Skapa ett nytt projekt.
  2. Lägg till ett certifikat. Ställ in taggen för bygget.
  3. Lägg till en versionsfil.
  4. Konfigurera om vägarna dit projektet går.
  5. Byt namn på mappen så att den matchar den interna specifikationen.
  6. Lägg till projektet i lösningen igen.
  7. Lägg till ett par sammansättningar som alla projekt behöver länkar till.
  8. Lägg till bygget till alla nödvändiga lösningar:test och produkt.
  9. För alla produktlösningar, lägg till sammansättningarna i installationen.

Dessa 9 steg fick upprepas cirka 10 gånger. Steg 8 och 9 är inte så triviala, och det är lätt att glömma att lägga till byggen överallt.

Inför en så stor och rutinmässig uppgift skulle vilken normal programmerare som helst vilja automatisera den. Det var precis vad vi ville göra. Men hur anger vi exakt vilka lösningar och installationer som ska läggas till i det nyskapade projektet? Det finns så många scenarier och vad mer är, det är svårt att förutse några av dem.

Vi kom på en galen idé. Lösningar är kopplade till projekt som många-till-många, projekt med installationer på samma sätt, och SQL kan lösa precis den typ av uppgifter som vi hade.

Vi skapade en .Net Core Console-app som skannar alla .sln-filer i källmappen, hämtar listan över projekt från dem med hjälp av DotNet CLI och lägger den i SQLite-databasen. Programmet har några lägen:

  • Ny – skapar ett projekt och alla nödvändiga mappar, lägger till ett certifikat, ställer in en tagg, lägger till en version, minsta nödvändiga sammansättningar.
  • Add-Project – lägger till projektet i alla lösningar som uppfyller SQL-frågan som kommer att ges som en av parametrarna. För att lägga till projektet i lösningen använder programmet inuti DotNet CLI.
  • Add-ISS – lägger till projektet i alla installationer som uppfyller SQL-frågor.

Även om idén att ange listan med lösningar genom SQL-frågan kan verka besvärlig, stängde den helt alla befintliga fall och troligen alla möjliga fall i framtiden.

Låt mig demonstrera scenariot. Skapa ett projekt "A" och lägg till det i alla lösningar där projekt “B” används:

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

Ett problem med LiteDB

För ett par år sedan fick vi i uppdrag att utveckla en bakgrundsfunktion för att spara användardokument. Den hade två huvudsakliga programflöden:möjligheten att omedelbart stänga IDE och lämna, och när du återvände till start där du slutade, och förmågan att återställa i brådskande situationer som strömavbrott eller programkrascher.

För att genomföra denna uppgift var det nödvändigt att spara innehållet i filerna någonstans på sidan och göra det ofta och snabbt. Förutom innehållet var det nödvändigt att spara lite metadata, vilket gjorde direktlagring i filsystemet obekvämt.

Vid den tidpunkten hittade vi LiteDB-biblioteket, som imponerade på oss med sin enkelhet och prestanda. LiteDB är en snabb och lätt inbäddad databas, som helt skrevs i C#. Snabbheten och den övergripande enkelheten vann oss över.

Under utvecklingsprocessen var hela teamet nöjda med arbetet med LiteDB. De största problemen började dock efter releasen.

Den officiella dokumentationen garanterade att databasen säkerställde korrekt arbete med samtidig åtkomst från flera trådar samt flera processer. Aggressiva syntetiska tester visade att databasen inte fungerar korrekt i en flertrådad miljö.

För att snabbt åtgärda problemet synkroniserade vi processerna med hjälp av den självskrivna interprocessen ReadWriteLock. Nu, efter nästan tre år, fungerar LiteDB mycket bättre.

StreamStringList

Detta problem är motsatsen till fallet med den partiella lexikala analysen. När vi arbetar med en text är det bekvämare att arbeta med den som en stränglista. Strängar kan begäras i slumpmässig ordning, men viss minnesåtkomstdensitet finns fortfarande. Vid något tillfälle var det nödvändigt att köra flera uppgifter för att bearbeta mycket stora filer utan full minnesbelastning. Tanken var följande:

  1. Att läsa filen rad för rad. Kom ihåg förskjutningar i filen.
  2. På begäran, utfärda nästa raduppsättning en obligatorisk offset och returnera data.

Huvuduppgiften är klar. Denna struktur tar inte upp mycket utrymme jämfört med filstorleken. I teststadiet kontrollerar vi noggrant minnesfotavtrycket för stora och mycket stora filer. Stora filer har bearbetats under lång tid och små kommer att behandlas omedelbart.

Det fanns ingen referens för kontroll av tiden för körning . RAM kallas Random Access Memory – det är dess konkurrensfördel gentemot SSD och särskilt över HDD. Dessa drivrutiner börjar fungera dåligt för slumpmässig åtkomst. Det visade sig att detta tillvägagångssätt saktade ner arbetet med nästan 40 gånger, jämfört med att ladda en fil helt i minnet. Dessutom läser vi filen 2,5 -10 hela gånger beroende på sammanhanget.

Lösningen var enkel och det räckte med förbättringar så att operationen bara skulle ta lite längre tid än när filen är helt laddad i minnet.

Likaså var RAM-förbrukningen också obetydlig. Vi hittade inspiration i principen att ladda data från RAM till en cache-processor. När du kommer åt ett arrayelement kopierar processorn dussintals intilliggande element till sin cache eftersom de nödvändiga elementen ofta finns i närheten.

Många datastrukturer använder denna processoroptimering för att få högsta prestanda. Det är på grund av denna egenhet som slumpmässig åtkomst till arrayelement är mycket långsammare än sekventiell åtkomst. Vi implementerade en liknande mekanism:vi läste en uppsättning av tusen strängar och kom ihåg deras förskjutningar. När vi kommer åt den 1001:a strängen släpper vi de första 500 strängarna och laddar nästa 500. Om vi ​​behöver någon av de första 500 raderna, går vi till den separat, eftersom vi redan har offseten.

Programmeraren behöver inte nödvändigtvis noggrant formulera och kontrollera icke-funktionella krav. Som ett resultat kom vi ihåg för framtida fall att vi måste arbeta sekventiellt med beständigt minne.

Analysera undantagen

Du kan enkelt samla in användaraktivitetsdata på webben. Det är dock inte fallet med att analysera skrivbordsapplikationer. Det finns inget sådant verktyg som kan ge en otrolig uppsättning mätvärden och visualiseringsverktyg som Google Analytics. Varför? Här är mina antaganden:

  1. Under större delen av utvecklingen av datorapplikationers historia hade de ingen stabil och permanent tillgång till webben.
  2. Det finns många utvecklingsverktyg för skrivbordsapplikationer. Därför är det omöjligt att bygga ett multifunktionsverktyg för insamling av användardata för alla ramverk och tekniker för användargränssnitt.

En nyckelaspekt med att samla in data är att spåra undantag. Vi samlar till exempel in data om krascher. Tidigare var våra användare tvungna att själva skriva till kundsupportens e-post och lägga till en Stack Trace av ett fel, som kopierades från ett speciellt appfönster. Få användare följde alla dessa steg. Insamlad data är helt anonymiserad, vilket berövar oss möjligheten att ta reda på reproduktionssteg eller annan information från användaren.

Å andra sidan finns feldata i Postgres-databasen, och detta banar väg för en omedelbar kontroll av dussintals hypoteser. Du kan omedelbart få svaren genom att helt enkelt göra SQL-frågor till databasen. Det är ofta oklart från bara en stack eller undantagstyp hur undantaget inträffade, det är därför all denna information är avgörande för att studera problemet.

Utöver det har du möjlighet att analysera all insamlad data och hitta de mest problematiska modulerna och klasserna. Med hjälp av analysresultaten kan du planera omfaktorisering eller ytterligare tester för att täcka dessa delar av programmet.

Stackavkodningstjänst

.NET-byggen innehåller IL-kod, som enkelt kan konverteras tillbaka till C#-kod, exakt för operatören, med hjälp av flera specialprogram. Ett av sätten att skydda programkoden är dess förvirring. Program kan bytas namn; metoder, variabler och klasser kan ersättas; kod kan ersättas med dess motsvarighet, men det är verkligen obegripligt.

Nödvändigheten att fördunkla källkoden uppstår när du distribuerar din produkt på ett sätt som tyder på att användaren får versionerna av din applikation. Desktopapplikationer är dessa fall. Alla builds, inklusive mellanliggande builds för testare, är noggrant förvirrade.

Vår kvalitetssäkringsenhet använder avkodningsstackverktyg från obfuscator-utvecklaren. För att börja avkoda måste de köra programmet, hitta deobfuskationskartor som publicerats av CI för en specifik konstruktion och infoga undantagsstacken i inmatningsfältet.

Olika versioner och redaktörer fördunklades på ett annat sätt, vilket gjorde det svårt för en utvecklare att studera problemet eller till och med satte honom på fel spår. Det var uppenbart att denna process måste automatiseras.

Deobfuscation kartformatet visade sig vara ganska okomplicerat. Vi tog lätt upp det och skrev ett stackavkodningsprogram. Kort dessförinnan utvecklades ett webbgränssnitt för att rendera undantag efter produktversioner och gruppera dem i stacken. Det var en .NET Core-webbplats med en databas i SQLite.

SQLite är ett snyggt verktyg för små lösningar. Vi försökte sätta deobfuskationskartor där också. Varje build genererade cirka 500 tusen krypterings- och dekrypteringspar. SQLite kunde inte hantera en så aggressiv infogningshastighet.

Medan data på en build infogades i databasen lades ytterligare två till i kön. Inte långt innan det problemet lyssnade jag på ett reportage om Clickhouse och var ivrig att testa det. Det visade sig vara utmärkt, insättningshastigheten ökade med mer än 200 gånger.

Som sagt, stackavkodning (läsning från databas) saktades ner med nästan 50 gånger, men eftersom varje stack tog mindre än 1 ms var det kostnadseffektivt att lägga tid på att studera detta 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.


  1. Oracle INTERSECT Operator förklaras

  2. Hur man ändrar sekunder till ett tidsvärde i MySQL

  3. När ska jag använda ett sammansatt index?

  4. Vilken typ av JOIN som ska användas