Více než pět let pracuji pro společnost, která vyvíjí IDE pro interakci s databázemi. Než jsem začal psát tento článek, netušil jsem, kolik fantastických příběhů mě čeká.
Můj tým vyvíjí a podporuje funkce jazyka IDE a automatické dokončování kódu je tou hlavní. Setkal jsem se s mnoha vzrušujícími věcmi. Některé věci se nám povedly na první pokus skvěle, jiné se nezdařily ani po několika výstřelech.
Analýza SQL a dialektů
SQL je pokus vypadat jako přirozený jazyk a pokus je docela úspěšný, řekl bych. V závislosti na dialektu existuje několik tisíc klíčových slov. Chcete-li rozlišit jeden výrok od druhého, musíte často hledat jedno nebo dvě slova (tokeny) dopředu. Tento přístup se nazývá dohled .
Existuje klasifikace analyzátoru v závislosti na tom, jak daleko se mohou dívat dopředu:LA(1), LA(2) nebo LA(*), což znamená, že analyzátor se může dívat tak daleko dopředu, jak je potřeba, aby definoval správnou větev.
Někdy se konec nepovinné klauzule shoduje se začátkem jiné volitelné klauzule. Tyto situace značně znesnadňují spuštění analýzy. T-SQL věci neusnadňuje. Některé příkazy SQL také mohou mít, ale ne nutně, konce, které mohou být v konfliktu se začátkem předchozích příkazů.
nevěříte tomu? Existuje způsob, jak popsat formální jazyky prostřednictvím gramatiky. Pomocí toho či onoho nástroje z něj můžete vygenerovat analyzátor. Nejpozoruhodnějšími nástroji a jazyky, které popisují gramatiku, jsou YACC a ANTLR.
YACC -generované analyzátory se používají v motorech MySQL, MariaDB a PostgreSQL. Mohli bychom je zkusit vzít přímo ze zdrojového kódu a vyvinout dokončení kódu a další funkce založené na analýze SQL pomocí těchto analyzátorů. Kromě toho by tento produkt dostával bezplatné vývojové aktualizace a analyzátor by se choval stejně jako zdrojový modul.
Proč tedy stále používáme ANTLR ? Pevně podporuje C#/.NET, má slušnou sadu nástrojů, jeho syntaxe je mnohem jednodušší na čtení i zápis. Syntaxe ANTLR se stala tak užitečnou, že ji nyní Microsoft používá ve své oficiální dokumentaci C#.
Ale vraťme se ke složitosti SQL, pokud jde o analýzu. Rád bych porovnal gramatické velikosti veřejně dostupných jazyků. V dbForge používáme naše části gramatiky. Jsou úplnější než ostatní. Bohužel jsou přetíženy vložkami kódu C# pro podporu různých funkcí.
Velikosti gramatiky pro různé jazyky jsou následující:
JS – 475 řádků analyzátoru + 273 lexerů =748 řádků
Java – 615 řádků analyzátoru + 211 lexerů =826 řádků
C# – 1159 řádků analyzátoru + 433 lexerů =1592 řádků
С++ – 1933 řádků
MySQL – 2515 řádků analyzátoru + 1189 lexerů =3704 řádků
T-SQL – 4035 řádků analyzátoru + 896 lexerů =4931 řádků
PL SQL – 6719 řádků analyzátoru + 2366 lexerů =9085 řádků
Koncovky některých lexerů obsahují seznamy znaků Unicode dostupných v daném jazyce. Tyto seznamy jsou z hlediska hodnocení jazykové složitosti k ničemu. Počet řádků, které jsem vzal, tedy vždy skončil před těmito seznamy.
Hodnocení složitosti analýzy jazyka na základě počtu řádků v jazykové gramatice je diskutabilní. Přesto věřím, že je důležité ukázat čísla, která ukazují obrovský rozdíl.
To není vše. Protože vyvíjíme IDE, měli bychom se vypořádat s neúplnými nebo neplatnými skripty. Museli jsme vymyslet mnoho triků, ale zákazníci stále posílají mnoho pracovních scénářů s nedokončenými skripty. Musíme to vyřešit.
Predikátní války
Během analýzy kódu vám slovo někdy neřekne, kterou ze dvou alternativ si vybrat. Mechanismus, který řeší tento typ nepřesností, je dohled v ANTLR. Metoda analyzátoru je vložený řetězec if's a každý z nich vypadá o krok vpřed. Viz příklad gramatiky, která generuje nejistotu tohoto druhu:
rule1:
'a' rule2 | rule3
;
rule2:
'b' 'c' 'd'
;
rule3:
'b' 'c' 'e'
;
Uprostřed pravidla 1, když je token „a“ již předán, se analyzátor podívá o dva kroky dopředu, aby zvolil pravidlo, které se má následovat. Tato kontrola bude provedena ještě jednou, ale tuto gramatiku lze přepsat, aby se vyloučily hledání . Nevýhodou je, že takové optimalizace poškozují strukturu, zatímco zvýšení výkonu je spíše malé.
Existují složitější způsoby, jak tento druh nejistoty vyřešit. Například predikát syntaxe (SynPred) mechanismus v ANTLR3 . Pomáhá, když volitelná koncovka klauzule protíná začátek další volitelné klauzule.
Pokud jde o ANTLR3, predikát je vygenerovaná metoda, která provádí virtuální zadávání textu podle jedné z alternativ . Když bude úspěšný, vrátí true hodnotu a dokončení predikátu je úspěšné. Pokud se jedná o virtuální záznam, nazývá se to backtracking vstup do režimu. Pokud predikát funguje úspěšně, dojde ke skutečnému vstupu.
Je to jen problém, když predikát začíná uvnitř jiného predikátu. Pak může být jedna vzdálenost překročena stokrát nebo tisíckrát.
Podívejme se na zjednodušený příklad. Existují tři body nejistoty:(A, B, C).
- Analyzátor zadá A, zapamatuje si jeho pozici v textu a spustí virtuální záznam úrovně 1.
- Analyzátor zadá B, zapamatuje si jeho pozici v textu a spustí virtuální záznam úrovně 2.
- Analyzátor zadá C, zapamatuje si jeho pozici v textu a spustí virtuální záznam úrovně 3.
- Analyzátor dokončí virtuální záznam úrovně 3, vrátí se na úroveň 2 a znovu předá C.
- Analyzátor dokončí virtuální záznam úrovně 2, vrátí se na úroveň 1 a znovu předá B a C.
- Analyzátor dokončí virtuální záznam, vrátí se a provede skutečný záznam přes A, B a C.
V důsledku toho budou všechny kontroly v rámci C provedeny 4krát, v rámci B – 3krát, v rámci A – 2krát.
Ale co když je vhodná alternativa na druhém nebo třetím v seznamu? Pak jedna z predikátových fází selže. Jeho pozice v textu se vrátí zpět a spustí se další predikát.
Při analýze důvodů zamrznutí aplikace často narazíme na stopu SynPred popraven několik tisíckrát. SynPred s jsou zvláště problematické v rekurzivních pravidlech. Je smutné, že SQL je ze své podstaty rekurzivní. Možnost používat poddotazy téměř všude má svou cenu. Je však možné manipulovat s pravidlem tak, aby predikát zmizel.
SynPred poškozuje výkon. V určitém okamžiku byl jejich počet pod přísnou kontrolou. Problém je ale v tom, že když píšete gramatický kód, SynPred se vám může zdát nezřejmý. Navíc změna jednoho pravidla může způsobit, že se SynPred objeví v jiném pravidle, a to prakticky znemožňuje kontrolu nad nimi.
Vytvořili jsme jednoduchý regulární výraz nástroj pro řízení počtu predikátů spuštěných speciální MSBuild Task . Pokud se počet predikátů neshodoval s počtem zadaným v souboru, úloha se okamžitě nezdařila a upozornila na chybu.
Když vývojář uvidí chybu, měl by kód pravidla několikrát přepsat, aby odstranil nadbytečné predikáty. Pokud se nelze vyhnout predikátům, vývojář by je přidal do speciálního souboru, který přitáhne zvláštní pozornost k recenzi.
Ve vzácných případech jsme dokonce psali naše predikáty pomocí C#, abychom se vyhnuli těm generovaným ANTLR. Naštěstí tato metoda také existuje.
Gramatická dědičnost
Když dojde k jakýmkoli změnám v našich podporovaných DBMS, musíme je splnit v našich nástrojích. Podpora konstrukcí gramatické syntaxe je vždy výchozím bodem.
Pro každý dialekt SQL vytváříme speciální gramatiku. Umožňuje určité opakování kódu, ale je to jednodušší, než se snažit najít, co mají společného.
Rozhodli jsme se napsat náš vlastní gramatický preprocesor ANTLR, který dědí gramatiku.
Také se ukázalo, že potřebujeme mechanismus pro polymorfismus – schopnost pravidlo nejen předefinovat v potomkovi, ale také zavolat to základní. Rádi bychom také řídili pozici při volání základního pravidla.
Nástroje jsou jednoznačným plusem, když porovnáme ANTLR s jinými nástroji pro rozpoznávání jazyků, Visual Studio a ANTLRWorks. A o tuto výhodu při implementaci dědictví nechcete přijít. Řešením bylo specifikovat základní gramatiku v zděděné gramatice ve formátu komentáře ANTLR. Pro nástroje ANTLR je to jen komentář, ale můžeme z něj získat všechny požadované informace.
Napsali jsme úlohu MsBuild Task, která byla vložena do celého systému sestavení jako akce před sestavením. Úkolem bylo udělat práci preprocesoru pro gramatiku ANTLR vygenerováním výsledné gramatiky z jejích základních a zděděných vrstevníků. Výsledná gramatika byla zpracována samotným ANTLR.
Následné zpracování ANTLR
V mnoha programovacích jazycích nelze klíčová slova použít jako názvy předmětů. V SQL může být 800 až 3000 klíčových slov v závislosti na dialektu. Většina z nich je vázána na kontext uvnitř databází. Zakázat je jako názvy objektů by tedy uživatele frustrovalo. To je důvod, proč má SQL vyhrazená a nerezervovaná klíčová slova.
Svůj objekt nemůžete pojmenovat jako vyhrazené slovo (SELECT, FROM atd.), aniž byste jej uvozovali, ale můžete to udělat s nevyhrazeným slovem (CONVERSATION, AVAILABILITY atd.). Tato interakce ztěžuje vývoj analyzátoru.
Během lexikální analýzy je kontext neznámý, ale analyzátor již vyžaduje různá čísla pro identifikátor a klíčové slovo. Proto jsme do analyzátoru ANTLR přidali další postprocessing. Nahradil všechny zřejmé kontroly identifikátorů voláním speciální metody.
Tato metoda má podrobnější kontrolu. Pokud záznam volá identifikátor a my očekáváme, že je identifikátor splněn, pak je vše v pořádku. Ale pokud je záznamem nevyhrazené slovo, měli bychom to znovu zkontrolovat. Tato zvláštní kontrola zkontroluje vyhledávání pobočky v aktuálním kontextu, kde toto nevyhrazené klíčové slovo může být klíčovým slovem. Pokud takové větve neexistují, lze jej použít jako identifikátor.
Technicky by tento problém mohl být vyřešen pomocí ANTLR, ale toto rozhodnutí není optimální. Způsob ANTLR je vytvořit pravidlo, které vypíše všechna nevyhrazená klíčová slova a identifikátor lexému. Dále bude místo identifikátoru lexému sloužit speciální pravidlo. Toto řešení umožňuje vývojáři nezapomenout přidat klíčové slovo tam, kde se používá, a do speciálního pravidla. Také optimalizuje strávený čas.
Chyby v syntaktické analýze bez stromů
Syntaktický strom je obvykle výsledkem práce parseru. Je to datová struktura, která odráží text programu prostřednictvím formální gramatiky. Pokud chcete implementovat editor kódu s automatickým dokončováním jazyka, s největší pravděpodobností získáte následující algoritmus:
- Analyzujte text v editoru. Poté získáte strom syntaxe.
- Najděte uzel pod vozíkem a porovnejte jej s gramatikou.
- Zjistěte, která klíčová slova a typy objektů budou v bodě k dispozici.
V tomto případě je gramatika snadno představitelná jako Graph nebo State Machine.
Bohužel, pouze třetí verze ANTLR byla k dispozici, když dbForge IDE zahájilo svůj vývoj. Nebylo to však tak svižné, a přestože jste mohli ANTLR říci, jak postavit strom, použití nebylo plynulé.
Mnoho článků na toto téma navíc navrhovalo použití mechanismu „akcí“ pro spouštění kódu, když analyzátor procházel pravidlem. Tento mechanismus je velmi praktický, ale vedl k architektonickým problémům a zkomplikoval podporu nových funkcí.
Jde o to, že jeden gramatický soubor začal hromadit „akce“ kvůli velkému počtu funkcí, které by měly být spíše distribuovány do různých sestav. Podařilo se nám distribuovat obslužné nástroje akcí do různých sestavení a pro toto opatření vytvořit záludnou variaci vzoru předplatitel-notifikátor.
ANTLR3 pracuje podle našich měření 6x rychleji než ANTLR4. Syntaktický strom pro velké skripty také mohl zabírat příliš mnoho paměti RAM, což nebyla dobrá zpráva, takže jsme museli pracovat v 32bitovém adresním prostoru sady Visual Studio a SQL Management Studio.
Následné zpracování analyzátoru ANTLR
Při práci s řetězci je jedním z nejkritičtějších momentů fáze lexikální analýzy, kdy rozdělujeme písmo na samostatná slova.
ANTLR bere jako vstupní gramatiku, která specifikuje jazyk a vydává analyzátor v jednom z dostupných jazyků. V určitém okamžiku vygenerovaný parser narostl do takové míry, že jsme se ho báli ladit. Pokud při ladění stisknete F11 (vstoupíte do) a přejdete do souboru analyzátoru, Visual Studio by prostě spadlo.
Ukázalo se, že selhal kvůli výjimce OutOfMemory při analýze souboru analyzátoru. Tento soubor obsahoval více než 200 000 řádků kódu.
Ale ladění analyzátoru je nezbytnou součástí pracovního procesu a nemůžete ho vynechat. S pomocí dílčích tříd C# jsme vygenerovaný parser analyzovali pomocí regulárních výrazů a rozdělili jej do několika souborů. Visual Studio s ním fungovalo perfektně.
Lexikální analýza bez podřetězce před rozhraním Span API
Hlavním úkolem lexikální analýzy je klasifikace – definování hranic slov a jejich porovnání se slovníkem. Pokud je slovo nalezeno, lexer vrátí jeho index. Pokud ne, je slovo považováno za identifikátor objektu. Toto je zjednodušený popis algoritmu.
Lexování na pozadí během otevírání souboru
Zvýraznění syntaxe je založeno na lexikální analýze. Tato operace obvykle trvá mnohem déle než čtení textu z disku. V čem je háček? V jednom vlákně se text čte ze souboru, zatímco lexikální analýza se provádí v jiném vláknu.
Lexer čte text řádek po řádku. Pokud požaduje řádek, který neexistuje, zastaví se a počká.
BlockingCollection
- Čtení ze souboru je producent, zatímco lexer je spotřebitel.
- Lexer je již producent a textový editor je spotřebitel.
Tato sada triků nám umožňuje výrazně zkrátit čas strávený otevíráním velkých souborů. První stránka dokumentu se zobrazí velmi rychle, dokument však může zamrznout, pokud se uživatelé během prvních několika sekund pokusí přesunout na konec souboru. Stává se to proto, že čtenář na pozadí a lexer potřebují dosáhnout konce dokumentu. Pokud se však uživatel pohybuje od začátku dokumentu ke konci pomalu, nedojde k žádnému znatelnému zamrznutí.
Nejednoznačná optimalizace:částečná lexikální analýza
Syntaktická analýza je obvykle rozdělena do dvou úrovní:
- vstupní znakový proud je zpracováván za účelem získání lexémů (tokenů) na základě jazykových pravidel – tomu se říká lexikální analýza
- analyzátor spotřebovává proud tokenů, kontroluje jej podle formálních pravidel gramatiky a často vytváří strom syntaxe.
Zpracování řetězců je nákladná operace. Abychom jej optimalizovali, rozhodli jsme se neprovádět pokaždé úplnou lexikální analýzu textu, ale znovu analyzovat pouze tu část, která byla změněna. Ale jak se vypořádat s víceřádkovými konstrukcemi, jako jsou blokové komentáře nebo řádky? Uložili jsme stav konce řádku pro každý řádek:„žádné víceřádkové tokeny“ =0, „začátek komentáře bloku“ =1, „začátek víceřádkového řetězcového literálu“ =2. Lexikální analýza začíná od změněné sekce a skončí, když se stav konce řádku rovná uloženému.
S tímto řešením byl jeden problém:je extrémně nepohodlné sledovat čísla řádků v takových strukturách, zatímco číslo řádku je povinným atributem tokenu ANTLR, protože když je řádek vložen nebo odstraněn, číslo dalšího řádku by se mělo odpovídajícím způsobem aktualizovat. Vyřešili jsme to tak, že jsme hned před předáním tokenu analyzátoru nastavili číslo řádku. Testy, které jsme provedli později, ukázaly, že výkon se zlepšil o 15–25 %. Skutečné zlepšení bylo ještě větší.
Množství paměti RAM potřebné k tomu všemu se ukázalo být mnohem větší, než jsme očekávali. Token ANTLR se skládal z:počátečního bodu – 8 bajtů, koncového bodu – 8 bajtů, odkazu na text slova – 4 nebo 8 bajtů (nemluvě o samotném řetězci), odkazu na text dokumentu – 4 nebo 8 bajtů, a typ tokenu – 4 bajty.
Co tedy můžeme uzavřít? Zaměřili jsme se na výkon a dostali jsme nadměrnou spotřebu RAM na místě, kde jsme to nečekali. Nepředpokládali jsme, že se to stane, protože jsme se místo tříd snažili použít lehké struktury. Tím, že jsme je nahradili těžkými předměty, jsme vědomě vynaložili další náklady na paměť, abychom získali lepší výkon. Naštěstí nás to naučilo důležitou lekci, takže nyní každá optimalizace výkonu končí profilováním spotřeby paměti a naopak.
Toto je příběh s morálkou. Některé funkce začaly fungovat téměř okamžitě a jiné jen o něco rychleji. Koneckonců, bylo by nemožné provést trik lexikální analýzy na pozadí, pokud by neexistoval objekt, kam by jedno z vláken mohlo ukládat tokeny.
Všechny další problémy se objevují v kontextu vývoje desktopů na .NET stacku.
Problém 32bitové verze
Někteří uživatelé se rozhodnou používat samostatné verze našich produktů. Jiní zůstávají v práci ve Visual Studiu a SQL Server Management Studio. Pro ně je vyvinuto mnoho rozšíření. Jedním z těchto rozšíření je SQL Complete. Abychom objasnili, poskytuje více pravomocí a funkcí než standardní SSMS pro dokončování kódu a VS pro SQL.
Analýza SQL je velmi nákladný proces, a to jak z hlediska zdrojů CPU, tak RAM. Abychom vyvolali seznam objektů v uživatelských skriptech, bez zbytečných volání na server, ukládáme mezipaměť objektů do paměti RAM. Často to nezabere moc místa, ale někteří naši uživatelé mají databáze, které obsahují až čtvrt milionu objektů.
Práce s SQL je zcela odlišná od práce s jinými jazyky. V C# nejsou prakticky žádné soubory ani s tisíci řádky kódu. Mezitím v SQL může vývojář pracovat s výpisem databáze sestávající z několika milionů řádků kódu. Není na tom nic neobvyklého.
DLL-Hell uvnitř VS
Existuje praktický nástroj pro vývoj zásuvných modulů v .NET Framework, je to aplikační doména. Vše se provádí izolovaným způsobem. Je možné vyložit. Implementace rozšíření je z větší části možná hlavním důvodem, proč byly zavedeny aplikační domény.
Existuje také MAF Framework, který byl navržen společností MS, aby vyřešil problém vytváření doplňků do programu. Izoluje tyto doplňky do takové míry, že je může poslat do samostatného procesu a převzít veškerou komunikaci. Upřímně řečeno, toto řešení je příliš těžkopádné a nezískalo velkou popularitu.
Microsoft Visual Studio a SQL Server Management Studio na něm postavené bohužel implementují systém rozšíření odlišně. Zjednodušuje to přístup k hostitelským aplikacím pro pluginy, ale nutí je, aby zapadaly do jednoho procesu a domény s jiným.
Stejně jako každá jiná aplikace v 21. století má i ta naše spoustu závislostí. Většina z nich jsou dobře známé, časem prověřené a oblíbené knihovny ve světě .NET.
Vytahování zpráv do zámku
Není všeobecně známo, že .NET Framework napumpuje Windows Message Queue do každého WaitHandle. Chcete-li jej umístit do každého zámku, lze zavolat jakýkoli ovladač jakékoli události v aplikaci, pokud má tento zámek čas přepnout se do režimu jádra a není uvolněn během fáze spin-wait.
To může mít za následek opětovné vniknutí na některá velmi neočekávaná místa. Několikrát to vedlo k problémům jako „Sbírka byla upravena během výčtu“ a různým výjimkám ArgumentOutOfRange.
Přidání sestavení do řešení pomocí SQL
Když projekt roste, úkol přidávat sestavy, zpočátku jednoduchý, se rozvine v tucet komplikovaných kroků. Jednou jsme museli k řešení přidat tucet různých sestav, provedli jsme velký refaktoring. Na základě přibližně 300 projektů .NET bylo vytvořeno téměř 80 řešení, včetně produktových a testovacích.
Na základě produktových řešení jsme napsali Inno Setup soubory. Zahrnovaly seznamy sestav zabalené v instalaci, kterou si uživatel stáhl. Algoritmus přidání projektu byl následující:
- Vytvořte nový projekt.
- Přidejte k němu certifikát. Nastavte značku sestavení.
- Přidejte soubor verze.
- Změňte konfiguraci cest, kudy projekt směřuje.
- Přejmenujte složku tak, aby odpovídala interní specifikaci.
- Ještě jednou přidejte projekt do řešení.
- Přidejte několik sestavení, na která všechny projekty potřebují odkazy.
- Přidejte sestavení ke všem nezbytným řešením:test a produkt.
- U všech řešení produktu přidejte k instalaci sestavy.
Těchto 9 kroků se muselo opakovat asi 10x. Kroky 8 a 9 nejsou tak triviální a je snadné zapomenout přidávat sestavení všude.
Tváří v tvář tak velkému a rutinnímu úkolu by jej každý normální programátor chtěl automatizovat. To je přesně to, co jsme chtěli udělat. Jak ale určíme, která řešení a instalace přesně přidat do nově vytvořeného projektu? Existuje tolik scénářů a co víc, je těžké některé z nich předvídat.
Přišli jsme na bláznivý nápad. Řešení jsou spojena s projekty jako many-to-many, projekty s instalacemi stejným způsobem a SQL dokáže vyřešit přesně ten druh úloh, které jsme měli my.
Vytvořili jsme .Net Core Console App, která prohledá všechny .sln soubory ve zdrojové složce, načte z nich seznam projektů pomocí DotNet CLI a vloží jej do databáze SQLite. Program má několik režimů:
- Nové – vytvoří projekt a všechny potřebné složky, přidá certifikát, nastaví tag, přidá verzi, minimum nezbytných sestavení.
- Add-Project – přidá projekt ke všem řešením, která splňují SQL dotaz, který bude zadán jako jeden z parametrů. K přidání projektu do řešení používá program uvnitř rozhraní DotNet CLI.
- Add-ISS – přidá projekt do všech instalací, které splňují SQL dotazy.
Ačkoli se nápad uvést seznam řešení prostřednictvím SQL dotazu může zdát těžkopádný, zcela uzavřel všechny existující případy a s největší pravděpodobností všechny možné případy v budoucnu.
Dovolte mi předvést scénář. Vytvořte projekt „A“ a přidejte jej ke všem řešením, kde projekty “B“ se používá:
dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"
Problém s LiteDB
Před pár lety jsme dostali úkol vyvinout funkci na pozadí pro ukládání uživatelských dokumentů. Měl dva hlavní aplikační toky:schopnost okamžitě zavřít IDE a odejít a po návratu začít od místa, kde jste skončili, a schopnost obnovit v naléhavých situacích, jako jsou výpadky napájení nebo selhání programu.
Pro realizaci tohoto úkolu bylo nutné uložit obsah souborů někam na stranu, a to často a rychle. Kromě obsahu bylo nutné uložit i některá metadata, což znepříjemňovalo přímé ukládání v souborovém systému.
V tu chvíli jsme našli knihovnu LiteDB, která nás zaujala svou jednoduchostí a výkonem. LiteDB je rychlá odlehčená vestavěná databáze, která byla celá napsána v C#. Rychlost a celková jednoduchost si nás získaly.
V průběhu vývoje byl celý tým spokojen s prací s LiteDB. Hlavní problémy však začaly po vydání.
Oficiální dokumentace zaručovala, že databáze zajišťuje správnou práci se současným přístupem z více vláken i několika procesů. Agresivní syntetické testy ukázaly, že databáze nefunguje správně ve vícevláknovém prostředí.
Abychom problém rychle vyřešili, synchronizovali jsme procesy s pomocí samostatně napsaného meziprocesu ReadWriteLock. Nyní, po téměř třech letech, LiteDB funguje mnohem lépe.
StreamStringList
Tento problém je opakem případu částečné lexikální analýzy. Když pracujeme s textem, je pohodlnější s ním pracovat jako se seznamem řetězců. Řetězce mohou být požadovány v náhodném pořadí, ale stále existuje určitá hustota přístupu do paměti. V určitém okamžiku bylo nutné spustit několik úloh pro zpracování velmi velkých souborů bez plného zatížení paměti. Myšlenka byla následující:
- Čtení souboru řádek po řádku. Zapamatovat si offsety v souboru.
- Na požádání zadejte na dalším řádku požadovaný offset a vraťte data.
Hlavní úkol je splněn. Tato struktura nezabírá mnoho místa ve srovnání s velikostí souboru. Ve fázi testování důkladně kontrolujeme paměťovou stopu pro velké a velmi velké soubory. Velké soubory byly zpracovány po dlouhou dobu a malé budou zpracovány okamžitě.
Neexistoval žádný odkaz na kontrolu času provedení . RAM se nazývá Random Access Memory – je to její konkurenční výhoda oproti SSD a především oproti HDD. Tyto ovladače začnou fungovat špatně pro náhodný přístup. Ukázalo se, že tento přístup zpomalil práci téměř 40krát ve srovnání s úplným načtením souboru do paměti. Kromě toho čteme soubor 2,5 až 10krát v závislosti na kontextu.
Řešení bylo jednoduché a vylepšení stačilo na to, aby operace trvala o něco déle, než když je soubor plně načten do paměti.
Stejně tak spotřeba RAM byla také zanedbatelná. Inspiraci jsme našli v principu načítání dat z RAM do cache procesoru. Když přistoupíte k prvku pole, procesor zkopíruje desítky sousedních prvků do své mezipaměti, protože potřebné prvky jsou často poblíž.
Mnoho datových struktur využívá tuto optimalizaci procesoru k získání špičkového výkonu. Právě kvůli této zvláštnosti je náhodný přístup k prvkům pole mnohem pomalejší než sekvenční přístup. Implementovali jsme podobný mechanismus:četli jsme sadu tisíců řetězců a pamatovali si jejich offsety. Když přistoupíme k 1001. řetězci, zahodíme prvních 500 řetězců a načteme dalších 500. V případě, že potřebujeme některý z prvních 500 řádků, přejdeme k němu samostatně, protože offset již máme.
Programátor nemusí nutně pečlivě formulovat a kontrolovat nefunkční požadavky. V důsledku toho jsme si pro budoucí případy pamatovali, že musíme pracovat postupně s trvalou pamětí.
Analýza výjimek
Na webu můžete snadno shromažďovat údaje o aktivitě uživatelů. To však není případ analýzy desktopových aplikací. Neexistuje žádný takový nástroj, který by byl schopen poskytnout neuvěřitelnou sadu metrik a vizualizačních nástrojů, jako je Google Analytics. Proč? Zde jsou mé předpoklady:
- Během velké části historie vývoje desktopových aplikací neměli žádný stabilní a trvalý přístup k webu.
- Existuje mnoho vývojových nástrojů pro desktopové aplikace. Proto je nemožné vytvořit víceúčelový nástroj pro sběr uživatelských dat pro všechny rámce a technologie uživatelského rozhraní.
Klíčovým aspektem shromažďování dat je sledování výjimek. Shromažďujeme například data o haváriích. Dříve museli naši uživatelé sami napsat na e-mail zákaznické podpory a přidat Stack Trace chyby, která byla zkopírována ze speciálního okna aplikace. Jen málo uživatelů provedlo všechny tyto kroky. Shromážděná data jsou zcela anonymizována, což nás připravuje o možnost zjistit od uživatele kroky reprodukce nebo jakékoli další informace.
Na druhou stranu chybová data jsou v databázi Postgres a to otevírá cestu k okamžité kontrole desítek hypotéz. Odpovědi můžete okamžitě získat jednoduchým zadáním SQL dotazů do databáze. Z jednoho zásobníku nebo typu výjimky často není jasné, jak k výjimce došlo, proto jsou všechny tyto informace důležité pro prostudování problému.
Kromě toho máte možnost analyzovat všechna shromážděná data a najít nejproblematičtější moduly a třídy. Na základě výsledků analýzy můžete naplánovat refaktoring nebo dodatečné testy, které pokrývají tyto části programu.
Služba dekódování zásobníku
Sestavení .NET obsahují kód IL, který lze pomocí několika speciálních programů snadno převést zpět do kódu C#, přesně pro operátora. Jedním ze způsobů ochrany programového kódu je jeho zatemnění. Programy lze přejmenovat; metody, proměnné a třídy lze nahradit; kód lze nahradit jeho ekvivalentem, ale je opravdu nesrozumitelný.
Nutnost zatemnit zdrojový kód se objeví, když distribuujete svůj produkt způsobem, který naznačuje, že uživatel dostane sestavení vaší aplikace. Desktopové aplikace jsou takové případy. Všechna sestavení, včetně přechodných sestavení pro testery, jsou pečlivě zamlžena.
Naše jednotka pro zajištění kvality používá nástroje pro dekódování zásobníku od vývojáře obfuscator. Aby mohli začít dekódovat, musí spustit aplikaci, najít deobfuskační mapy publikované CI pro konkrétní sestavení a vložit zásobník výjimek do vstupního pole.
Různé verze a editory byly zatemněny odlišným způsobem, což ztěžovalo vývojáři problém studovat nebo ho dokonce mohlo uvést na špatnou cestu. Bylo zřejmé, že tento proces musí být automatizován.
Formát deobfuskační mapy se ukázal být docela přímočarý. Snadno jsme to rozložili a napsali program pro dekódování zásobníku. Krátce předtím bylo vyvinuto webové uživatelské rozhraní, které vykresluje výjimky podle verzí produktu a seskupuje je podle zásobníku. Byl to web .NET Core s databází v SQLite.
SQLite je úhledný nástroj pro malá řešení. Zkusili jsme tam dát i deobfuskační mapy. Každá sestava vygenerovala přibližně 500 tisíc párů šifrování a dešifrování. SQLite nemohl zvládnout tak agresivní rychlost vkládání.
Zatímco data o jednom sestavení byla vložena do databáze, do fronty byly přidány další dvě. Nedlouho před tímto problémem jsem poslouchal zprávu o Clickhouse a chtěl jsem to vyzkoušet. Ukázalo se, že je vynikající, rychlost vkládání se zrychlila více než 200krát.
To znamená, že dekódování zásobníku (čtení z databáze) se zpomalilo téměř 50krát, ale protože každý zásobník zabral méně než 1 ms, bylo nákladově neefektivní věnovat čas studiu tohoto problému.
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.
Závěr
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.