Database
 sql >> Baza danych >  >> RDS >> Database

Uzupełnianie SQL. Historie sukcesu i porażki

Pracuję dla firmy, która opracowuje IDE do interakcji z bazami danych od ponad pięciu lat. Zanim zacząłem pisać ten artykuł, nie miałem pojęcia, ile bajek będzie czekało.

Mój zespół opracowuje i obsługuje funkcje języka IDE, a głównym z nich jest automatyczne uzupełnianie kodu. Wydarzyło się wiele ekscytujących rzeczy. Niektóre rzeczy spisały się świetnie od pierwszej próby, inne zawiodły nawet po kilku strzałach.

Przetwarzanie SQL i dialektów

SQL jest próbą upodobnienia się do języka naturalnego, a próba jest całkiem udana, powinienem powiedzieć. W zależności od dialektu istnieje kilka tysięcy słów kluczowych. Aby odróżnić jedno stwierdzenie od drugiego, często trzeba szukać jednego lub dwóch słów (tokenów) do przodu. Takie podejście nazywa się spojrzeniem w przyszłość .

Istnieje klasyfikacja parsera w zależności od tego, jak daleko mogą patrzeć w przyszłość:LA(1), LA(2) lub LA(*), co oznacza, że ​​parser może patrzeć tak daleko w przód, jak to konieczne, aby zdefiniować właściwy widelec.

Czasami opcjonalne zakończenie klauzuli pasuje do początku innej opcjonalnej klauzuli. Takie sytuacje znacznie utrudniają analizę składniową. T-SQL nie ułatwia spraw. Ponadto niektóre instrukcje SQL mogą mieć, choć niekoniecznie, końcówki, które mogą kolidować z początkiem poprzednich instrukcji.

Nie wierzysz w to? Istnieje sposób opisywania języków formalnych za pomocą gramatyki. Możesz wygenerować z niego parser za pomocą tego lub innego narzędzia. Najbardziej godne uwagi narzędzia i języki opisujące gramatykę to YACC i ANTLR.

YACC -generowane parsery są używane w silnikach MySQL, MariaDB i PostgreSQL. Moglibyśmy spróbować pobrać je bezpośrednio z kodu źródłowego i opracować uzupełnianie kodu oraz inne funkcje oparte na analizie SQL z wykorzystaniem tych parserów. Co więcej, ten produkt otrzymywałby bezpłatne aktualizacje programistyczne, a parser zachowywałby się tak samo jak silnik źródłowy.

Dlaczego więc nadal używamy ANTLR? ? Mocno wspiera C#/.NET, ma przyzwoity zestaw narzędzi, jego składnia jest znacznie łatwiejsza do czytania i pisania. Składnia ANTLR stała się tak przydatna, że ​​Microsoft używa jej teraz w swojej oficjalnej dokumentacji C#.

Wróćmy jednak do złożoności SQL, jeśli chodzi o parsowanie. Chciałbym porównać rozmiary gramatyczne publicznie dostępnych języków. W dbForge używamy naszych fragmentów gramatyki. Są bardziej kompletne niż inne. Niestety są one przeładowane wstawkami kodu C# do obsługi różnych funkcji.

Rozmiary gramatyczne dla różnych języków są następujące :

JS – 475 wierszy parsera + 273 lekserów =748 wierszy

Java – 615 wierszy parsera + 211 lekserów =826 wierszy

C# – 1159 wierszy parsera + 433 lekserów =1592 wierszy

С++ – 1933 rzędy

MySQL – 2515 wierszy parsera + 1189 lekserów =3704 wierszy

T-SQL – 4035 wierszy parsera + 896 lekserów =4931 wierszy

PL SQL – 6719 wierszy parsera + 2366 lekserów =9085 wierszy

Na końcówkach niektórych lekserów znajduje się lista znaków Unicode dostępnych w języku. Listy te są bezużyteczne w ocenie złożoności języka. Tak więc liczba wierszy, które wziąłem, zawsze kończyła się przed tymi listami.

Ocena złożoności parsowania języka na podstawie liczby wierszy w gramatyce języka jest dyskusyjna. Mimo to uważam, że ważne jest, aby pokazać liczby, które pokazują ogromną rozbieżność.

To nie wszystko. Ponieważ rozwijamy IDE, powinniśmy radzić sobie z niekompletnymi lub nieprawidłowymi skryptami. Musieliśmy wymyślić wiele sztuczek, ale klienci wciąż wysyłają wiele działających scenariuszy z niedokończonymi skryptami. Musimy to rozwiązać.

wojny predykatów

