Работя за компания, която разработва IDE за взаимодействие с база данни повече от пет години. Преди да започна да пиша тази статия, нямах представа колко фантастични приказки предстоят.
Екипът ми разработва и поддържа езикови функции на IDE, а автоматичното довършване на кода е основното. Случих се много вълнуващи неща. Някои неща направихме страхотно от първия опит, а други се провалиха дори след няколко удара.
Разбор на SQL и диалекти
SQL е опит да изглежда като естествен език и опитът е доста успешен, трябва да кажа. В зависимост от диалекта има няколко хиляди ключови думи. За да различите едно твърдение от друго, често трябва да търсите една или две думи (токени) напред. Този подход се нарича поглед напред .
Има класификация на синтактичния анализатор в зависимост от това колко далеч може да гледа напред:LA(1), LA(2) или LA(*), което означава, че анализаторът може да гледа толкова напред, колкото е необходимо, за да дефинира десния разклон.
Понякога краят на незадължителна клауза съвпада с началото на друга незадължителна клауза. Тези ситуации правят анализирането много по-трудно за изпълнение. T-SQL не улеснява нещата. Освен това някои SQL изрази може да имат, но не непременно, окончания, които могат да бъдат в конфликт с началото на предишни изрази.
Не вярвате ли? Има начин за описание на формалните езици чрез граматика. Можете да генерирате анализатор от него, като използвате този или онзи инструмент. Най-забележителните инструменти и езици, които описват граматиката, са YACC и ANTLR.
YACC -генерираните парсери се използват в MySQL, MariaDB и PostgreSQL двигатели. Бихме могли да опитаме да ги вземем направо от изходния код и да разработим завършване на кода и други функции, базирани на SQL анализа, използвайки тези парсери. Освен това този продукт ще получи безплатни актуализации за разработка и анализаторът ще се държи по същия начин, както го прави изходният двигател.
Така че защо все още използваме ANTLR ? Той стабилно поддържа C#/.NET, има приличен инструментариум, синтаксисът му е много по-лесен за четене и писане. Синтаксисът на ANTLR стана толкова удобен, че Microsoft вече го използва в своята официална C# документация.
Но нека се върнем към сложността на SQL, когато става въпрос за синтактичен анализ. Бих искал да сравня граматическите размери на публично достъпните езици. В dbForge ние използваме нашите части от граматиката. Те са по-пълни от останалите. За съжаление, те са претоварени с вмъкванията на C# код за поддръжка на различни функции.
Граматичните размери за различните езици са както следва:
JS – 475 реда анализатор + 273 лексера =748 реда
Java – 615 реда анализатор + 211 lexers =826 реда
C# – 1159 реда синтактичен анализатор + 433 лексера =1592 реда
С++ – 1933 реда
MySQL – 2515 реда за анализатор + 1189 lexers =3704 реда
T-SQL – 4035 реда анализатор + 896 lexers =4931 реда
PL SQL – 6719 реда за синтактичен анализ + 2366 lexers =9085 реда
Завършванията на някои лексери съдържат списъците на символите на Unicode, налични в езика. Тези списъци са безполезни по отношение на оценката на езиковата сложност. По този начин броят на редовете, които вземах, винаги свършваше преди тези списъци.
Оценяването на сложността на анализа на езика въз основа на броя на редовете в езиковата граматика е спорно. Все пак смятам, че е важно да се показват числата, които показват огромно несъответствие.
Това не е всичко. Тъй като разработваме IDE, трябва да работим с непълни или невалидни скриптове. Трябваше да измислим много трикове, но клиентите все още изпращат много работещи сценарии с недовършени скриптове. Трябва да разрешим това.
Войни на предикатите
По време на анализа на кода думата понякога не ви казва коя от двете алтернативи да изберете. Механизмът, който разрешава този тип неточности, е предвид в ANTLR. Методът на анализатора е вмъкнатата верига от if , и всеки от тях гледа една крачка напред. Вижте примера за граматиката, която генерира несигурност от този вид:
rule1:
'a' rule2 | rule3
;
rule2:
'b' 'c' 'd'
;
rule3:
'b' 'c' 'e'
;
В средата на правилото1, когато токенът „a“ вече е предаден, анализаторът ще погледне две стъпки напред, за да избере правилото, което да следва. Тази проверка ще бъде извършена отново, но тази граматика може да бъде пренаписана, за да се изключи напред . Недостатъкът е, че подобни оптимизации вредят на структурата, докато повишаването на производителността е доста малко.
Има по-сложни начини за решаване на този вид несигурност. Например, Синтактичният предикат (SynPred) механизъм вANTLR3 . Помага, когато незадължителният край на клауза пресече началото на следващата незадължителна клауза.
По отношение на ANTLR3, предикатът е генериран метод, който изпълнява виртуален текст в съответствие с една от алтернативите . Когато е успешен, той връща true стойност и завършването на предиката е успешно. Когато е виртуален запис, той се нарича връщане назад влизане в режим. Ако предикат работи успешно, се случва реалното вписване.
Проблем е само когато предикат започва вътре в друг предикат. Тогава едно разстояние може да бъде прекосено стотици или хиляди пъти.
Нека разгледаме опростен пример. Има три точки на несигурност:(A, B, C).
- Антактният анализатор въвежда A, запомня позицията му в текста, стартира виртуален запис от ниво 1.
- Анализаторът влиза в B, запомня позицията му в текста, стартира виртуален запис от ниво 2.
- Анализаторът влиза в C, запомня позицията му в текста, стартира виртуален запис от ниво 3.
- Анализаторът завършва виртуален запис от ниво 3, връща се към ниво 2 и отново предава C.
- Анализаторът завършва виртуален запис от ниво 2, връща се към ниво 1 и преминава отново B и C.
- Анализаторът завършва виртуален запис, връща и изпълнява реален запис през A, B и C.
В резултат на това всички проверки в рамките на C ще бъдат направени 4 пъти, в рамките на B – 3 пъти, в рамките на A – 2 пъти.
Но какво ще стане, ако подходяща алтернатива е във втората или третата в списъка? Тогава един от предикатните етапи ще се провали. Позицията му в текста ще се върне назад и друг предикат ще започне да се изпълнява.
Когато анализираме причините за блокиране на приложението, често се натъкваме на следата от SynPred екзекутиран няколко хиляди пъти. SynPred с са особено проблематични в рекурсивните правила. За съжаление SQL е рекурсивен по своето естество. Възможността за използване на подзаявки почти навсякъде има своята цена. Възможно е обаче да се манипулира правилото, за да се премахне предикатът.
SynPred вреди на производителността. В един момент техният брой беше поставен под строг контрол. Но проблемът е, че когато пишете граматически код, SynPred може да изглежда неочевидно за вас. Нещо повече, промяната на едно правило може да доведе до появата на SynPred в друго правило и това прави контрола върху тях практически невъзможен.
Създадохме прост регулярен израз инструмент за контролиране на броя на предикатите, изпълнявани от специалната MSBuild Task . Ако броят на предикатите не съвпада с номера, посочен във файл, задачата незабавно се проваля при изграждането и предупреждава за грешка.
Когато види грешката, разработчикът трябва да пренапише кода на правилото няколко пъти, за да премахне излишните предикати. Ако човек не може да избегне предикатите, разработчикът ще го добави към специален файл, който привлича допълнително внимание за прегледа.
В редки случаи дори написахме нашите предикати, използвайки C#, само за да избегнем генерираните от ANTLR. За щастие този метод също съществува.
Граматическо наследяване
Когато има някакви промени в поддържаните от нас СУБД, трябва да ги посрещнем в нашите инструменти. Поддръжката на граматически синтактични конструкции винаги е отправна точка.
Създаваме специална граматика за всеки SQL диалект. Позволява известно повторение на кода, но е по-лесно, отколкото да се опитвате да намерите общото между тях.
Отидохме да напишем собствен ANTLR граматически препроцесор, който наследява граматиката.
Също така стана очевидно, че имаме нужда от механизъм за полиморфизъм – способността не само да предефинираме правилото в потомъка, но и да извикаме основното. Бихме искали също да контролираме позицията при извикване на основното правило.
Инструментите са определено предимство, когато сравняваме ANTLR с други инструменти за разпознаване на езици, Visual Studio и ANTLRWorks. И не искате да загубите това предимство, докато прилагате наследството. Решението беше посочване на основна граматика в наследена граматика в коментарен формат ANTLR. За инструментите на ANTLR това е просто коментар, но можем да извлечем цялата необходима информация от него.
Написахме задача MsBuild, която беше вградена в цялата система за изграждане като действие преди изграждане. Задачата беше да върши работата на препроцесор за ANTLR граматика чрез генериране на получената граматика от неговата база и наследени подобни. Получената граматика беше обработена от самия ANTLR.
ANTLR последваща обработка
В много езици за програмиране ключовите думи не могат да се използват като имена на теми. В SQL може да има от 800 до 3000 ключови думи в зависимост от диалекта. Повечето от тях са свързани с контекста в базите данни. По този начин, забраняването им като имена на обекти би разочаровало потребителите. Ето защо SQL има запазени и нерезервирани ключови думи.
Не можете да назовете обекта си като запазена дума (SELECT, FROM и т.н.), без да го цитирате, но можете да направите това с нерезервирана дума (РАЗГОВОР, НАЛИЧНОСТ и т.н.). Това взаимодействие прави развитието на анализатора по-трудно.
По време на лексикалния анализ контекстът е неизвестен, но анализаторът вече изисква различни числа за идентификатора и ключовата дума. Ето защо добавихме още една последваща обработка към ANTLR анализатора. Той замени всички очевидни проверки на идентификатор с извикване на специален метод.
Този метод има по-подробна проверка. Ако записът извиква идентификатор и очакваме идентификаторът да бъде изпълнен нататък, тогава всичко е наред. Но ако една безрезервна дума е запис, трябва да я проверим отново. Тази допълнителна проверка преглежда търсенето в клонове в текущия контекст, където тази нерезервирана ключова дума може да бъде ключова дума. Ако няма такива клонове, може да се използва като идентификатор.
Технически този проблем може да бъде решен с помощта на ANTLR, но това решение не е оптимално. Начинът на ANTLR е да се създаде правило, което изброява всички нерезервирани ключови думи и идентификатор на лексема. По-нататък вместо идентификатор на лексема ще служи специално правило. Това решение кара разработчика да не забравя да добави ключовата дума там, където се използва и в специалното правило. Освен това оптимизира прекараното време.
Грешки в синтактичния анализ без дървета
Синтаксичното дърво обикновено е резултат от работа с анализатор. Това е структура от данни, която отразява програмния текст чрез официална граматика. Ако искате да внедрите редактор на код с автоматично довършване на езика, най-вероятно ще получите следния алгоритъм:
- Разберете текста в редактора. След това получавате синтактично дърво.
- Намерете възел под каретата и го съпоставете с граматиката.
- Разберете кои ключови думи и типове обекти ще бъдат налични в точката.
В този случай граматиката е лесно да си представим като графика или държавна машина.
За съжаление, само третата версия на ANTLR беше налична, когато dbForge IDE беше започнала своята разработка. Това обаче не беше толкова пъргаво и въпреки че бихте могли да кажете на ANTLR как да изгради дърво, използването не беше гладко.
Освен това, много статии по тази тема предлагаха да се използва механизмът „действия“ за изпълнение на код, когато анализаторът преминава през правилото. Този механизъм е много удобен, но доведе до архитектурни проблеми и направи поддръжката на нова функционалност по-сложна.
Работата е там, че един граматичен файл започна да натрупва „действия“ поради големия брой функционалности, които по-скоро трябваше да бъдат разпределени в различни компилации. Успяхме да разпределим манипулаторите на действия в различни компилации и да направим подъл вариант на модела на абонат-известител за тази мярка.
ANTLR3 работи 6 пъти по-бързо от ANTLR4 според нашите измервания. Освен това синтактичното дърво за големи скриптове може да отнеме твърде много RAM, което не беше добра новина, така че трябваше да работим в рамките на 32-битовото адресно пространство на Visual Studio и SQL Management Studio.
Постобработка на анализатора на ANTLR
При работа с низове един от най-критичните моменти е етапът на лексикален анализ, където разделяме скрипта на отделни думи.
ANTLR приема като входна граматика, която определя езика и извежда анализатор на един от наличните езици. В един момент генерираният анализатор нарасна до такава степен, че се страхувахме да го отстраним. Ако натиснете F11 (влезте) при отстраняване на грешки и отидете на файла на анализатора, Visual Studio просто ще се срине.
Оказа се, че се е провалил поради изключение OutOfMemory при анализиране на файла на анализатора. Този файл съдържа повече от 200 000 реда код.
Но отстраняването на грешки в анализатора е съществена част от работния процес и не можете да го пропуснете. С помощта на частични класове на C# анализирахме генерирания синтактичен анализатор с помощта на регулярни изрази и го разделихме на няколко файла. Visual Studio работи перфектно с него.
Лексикален анализ без подниз преди Span API
Основната задача на лексикалния анализ е класификацията – определяне на границите на думите и сравняването им с речник. Ако думата бъде намерена, lexer ще върне нейния индекс. Ако не, думата се счита за идентификатор на обект. Това е опростено описание на алгоритъма.
Лексиране на фона по време на отваряне на файл
Подчертаването на синтаксиса се основава на лексикален анализ. Тази операция обикновено отнема много повече време в сравнение с четенето на текст от диска. Каква е уловката? В една нишка текстът се чете от файла, докато лексикалният анализ се извършва в друга нишка.
Лексерът чете текста ред по ред. Ако поиска ред, който не съществува, той ще спре и ще изчака.
BlockingCollection
- Четенето от файл е производител, докато lexer е потребител.
- Lexer вече е производител, а текстовият редактор е потребител.
Този набор от трикове ни позволява значително да съкратим времето, прекарано за отваряне на големи файлове. Първата страница на документа се показва много бързо, но документът може да замръзне, ако потребителите се опитат да се придвижат до края на файла в рамките на първите няколко секунди. Това се случва, защото фоновият четец и lexer трябва да стигнат до края на документа. Ако обаче работата на потребителя се движи бавно от началото на документа към края, няма да има забележими замръзвания.
Двусмислена оптимизация:частичен лексикален анализ
Синтактичният анализ обикновено се разделя на две нива:
- потокът от входни знаци се обработва, за да се получат лексеми (токени) въз основа на езиковите правила – това се нарича лексикален анализ
- парсерът консумира поток от токени, проверявайки го в съответствие с официалните граматически правила и често изгражда синтактично дърво.
Обработката на низове е скъпа операция. За да го оптимизираме, решихме да не извършваме пълен лексикален анализ на текста всеки път, а да анализираме отново само частта, която е променена. Но как да се справим с многоредови конструкции като блокови коментари или редове? Съхраняваме крайно състояние на ред за всеки ред:“без многоредови токени” =0, “началото на блоков коментар” =1, “началото на многоредов литерал” =2. Лексикалния анализ започва от променената секция и завършва, когато състоянието на края на линията е равно на съхраненото.
Имаше един проблем с това решение:изключително неудобно е да се следят номерата на редове в такива структури, докато номерът на ред е задължителен атрибут на ANTLR маркер, тъй като когато ред се вмъкне или изтрие, номерът на следващия ред трябва да бъде съответно актуализиран. Решихме го, като зададохме номер на ред веднага, преди да предадем токена на анализатора. Тестовете, които направихме по-късно, показаха, че производителността се е подобрила с 15-25%. Действителното подобрение беше още по-голямо.
Количеството RAM, необходимо за всичко това, се оказа много повече, отколкото очаквахме. Токенът ANTLR се състои от:начална точка – 8 байта, крайна точка – 8 байта, връзка към текста на думата – 4 или 8 байта (без да се споменава самия низ), връзка към текста на документа – 4 или 8 байта, и тип токен – 4 байта.
И така, какво можем да заключим? Фокусирахме се върху производителността и получихме прекомерна консумация на RAM на място, което не очаквахме. Не предполагахме, че това ще се случи, защото се опитахме да използваме леки структури вместо класове. Като ги заменяхме с тежки предмети, ние съзнателно потърсихме допълнителни разходи за памет, за да постигнем по-добра производителност. За щастие това ни научи на важен урок, така че сега всяка оптимизация на производителността завършва с профилиране на потреблението на памет и обратно.
Това е история с морал. Някои функции започнаха да работят почти мигновено, а други - малко по-бързо. В крайна сметка би било невъзможно да се изпълни трика за фонов лексикален анализ, ако нямаше обект, където една от нишките може да съхранява токени.
Всички по-нататъшни проблеми се развиват в контекста на разработката на работния плот на стека .NET.
32-битовият проблем
Някои потребители избират да използват самостоятелни версии на нашите продукти. Други продължават да работят във Visual Studio и SQL Server Management Studio. За тях са разработени много разширения. Едно от тези разширения е SQL Complete. За да уточним, той предоставя повече правомощия и функции от стандартните SSMS за попълване на код и VS за SQL.
Разборът на SQL е много скъп процес, както по отношение на CPU, така и на RAM ресурси. За да изведем списък с обекти в потребителски скриптове, без ненужни извиквания към сървъра, ние съхраняваме кеша на обектите в RAM. Често това не заема много място, но някои от нашите потребители имат бази данни, които съдържат до четвърт милион обекта.
Работата със SQL е доста различна от работата с други езици. В C# на практика няма файлове дори с хиляда реда код. Междувременно в SQL разработчикът може да работи с дъмп на база данни, състоящ се от няколко милиона реда код. В това няма нищо необичайно.
DLL-Hell вътре VS
Има удобен инструмент за разработване на плъгини в .NET Framework, това е домейн на приложение. Всичко се извършва по изолиран начин. Възможно е разтоварване. В по-голямата си част внедряването на разширения е може би основната причина за въвеждането на домейни на приложения.
Също така има MAF Framework, който е проектиран от MS за решаване на проблема със създаването на добавки към програмата. Той изолира тези добавки до такава степен, че може да ги изпрати в отделен процес и да поеме всички комуникации. Честно казано, това решение е твърде тромаво и не е спечелило голяма популярност.
За съжаление, Microsoft Visual Studio и SQL Server Management Studio, изградени върху него, прилагат системата за разширения по различен начин. Това опростява достъпа до хостинг приложения за плъгини, но ги принуждава да се вписват заедно в един процес и домейн с друг.
Както всяко друго приложение в 21-ви век, нашето има много зависимости. Повечето от тях са добре познати, доказани във времето и популярни библиотеки в света на .NET.
Изтегляне на съобщения в ключалка
Не е широко известно, че .NET Framework ще изпомпва Windows Message Queue във всеки WaitHandle. За да го поставите във всяко заключване, всеки манипулатор на всяко събитие в приложение може да бъде извикан, ако това заключване има време да превключи в режим на ядрото и не се освобождава по време на фазата на изчакване.
Това може да доведе до повторно влизане на някои много неочаквани места. Няколко пъти това доведе до проблеми като „Колекцията беше променена по време на изброяване“ и различни ArgumentOutOfRangeException.
Добавяне на сбор към решение с помощта на SQL
Когато проектът се разраства, задачата за добавяне на сборки, проста в началото, се развива в дузина сложни стъпки. След като трябваше да добавим дузина различни асембли към решението, направихме голям рефакторинг. Близо 80 решения, включително продуктови и тестови, бяха създадени въз основа на около 300 .NET проекта.
Въз основа на продуктови решения, ние написахме Inno Setup файлове. Те включват списъци със сборки, пакетирани в инсталацията, която потребителят е изтеглил. Алгоритъмът за добавяне на проект беше следният:
- Създайте нов проект.
- Добавете сертификат към него. Настройте маркера на компилацията.
- Добавете файл с версия.
- Преконфигурирайте пътищата, по които върви проектът.
- Преименувайте папката, за да съответства на вътрешната спецификация.
- Добавете проекта към решението още веднъж.
- Добавете няколко сборки, към които всички проекти се нуждаят от връзки.
- Добавете компилацията към всички необходими решения:тест и продукт.
- За всички продуктови решения добавете модулите към инсталацията.
Тези 9 стъпки трябваше да се повторят около 10 пъти. Стъпки 8 и 9 не са толкова тривиални и е лесно да забравите да добавяте компилации навсякъде.
Изправен пред толкова голяма и рутинна задача, всеки нормален програмист би искал да я автоматизира. Точно това искахме да направим. Но как да посочим кои решения и инсталации точно да добавим към новосъздадения проект? Има толкова много сценарии и нещо повече, че е трудно да се предвидят някои от тях.
Хрумна ни луда идея. Решенията са свързани с проекти като много към много, проекти с инсталации по същия начин, а SQL може да реши точно такива задачи, които сме имали.
Създадохме конзолно приложение .Net Core, което сканира всички .sln файлове в изходната папка, извлича списъка с проекти от тях с помощта на DotNet CLI и го поставя в базата данни на SQLite. Програмата има няколко режима:
- Ново – създава проект и всички необходими папки, добавя сертификат, настройва маркер, добавя версия, минимални основни сглобки.
- Add-Project – добавя проекта към всички решения, които отговарят на SQL заявката, която ще бъде дадена като един от параметрите. За да добавите проекта към решението, програмата вътре използва DotNet CLI.
- Add-ISS – добавя проекта към всички инсталации, които отговарят на SQL заявки.
Въпреки че идеята да се посочи списъкът с решения чрез SQL заявката може да изглежда тромава, тя напълно затвори всички съществуващи случаи и най-вероятно всички възможни случаи в бъдеще.
Нека демонстрирам сценария. Създайте проект „A“ и го добавете към всички решения, където проекти “B” се използва:
dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"
Проблем с LiteDB
Преди няколко години получихме задача да разработим фонова функция за запазване на потребителски документи. Имаше два основни потока на приложения:възможност за незабавно затваряне на IDE и напускане и при връщане да започнете оттам, откъдето сте спрели, и възможност за възстановяване в спешни ситуации като прекъсване на работата или сривове на програмата.
За да изпълните тази задача, беше необходимо да запазите съдържанието на файловете някъде отстрани и да го правите често и бързо. Освен съдържанието беше необходимо да се запишат някои метаданни, което направи неудобно директното съхранение във файловата система.
В този момент открихме библиотеката LiteDB, която ни впечатли със своята простота и производителност. LiteDB е бърза лека вградена база данни, която е изцяло написана на C#. Скоростта и цялостната простота ни спечелиха.
В процеса на разработка целият екип остана доволен от работата с LiteDB. Основните проблеми обаче започнаха след пускането.
Официалната документация гарантира, че базата данни осигурява правилна работа с едновременен достъп от множество нишки, както и няколко процеса. Агресивните синтетични тестове показаха, че базата данни не работи правилно в многонишкова среда.
За да отстраним бързо проблема, синхронизирахме процесите с помощта на самостоятелно написания междупроцес ReadWriteLock. Сега, след почти три години, LiteDB работи много по-добре.
StreamStringList
Този проблем е обратен на случая с частичния лексикален анализ. Когато работим с текст, е по-удобно да работим с него като списък с низове. Низовете могат да бъдат заявени в произволен ред, но все още е налице определена плътност на достъп до паметта. В даден момент беше необходимо да се изпълнят няколко задачи за обработка на много големи файлове без пълно натоварване на паметта. Идеята беше следната:
- За да прочетете файла ред по ред. Запомнете отместванията във файла.
- При поискване пуснете следващия ред, задайте необходимо отместване и върнете данните.
Основната задача е изпълнена. Тази структура не заема много място в сравнение с размера на файла. На етапа на тестване ние внимателно проверяваме обема на паметта за големи и много големи файлове. Големите файлове се обработват дълго време, а малките ще бъдат обработени незабавно.
Нямаше препратка за проверка на времета на изпълнение . RAM паметта се нарича памет с произволен достъп – това е нейното конкурентно предимство пред SSD и особено пред HDD. Тези драйвери започват да работят зле за произволен достъп. Оказа се, че този подход забавя работата почти 40 пъти в сравнение с пълното зареждане на файл в паметта. Освен това четем файла 2,5 -10 пълни пъти в зависимост от контекста.
Решението беше просто и подобрението беше достатъчно, така че операцията да отнеме малко повече време, отколкото когато файлът е напълно зареден в паметта.
По същия начин консумацията на RAM също беше незначителна. Намерихме вдъхновение в принципа на зареждане на данни от RAM в кеш процесор. Когато имате достъп до елемент от масив, процесорът копира десетки съседни елементи в своя кеш, тъй като необходимите елементи често са наблизо.
Много структури от данни използват тази оптимизация на процесора, за да постигнат максимална производителност. Именно поради тази особеност произволният достъп до елементите на масива е много по-бавен от последователния достъп. Внедрихме подобен механизъм:прочетохме набор от хиляда низове и запомнихме техните измествания. Когато осъществим достъп до 1001-вия низ, пускаме първите 500 низа и зареждаме следващите 500. В случай че имаме нужда от някой от първите 500 реда, тогава отиваме към него отделно, защото вече имаме отместването.
Програмистът не трябва непременно внимателно да формулира и проверява нефункционални изисквания. В резултат на това запомнихме за бъдещи случаи, че трябва да работим последователно с постоянна памет.
Анализиране на изключенията
Можете лесно да събирате данни за активността на потребителите в мрежата. Не е така обаче при анализирането на настолни приложения. Няма такъв инструмент, който да е в състояние да даде невероятен набор от показатели и инструменти за визуализация като Google Analytics. Защо? Ето моите предположения са:
- През по-голямата част от историята на разработката на настолни приложения те нямаха стабилен и постоянен достъп до мрежата.
- Има много инструменти за разработка на настолни приложения. Следователно е невъзможно да се създаде многофункционален инструмент за събиране на потребителски данни за всички рамки и технологии на потребителския интерфейс.
Ключов аспект на събирането на данни е проследяването на изключенията. Например събираме данни за сривове. Преди това нашите потребители трябваше сами да пишат до имейла за поддръжка на клиенти, добавяйки Stack Trace за грешка, която беше копирана от специален прозорец на приложението. Малко потребители изпълниха всички тези стъпки. Събраните данни са напълно анонимни, което ни лишава от възможността да разберем стъпките за възпроизвеждане или каквато и да е друга информация от потребителя.
От друга страна, данните за грешки са в базата данни на Postgres и това проправя пътя за незабавна проверка на десетки хипотези. Можете незабавно да получите отговорите, като просто направите SQL заявки към базата данни. Често не е ясно само от един стек или тип изключение как е възникнало изключението, ето защо цялата тази информация е от решаващо значение за изследване на проблема.
Освен това имате възможност да анализирате всички събрани данни и да намерите най-проблемните модули и класове. Разчитайки на резултатите от анализа, можете да планирате рефакторинг или допълнителни тестове, които да покрият тези части от програмата.
Услуга за декодиране на стек
.NET компилациите съдържат IL код, който може лесно да се преобразува обратно в C# код, точен за оператора, с помощта на няколко специални програми. Един от начините за защита на програмния код е неговото обфускиране. Програмите могат да бъдат преименувани; методи, променливи и класове могат да бъдат заменени; кодът може да бъде заменен с негов еквивалент, но наистина е неразбираем.
Необходимостта от закриване на изходния код се появява, когато разпространявате продукта си по начин, който предполага, че потребителят получава компилациите на вашето приложение. Настолните приложения са тези случаи. Всички компилации, включително междинни компилации за тестери, са внимателно обфуцирани.
Нашият отдел за осигуряване на качество използва инструменти за декодиране на стека от разработчика на обфускатора. За да започнат да декодират, те трябва да стартират приложението, да намерят карти за деобфускация, публикувани от CI за конкретна компилация, и да вмъкнат стека на изключенията в полето за въвеждане.
Различните версии и редактори бяха замъглени по различен начин, което затрудняваше разработчика да проучи проблема или дори можеше да го постави на грешен път. Беше очевидно, че този процес трябва да бъде автоматизиран.
Форматът на картата за деобфускация се оказа доста ясен. Лесно го анализирахме и написахме програма за декодиране на стека. Малко преди това беше разработен уеб потребителски интерфейс за изобразяване на изключения по версии на продукта и групирането им по стека. Това беше уебсайт на .NET Core с база данни в SQLite.
SQLite е страхотен инструмент за малки решения. Опитахме се да поставим и карти за деобфускация. Всяка компилация генерира приблизително 500 хиляди двойки за криптиране и декриптиране. SQLite не може да се справи с такава агресивна скорост на вмъкване.
Докато данните за една компилация бяха вмъкнати в базата данни, към опашката бяха добавени още две. Не много преди този проблем слушах репортаж за Clickhouse и нямах търпение да го изпробвам. Доказа се отлично, скоростта на вмъкване се ускори с повече от 200 пъти.
Въпреки това декодирането на стека (четене от база данни) се забави близо 50 пъти, но тъй като всеки стек отне по-малко от 1 ms, беше рентабилно да отделите време за изучаване на този проблем.
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.
Заключение
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.