Podczas parsowania kodu słowo czasami nie mówi, którą z dwóch alternatyw wybrać. Mechanizmem rozwiązującym tego typu nieścisłości jest lookahead w ANTLR. Metoda parsera to wstawiony łańcuch if , a każdy z nich wygląda o krok do przodu. Zobacz przykład gramatyki generującej niepewność tego rodzaju:

rule1:
  'a' rule2 | rule3
;

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

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

W środku reguły1, gdy token „a” jest już przekazany, parser będzie czekał dwa kroki do przodu, aby wybrać regułę, której należy przestrzegać. To sprawdzenie zostanie wykonane jeszcze raz, ale gramatykę można przepisać, aby wykluczyć przewidywanie . Minusem jest to, że takie optymalizacje szkodzą konstrukcji, podczas gdy wzrost wydajności jest raczej niewielki.

Istnieją bardziej złożone sposoby rozwiązania tego rodzaju niepewności. Na przykład predykat składni (SynPred) mechanizm w ANTLR3 . Pomaga, gdy opcjonalne zakończenie klauzuli przecina początek następnej opcjonalnej klauzuli.

Jeśli chodzi o ANTLR3, predykat jest generowaną metodą, która wykonuje wirtualny wpis tekstowy zgodnie z jedną z alternatyw . Gdy się powiedzie, zwraca prawdę wartość, a zakończenie predykatu powiodło się. Kiedy jest to wejście wirtualne, nazywa się to cofaniem wejście w tryb. Jeśli predykat zadziała pomyślnie, nastąpi prawdziwy wpis.

Problem pojawia się tylko wtedy, gdy predykat zaczyna się w innym predykacie. Wtedy jedna odległość może zostać przekroczona setki lub tysiące razy.

Przyjrzyjmy się uproszczonemu przykładowi. Istnieją trzy punkty niepewności:(A, B, C).

  1. Parser wpisuje A, zapamiętuje swoją pozycję w tekście, rozpoczyna wirtualny wpis poziomu 1.
  2. Parser wpisuje B, zapamiętuje swoją pozycję w tekście, rozpoczyna wirtualny wpis poziomu 2.
  3. Parser wpisuje C, zapamiętuje swoją pozycję w tekście, rozpoczyna wirtualny wpis poziomu 3.
  4. Parser kończy wirtualny wpis poziomu 3, powraca do poziomu 2 i ponownie przechodzi C.
  5. Parser kończy wirtualny wpis poziomu 2, powraca do poziomu 1 i ponownie przechodzi B i C.
  6. Parser uzupełnia wirtualny wpis, zwraca i wykonuje rzeczywisty wpis poprzez A, B i C.

W rezultacie wszystkie kontrole w C zostaną wykonane 4 razy, w B – 3 razy, w A – 2 razy.

Ale co, jeśli odpowiednia alternatywa znajduje się na drugim lub trzecim miejscu na liście? Wtedy jeden z etapów predykatu zawiedzie. Jego pozycja w tekście cofnie się i rozpocznie się kolejny predykat.

Analizując przyczyny zawieszania się aplikacji, często natykamy się na ślad SynPred wykonane kilka tysięcy razy. SynPred s są szczególnie problematyczne w regułach rekurencyjnych. Niestety SQL jest z natury rekurencyjny. Możliwość korzystania z podzapytań niemal wszędzie ma swoją cenę. Można jednak manipulować regułą, aby predykat zniknął.

SynPred szkodzi wydajności. W pewnym momencie ich liczba została poddana ścisłej kontroli. Problem polega jednak na tym, że kiedy piszesz kod gramatyczny, SynPred może wydawać się nieoczywisty. Co więcej, zmiana jednej reguły może spowodować, że SynPred pojawi się w innej regule, a to praktycznie uniemożliwi kontrolę nad nimi.

Stworzyliśmy proste wyrażenie regularne narzędzie do kontrolowania liczby predykatów uruchamianych przez specjalne zadanie MSBuild . Jeśli liczba predykatów nie była zgodna z liczbą określoną w pliku, zadanie natychmiast nie powiodło się kompilacja i ostrzegało o błędzie.

Widząc błąd, programista powinien kilkakrotnie przepisać kod reguły, aby usunąć zbędne predykaty. Jeśli nie da się uniknąć predykatów, programista doda go do specjalnego pliku, który przyciągnie dodatkową uwagę do recenzji.

W rzadkich przypadkach pisaliśmy nawet nasze predykaty w C# tylko po to, by uniknąć tych generowanych przez ANTLR. Na szczęście ta metoda również istnieje.

Dziedziczenie gramatyki

Kiedy są jakieś zmiany w obsługiwanych przez nas DBMS, musimy je uwzględnić w naszych narzędziach. Wsparcie dla konstrukcji składni gramatycznej jest zawsze punktem wyjścia.

Dla każdego dialektu SQL tworzymy specjalną gramatykę. Umożliwia to pewne powtórzenie kodu, ale jest to łatwiejsze niż próba znalezienia tego, co mają wspólnego.

Postanowiliśmy napisać nasz własny preprocesor gramatyczny ANTLR, który dziedziczy gramatykę.

Stało się również oczywiste, że potrzebujemy mechanizmu polimorfizmu – umiejętności nie tylko redefiniowania reguły w potomku, ale także wywołania reguły podstawowej. Chcielibyśmy również kontrolować pozycję podczas wywoływania reguły podstawowej.

Narzędzia to zdecydowany plus, gdy porównamy ANTLR z innymi narzędziami do rozpoznawania języków, Visual Studio i ANTLRWorks. I nie chcesz stracić tej przewagi przy wdrażaniu dziedziczenia. Rozwiązaniem było określenie podstawowej gramatyki w gramatyce dziedziczonej w formacie komentarza ANTLR. W przypadku narzędzi ANTLR to tylko komentarz, ale możemy wydobyć z niego wszystkie wymagane informacje.

Napisaliśmy zadanie MsBuild, które zostało osadzone w całym systemie kompilacji jako akcja przed kompilacją. Zadanie polegało na wykonaniu pracy preprocesora dla gramatyki ANTLR poprzez wygenerowanie wynikowej gramatyki na podstawie jej bazowych i dziedziczonych odpowiedników. Otrzymana gramatyka została przetworzona przez sam ANTLR.

Przetwarzanie ANTLR

W wielu językach programowania słowa kluczowe nie mogą być używane jako nazwy tematów. W zależności od dialektu w SQL może być od 800 do 3000 słów kluczowych. Większość z nich jest powiązana z kontekstem w bazach danych. W związku z tym zakazanie ich jako nazw obiektów frustruje użytkowników. Właśnie dlatego SQL zarezerwował i niezarezerwowane słowa kluczowe.

Nie możesz nazwać swojego obiektu słowem zastrzeżonym (SELECT, FROM itp.) bez cytowania, ale możesz to zrobić ze słowem niezastrzeżonym (KONWERSJA, DOSTĘPNOŚĆ itp.). Ta interakcja utrudnia rozwój parsera.

Podczas analizy leksykalnej kontekst jest nieznany, ale parser wymaga już różnych liczb dla identyfikatora i słowa kluczowego. Dlatego dodaliśmy kolejny postprocessing do parsera ANTLR. Zastąpiło to wszystkie oczywiste sprawdzenia identyfikatorów wywołaniem specjalnej metody.

Ta metoda ma bardziej szczegółową kontrolę. Jeśli wpis wywołuje identyfikator, a spodziewamy się, że identyfikator zostanie spełniony dalej, to wszystko jest w porządku. Ale jeśli niezastrzeżone słowo jest wpisem, powinniśmy je dokładnie sprawdzić. Ta dodatkowa kontrola sprawdza wyszukiwanie oddziałów w bieżącym kontekście, w którym to niezarezerwowane słowo kluczowe może być słowem kluczowym. Jeśli nie ma takich gałęzi, może być użyty jako identyfikator.

Technicznie rzecz biorąc, ten problem można rozwiązać za pomocą ANTLR, ale ta decyzja nie jest optymalna. Sposób ANTLR polega na stworzeniu reguły zawierającej wszystkie niezarezerwowane słowa kluczowe oraz identyfikator leksemu. Dalej zamiast identyfikatora leksemu posłuży specjalna reguła. Takie rozwiązanie sprawia, że ​​programista nie zapomina o dodaniu słowa kluczowego tam, gdzie jest używane i w specjalnej regule. Ponadto optymalizuje spędzany czas.

Błędy w analizie składni bez drzew

Drzewo składni jest zwykle wynikiem pracy parsera. Jest to struktura danych, która odzwierciedla tekst programu poprzez gramatykę formalną. Jeśli chcesz zaimplementować edytor kodu z automatycznym uzupełnianiem języka, najprawdopodobniej otrzymasz następujący algorytm:

  1. Przeanalizuj tekst w edytorze. Wtedy otrzymasz drzewo składni.
  2. Znajdź węzeł pod wózkiem i dopasuj go do gramatyki.
  3. Dowiedz się, jakie słowa kluczowe i typy obiektów będą dostępne w Punkcie.

W tym przypadku gramatykę łatwo sobie wyobrazić jako wykres lub maszynę stanową.

Niestety, tylko trzecia wersja ANTLR była dostępna, gdy dbForge IDE rozpoczęło swój rozwój. Jednak nie był tak zwinny i chociaż można było powiedzieć ANTLR, jak zbudować drzewo, użycie nie było płynne.

Co więcej, wiele artykułów na ten temat sugerowało użycie mechanizmu „akcji” do uruchamiania kodu, gdy parser przechodził przez regułę. Ten mechanizm jest bardzo przydatny, ale doprowadził do problemów architektonicznych i sprawił, że obsługa nowych funkcji stała się bardziej złożona.

Chodzi o to, że jeden plik gramatyczny zaczął gromadzić „działania” ze względu na dużą liczbę funkcji, które powinny być raczej dystrybuowane do różnych kompilacji. Udało nam się dystrybuować programy obsługi działań do różnych kompilacji i stworzyć podstępną odmianę wzorca subskrybenta-powiadomienia dla tego środka.

ANTLR3 działa 6 razy szybciej niż ANTLR4 według naszych pomiarów. Ponadto drzewo składni dla dużych skryptów może zajmować zbyt dużo pamięci RAM, co nie było dobrą wiadomością, więc musieliśmy działać w 32-bitowej przestrzeni adresowej Visual Studio i SQL Management Studio.

Przetwarzanie końcowe parsera ANTLR

Podczas pracy z ciągami, jednym z najbardziej krytycznych momentów jest etap analizy leksykalnej, gdzie dzielimy skrypt na osobne słowa.

ANTLR przyjmuje jako wejściową gramatykę określającą język i generuje parser w jednym z dostępnych języków. W pewnym momencie wygenerowany parser rozrósł się do tego stopnia, że ​​baliśmy się go debugować. Jeśli naciśniesz F11 (wejdź) podczas debugowania i przejdziesz do pliku parsera, Visual Studio po prostu ulegnie awarii.

Okazało się, że nie powiodło się z powodu wyjątku OutOfMemory podczas analizy pliku parsera. Ten plik zawierał ponad 200 000 linii kodu.

Ale debugowanie parsera jest istotną częścią procesu pracy i nie można go pominąć. Za pomocą klas częściowych C# przeanalizowaliśmy wygenerowany parser za pomocą wyrażeń regularnych i podzieliliśmy go na kilka plików. Visual Studio doskonale z nim współpracowało.

Analiza leksykalna bez podciągu przed Span API

Głównym zadaniem analizy leksykalnej jest klasyfikacja – określenie granic słów i porównanie ich ze słownikiem. Jeśli słowo zostanie znalezione, lekser zwróci jego indeks. Jeśli nie, słowo jest uważane za identyfikator obiektu. To jest uproszczony opis algorytmu.

Leksing w tle podczas otwierania pliku

Podświetlanie składni opiera się na analizie leksykalnej. Ta operacja zwykle zajmuje znacznie więcej czasu w porównaniu z odczytaniem tekstu z dysku. Jaki jest haczyk? W jednym wątku tekst jest odczytywany z pliku, podczas gdy analiza leksykalna jest przeprowadzana w innym wątku.

Lekser odczytuje tekst wiersz po wierszu. Jeśli zażąda wiersza, który nie istnieje, zatrzyma się i poczeka.

BlockingCollection z BCL działa na podobnej zasadzie, a algorytm obejmuje typowe zastosowanie współbieżnego wzorca Producent-Konsument. Edytor pracujący w głównym wątku żąda danych o pierwszym podświetlonym wierszu, a jeśli jest niedostępny, zatrzymuje się i czeka. W naszym edytorze dwukrotnie użyliśmy wzorca producent-konsument i blokowania-kolekcji:

  1. Czytanie z pliku to Producent, podczas gdy lexer to Konsument.
  2. Lexer jest już producentem, a edytor tekstu jest konsumentem.

Ten zestaw trików pozwala nam znacznie skrócić czas poświęcony na otwieranie dużych plików. Pierwsza strona dokumentu jest wyświetlana bardzo szybko, jednak dokument może się zawiesić, jeśli użytkownicy spróbują przejść na koniec pliku w ciągu pierwszych kilku sekund. Dzieje się tak, ponieważ czytający w tle i lekser muszą dotrzeć do końca dokumentu. Jeśli jednak użytkownik powoli przesuwa się od początku dokumentu do końca, nie będzie żadnych zauważalnych zawieszeń.

Niejednoznaczna optymalizacja:częściowa analiza leksykalna

Analiza składniowa jest zwykle podzielona na dwa poziomy:

  • strumień znaków wejściowych jest przetwarzany w celu uzyskania leksemów (tokenów) na podstawie reguł języka – nazywa się to analizą leksykalną
  • parser zużywa strumień tokenów, sprawdzając go zgodnie z formalnymi regułami gramatyki i często buduje drzewo składni.

Przetwarzanie ciągów to kosztowna operacja. Aby go zoptymalizować, postanowiliśmy nie przeprowadzać za każdym razem pełnej analizy leksykalnej tekstu, a jedynie ponownie analizować tylko tę część, która została zmieniona. Ale jak radzić sobie z konstrukcjami wielowierszowymi, takimi jak komentarze blokowe lub wiersze? Zapisaliśmy stan końca linii dla każdej linii:„brak tokenów wielowierszowych” =0, „początek komentarza blokowego” =1, „początek literału ciągu wielowierszowego” =2. Analiza leksykalna zaczyna się od zmienionej sekcji i kończy się, gdy stan końca linii jest równy zapisanemu.

Z tym rozwiązaniem był jeden problem:bardzo niewygodne jest monitorowanie numerów linii w takich strukturach, podczas gdy numer linii jest wymaganym atrybutem tokena ANTLR, ponieważ po wstawieniu lub usunięciu linii należy odpowiednio zaktualizować numer następnej linii. Rozwiązaliśmy go, ustawiając numer linii natychmiast, przed przekazaniem tokena parserowi. Testy, które przeprowadziliśmy później, wykazały, że wydajność poprawiła się o 15-25%. Rzeczywista poprawa była jeszcze większa.

Ilość pamięci RAM wymagana do tego wszystkiego okazała się znacznie większa niż się spodziewaliśmy. Token ANTLR składał się z:punktu początkowego – 8 bajtów, punktu końcowego – 8 bajtów, linku do tekstu słowa – 4 lub 8 bajtów (nie wspominając o samym ciągu), linku do tekstu dokumentu – 4 lub 8 bajtów, oraz typ tokena – 4 bajty.

Więc co możemy wnioskować? Skupiliśmy się na wydajności i uzyskaliśmy nadmierne zużycie pamięci RAM w miejscu, którego się nie spodziewaliśmy. Nie zakładaliśmy, że tak się stanie, ponieważ próbowaliśmy użyć lekkich struktur zamiast klas. Zastępując je ciężkimi obiektami, świadomie poszliśmy na dodatkowe wydatki na pamięć, aby uzyskać lepszą wydajność. Na szczęście nauczyło nas to ważnej lekcji, więc teraz każda optymalizacja wydajności kończy się profilowaniem zużycia pamięci i odwrotnie.

To jest historia z morałem. Niektóre funkcje zaczęły działać niemal natychmiast, a inne nieco szybciej. W końcu niemożliwe byłoby wykonanie sztuczki z analizą leksykalną tła, gdyby nie istniał obiekt, w którym jeden z wątków mógłby przechowywać tokeny.

Wszystkie dalsze problemy rozwijają się w kontekście tworzenia pulpitu na stosie .NET.

Problem 32-bitowy

Niektórzy użytkownicy decydują się na korzystanie z samodzielnych wersji naszych produktów. Inni trzymają się pracy w Visual Studio i SQL Server Management Studio. Dla nich opracowano wiele rozszerzeń. Jednym z tych rozszerzeń jest SQL Complete. Aby wyjaśnić, zapewnia więcej możliwości i funkcji niż standardowe SSMS do uzupełniania kodu i VS dla SQL.

Parsowanie SQL to bardzo kosztowny proces, zarówno pod względem zasobów procesora, jak i pamięci RAM. Aby podpowiedzieć listę obiektów w skryptach użytkownika, bez zbędnych wywołań do serwera, przechowujemy cache obiektów w pamięci RAM. Często nie zajmuje dużo miejsca, ale niektórzy z naszych użytkowników mają bazy danych zawierające do ćwierć miliona obiektów.

Praca z SQL różni się od pracy z innymi językami. W C# praktycznie nie ma plików nawet z tysiącem linii kodu. Tymczasem w SQL programista może pracować ze zrzutem bazy danych składającym się z kilku milionów linijek kodu. Nie ma w tym nic niezwykłego.

DLL-Hell w VS

Istnieje przydatne narzędzie do tworzenia wtyczek w .NET Framework, jest to domena aplikacji. Wszystko odbywa się w odosobniony sposób. Istnieje możliwość rozładunku. W większości przypadków implementacja rozszerzeń jest prawdopodobnie głównym powodem wprowadzenia domen aplikacji.

Istnieje również MAF Framework, który został zaprojektowany przez MS w celu rozwiązania problemu tworzenia dodatków do programu. Izoluje te dodatki do tego stopnia, że ​​może wysłać je do osobnego procesu i przejąć całą komunikację. Szczerze mówiąc to rozwiązanie jest zbyt kłopotliwe i nie zyskało dużej popularności.

Niestety, oparte na nim Microsoft Visual Studio i SQL Server Management Studio implementują system rozszerzeń w inny sposób. Ułatwia dostęp do aplikacji hostingowych dla wtyczek, ale zmusza je do dopasowania się do jednego procesu i domeny z innym.

Jak każda inna aplikacja w XXI wieku, nasza ma wiele zależności. Większość z nich to dobrze znane, sprawdzone i popularne biblioteki w świecie .NET.

Wyciąganie wiadomości do zamka

Nie jest powszechnie wiadomo, że .NET Framework będzie pompować Windows Message Queue w każdym WaitHandle. Aby umieścić go wewnątrz każdej blokady, można wywołać dowolną procedurę obsługi dowolnego zdarzenia w aplikacji, jeśli ta blokada ma czas na przełączenie w tryb jądra i nie zostanie zwolniona podczas fazy wirowania i oczekiwania.

Może to spowodować ponowne wejście w bardzo nieoczekiwane miejsca. Kilka razy doprowadziło to do problemów, takich jak „Kolekcja została zmodyfikowana podczas wyliczania” i różnych ArgumentOutOfRangeException.

Dodawanie zestawu do rozwiązania za pomocą SQL

Gdy projekt się rozrasta, zadanie dodawania zestawów, z początku proste, rozwija się w kilkanaście skomplikowanych kroków. Kiedyś musieliśmy dodać do rozwiązania kilkanaście różnych złożeń, przeprowadziliśmy duży refaktoryzacja. Prawie 80 rozwiązań, w tym produktowych i testowych, zostało stworzonych w oparciu o około 300 projektów .NET.

W oparciu o rozwiązania produktowe napisaliśmy pliki Inno Setup. Zawierały listy zestawów spakowanych w instalacji, którą pobrał użytkownik. Algorytm dodawania projektu wyglądał następująco:

  1. Utwórz nowy projekt.
  2. Dodaj do niego certyfikat. Ustaw tag kompilacji.
  3. Dodaj plik wersji.
  4. Zmień konfigurację ścieżek, do których zmierza projekt.
  5. Zmień nazwę folderu, aby odpowiadała specyfikacji wewnętrznej.
  6. Dodaj projekt do rozwiązania jeszcze raz.
  7. Dodaj kilka zespołów, do których wszystkie projekty potrzebują linków.
  8. Dodaj kompilację do wszystkich niezbędnych rozwiązań:test i produkt.
  9. W przypadku wszystkich rozwiązań produktowych dodaj zespoły do ​​instalacji.

Te 9 kroków trzeba było powtórzyć około 10 razy. Kroki 8 i 9 nie są aż tak trywialne i łatwo zapomnieć o dodawaniu kompilacji wszędzie.

W obliczu tak dużego i rutynowego zadania każdy normalny programista chciałby je zautomatyzować. Właśnie to chcieliśmy zrobić. Ale jak wskazać, jakie konkretnie rozwiązania i instalacje dodać do nowo powstałego projektu? Scenariuszy jest tak wiele, a co więcej, niektóre z nich trudno przewidzieć.

Wpadliśmy na szalony pomysł. Rozwiązania są powiązane z projektami typu wiele-do-wielu, projektami z instalacjami w ten sam sposób, a SQL może rozwiązać dokładnie takie zadania, jakie mieliśmy.

Stworzyliśmy aplikację .Net Core Console, która skanuje wszystkie pliki .sln w folderze źródłowym, pobiera z nich listę projektów za pomocą DotNet CLI i umieszcza ją w bazie danych SQLite. Program posiada kilka trybów:

  • Nowy – tworzy projekt i wszystkie niezbędne foldery, dodaje certyfikat, ustawia tag, dodaje wersję, minimum niezbędnych zestawów.
  • Dodaj-Projekt – dodaje projekt do wszystkich rozwiązań spełniających zapytanie SQL, które zostanie podane jako jeden z parametrów. Aby dodać projekt do rozwiązania, program w środku używa DotNet CLI.
  • Add-ISS – dodaje projekt do wszystkich instalacji, które spełniają zapytania SQL.

Chociaż pomysł wskazania listy rozwiązań za pomocą zapytania SQL może wydawać się kłopotliwy, całkowicie zamknął wszystkie istniejące przypadki i najprawdopodobniej wszelkie możliwe przypadki w przyszłości.

Pozwolę sobie zademonstrować scenariusz. Utwórz projekt „A” i dodaj go do wszystkich rozwiązań, w których projekty „B” jest używany:

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

Problem z LiteDB

Kilka lat temu otrzymaliśmy zadanie opracowania funkcji w tle do zapisywania dokumentów użytkownika. Miał dwa główne przepływy aplikacji:możliwość natychmiastowego zamknięcia IDE i opuszczenia oraz po powrocie do miejsca, w którym zostało przerwane, oraz możliwość przywrócenia w nagłych sytuacjach, takich jak przerwy w dostawie prądu lub awarie programu.

Aby zrealizować to zadanie, należało zapisać zawartość plików gdzieś na boku i robić to często i szybko. Oprócz zawartości konieczne było zapisanie pewnych metadanych, co utrudniało bezpośrednie przechowywanie w systemie plików.

W tym momencie znaleźliśmy bibliotekę LiteDB, która zrobiła na nas wrażenie swoją prostotą i wydajnością. LiteDB to szybka, lekka, wbudowana baza danych, która została w całości napisana w C#. Szybkość i ogólna prostota przekonały nas.

W trakcie procesu rozwoju cały zespół był zadowolony ze współpracy z LiteDB. Jednak główne problemy zaczęły się po wydaniu.

Oficjalna dokumentacja gwarantowała, że ​​baza danych zapewnia poprawną pracę przy równoczesnym dostępie z wielu wątków oraz kilku procesów. Agresywne testy syntetyczne wykazały, że baza danych nie działa poprawnie w środowisku wielowątkowym.

Aby szybko rozwiązać problem, zsynchronizowaliśmy procesy za pomocą samodzielnie napisanego interprocesu ReadWriteLock. Teraz, po prawie trzech latach, LiteDB działa znacznie lepiej.

StringStringList

Ten problem jest odwrotny niż w przypadku częściowej analizy leksykalnej. Kiedy pracujemy z tekstem, wygodniej jest pracować z nim jako z listą ciągów. Można żądać łańcuchów w kolejności losowej, ale pewna gęstość dostępu do pamięci jest nadal obecna. W pewnym momencie konieczne było uruchomienie kilku zadań, aby przetworzyć bardzo duże pliki bez pełnego obciążenia pamięci. Pomysł był następujący:

  1. Aby czytać plik wiersz po wierszu. Zapamiętaj przesunięcia w pliku.
  2. Na żądanie, wydaj następną linię, ustaw wymagane przesunięcie i zwróć dane.

Zadanie główne zakończone. Ta struktura nie zajmuje dużo miejsca w porównaniu do rozmiaru pliku. Na etapie testów dokładnie sprawdzamy zużycie pamięci pod kątem dużych i bardzo dużych plików. Duże pliki były przetwarzane przez długi czas, a małe będą przetwarzane natychmiast.

Nie było odniesienia do sprawdzenia czasu wykonania . Pamięć RAM nazywana jest pamięcią o dostępie swobodnym – jest to jego przewaga konkurencyjna nad SSD, a zwłaszcza nad HDD. Te sterowniki zaczynają źle działać w przypadku dostępu losowego. Okazało się, że takie podejście spowolniło pracę prawie 40-krotnie w porównaniu do pełnego załadowania pliku do pamięci. Poza tym czytamy plik 2,5 -10 pełnych razy w zależności od kontekstu.

Rozwiązanie było proste, a poprawa wystarczyła, aby operacja trwała tylko trochę dłużej niż w przypadku pełnego załadowania pliku do pamięci.

Podobnie zużycie pamięci RAM również było nieznaczne. Znaleźliśmy inspirację w zasadzie ładowania danych z pamięci RAM do procesora pamięci podręcznej. Kiedy uzyskujesz dostęp do elementu tablicy, procesor kopiuje dziesiątki sąsiednich elementów do swojej pamięci podręcznej, ponieważ niezbędne elementy często znajdują się w pobliżu.

Wiele struktur danych wykorzystuje tę optymalizację procesora, aby uzyskać najwyższą wydajność. To z powodu tej osobliwości losowy dostęp do elementów tablicy jest znacznie wolniejszy niż dostęp sekwencyjny. Zaimplementowaliśmy podobny mechanizm:odczytaliśmy zbiór tysiąca napisów i zapamiętaliśmy ich przesunięcia. Kiedy uzyskujemy dostęp do 1001. ciągu, porzucamy pierwsze 500 ciągów i ładujemy następne 500. W przypadku, gdy potrzebujemy któregokolwiek z pierwszych 500 wierszy, przechodzimy do niego osobno, ponieważ mamy już offset.

Programista niekoniecznie musi dokładnie formułować i sprawdzać wymagania niefunkcjonalne. W rezultacie pamiętaliśmy na przyszłość, że musimy pracować sekwencyjnie z pamięcią trwałą.

Analiza wyjątków

Możesz łatwo zbierać dane dotyczące aktywności użytkowników w sieci. Nie dotyczy to jednak analizy aplikacji desktopowych. Nie ma takiego narzędzia, które byłoby w stanie dać niesamowity zestaw metryk i narzędzi wizualizacyjnych, takich jak Google Analytics. Czemu? Oto moje założenia:

  1. Przez większą część historii tworzenia aplikacji komputerowych nie mieli stabilnego i stałego dostępu do sieci.
  2. Istnieje wiele narzędzi programistycznych do aplikacji komputerowych. W związku z tym niemożliwe jest zbudowanie wielofunkcyjnego narzędzia do zbierania danych użytkownika dla wszystkich struktur i technologii interfejsu użytkownika.

Kluczowym aspektem zbierania danych jest śledzenie wyjątków. Na przykład zbieramy dane o awariach. Wcześniej nasi użytkownicy musieli sami napisać do obsługi klienta e-mail, dodając ślad błędu, który został skopiowany ze specjalnego okna aplikacji. Niewielu użytkowników wykonało wszystkie te kroki. Zebrane dane są całkowicie anonimizowane, co pozbawia nas możliwości poznania kroków reprodukcji lub jakichkolwiek innych informacji od użytkownika.

Z drugiej strony dane o błędach znajdują się w bazie danych Postgres, co toruje drogę do natychmiastowego sprawdzenia dziesiątek hipotez. Możesz natychmiast uzyskać odpowiedzi, po prostu wykonując zapytania SQL do bazy danych. Często na podstawie tylko jednego stosu lub typu wyjątku nie jest jasne, w jaki sposób wystąpił wyjątek, dlatego wszystkie te informacje mają kluczowe znaczenie dla zbadania problemu.

Oprócz tego masz możliwość przeanalizowania wszystkich zebranych danych i znalezienia najbardziej problematycznych modułów i klas. Opierając się na wynikach analizy, możesz zaplanować refaktoryzację lub dodatkowe testy obejmujące te części programu.

Usługa dekodowania stosu

Kompilacje .NET zawierają kod IL, który można łatwo przekonwertować z powrotem na kod C#, dokładny dla operatora, za pomocą kilku specjalnych programów. Jednym ze sposobów ochrony kodu programu jest jego zaciemnianie. Programy można zmieniać; metody, zmienne i klasy można zastąpić; kod można zastąpić jego odpowiednikiem, ale jest to naprawdę niezrozumiałe.

Konieczność zaciemniania kodu źródłowego pojawia się, gdy dystrybuujesz swój produkt w sposób sugerujący, że użytkownik otrzyma kompilacje Twojej aplikacji. Takimi przypadkami są aplikacje na komputery stacjonarne. Wszystkie kompilacje, w tym kompilacje pośrednie dla testerów, są starannie zaciemniane.

Nasza jednostka zapewniania jakości korzysta z narzędzi dekodowania stosu od dewelopera obfuscator. Aby rozpocząć dekodowanie, muszą uruchomić aplikację, znaleźć mapy odszyfrowania opublikowane przez CI dla konkretnej kompilacji i wstawić stos wyjątków do pola wejściowego.

Różne wersje i edytory były zaciemniane w inny sposób, co utrudniało programiście zbadanie problemu, a nawet mogło naprowadzić go na złą ścieżkę. Było oczywiste, że ten proces musiał zostać zautomatyzowany.

Format mapy dezaciemniającej okazał się całkiem prosty. Z łatwością przeanalizowaliśmy go i napisaliśmy program do dekodowania stosu. Krótko przed tym opracowano internetowy interfejs użytkownika do renderowania wyjątków według wersji produktów i grupowania ich według stosu. Była to witryna .NET Core z bazą danych w SQLite.

SQLite to zgrabne narzędzie do małych rozwiązań. Tam też staraliśmy się umieścić mapy dezaciemniające. Każda kompilacja generowała około 500 tysięcy par szyfrowania i deszyfrowania. SQLite nie poradził sobie z tak agresywną szybkością wstawiania.

Podczas gdy dane z jednej kompilacji zostały wstawione do bazy danych, dwie kolejne zostały dodane do kolejki. Niedługo przed tym problemem słuchałem raportu na temat Clickhouse i chciałem go wypróbować. Okazało się to doskonałe, szybkość wstawiania wzrosła ponad 200 razy.

To powiedziawszy, dekodowanie stosu (odczyt z bazy danych) zwolniło prawie 50 razy, ale ponieważ każdy stos trwał mniej niż 1 ms, poświęcanie czasu na badanie tego problemu było nieopłacalne.

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.

Wniosek

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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Śledzenie zmian w bazie danych za pomocą kontroli źródła folderu roboczego

  2. Poznaj podstawy rejestrowania w Javie

  3. Co to jest baza danych? Definicja, typy i komponenty

  4. Poziomy zgodności i podstawa szacowania kardynalności

  5. ODBC 4.0