Tôi đã làm việc cho một công ty phát triển IDE để tương tác với cơ sở dữ liệu trong hơn năm năm. Trước khi bắt đầu viết bài này, tôi không biết có bao nhiêu câu chuyện kỳ thú sẽ nằm ở phía trước.
Nhóm của tôi phát triển và hỗ trợ các tính năng ngôn ngữ IDE và tính năng tự động hoàn thành mã là tính năng chính. Tôi đã phải đối mặt với nhiều điều thú vị đang xảy ra. Một số điều chúng tôi đã làm tuyệt vời ngay từ lần thử đầu tiên, và một số điều khác đã thất bại ngay cả sau vài lần bắn.
Phân tích cú pháp SQL và phương ngữ
SQL là một nỗ lực để trông giống như một ngôn ngữ tự nhiên, và tôi nên nói rằng nỗ lực này khá thành công. Tùy thuộc vào phương ngữ, có hàng ngàn từ khóa. Để phân biệt một câu lệnh này với một câu lệnh khác, bạn thường cần tìm một hoặc hai từ (mã thông báo) phía trước. Cách tiếp cận này được gọi là cái nhìn trước mặt .
Có một phân loại trình phân tích cú pháp tùy thuộc vào mức độ họ có thể nhìn về phía trước:LA (1), LA (2) hoặc LA (*), có nghĩa là trình phân tích cú pháp có thể nhìn xa hơn nếu cần để xác định đúng nhánh.
Đôi khi, phần kết thúc của một mệnh đề tùy chọn khớp với phần bắt đầu của một mệnh đề tùy chọn khác. Những tình huống này làm cho phân tích cú pháp khó chạy hơn nhiều. T-SQL không làm cho mọi thứ dễ dàng hơn. Ngoài ra, một số câu lệnh SQL có thể có, nhưng không nhất thiết, phần cuối có thể xung đột với phần đầu của các câu lệnh trước đó.
Bạn không tin à? Có một cách mô tả ngôn ngữ chính thức thông qua Ngữ pháp. Bạn có thể tạo một trình phân tích cú pháp từ nó bằng cách sử dụng công cụ này hoặc công cụ kia. Các công cụ và ngôn ngữ mô tả ngữ pháp đáng chú ý nhất là YACC và ANTLR.
YACC phân tích cú pháp-tạo được sử dụng trong MySQL, MariaDB và PostgreSQL engine. Chúng tôi có thể thử và lấy chúng ngay từ mã nguồn và phát triển tính năng hoàn thành mã và các chức năng khác dựa trên phân tích SQL bằng cách sử dụng các trình phân tích cú pháp này. Hơn nữa, sản phẩm này sẽ nhận được các bản cập nhật phát triển miễn phí và trình phân tích cú pháp sẽ hoạt động giống như cách mà công cụ nguồn thực hiện.
Vậy tại sao chúng tôi vẫn sử dụng ANTLR ? Nó hỗ trợ chắc chắn C # /. NET, có một bộ công cụ tốt, cú pháp của nó dễ đọc và viết hơn nhiều. Cú pháp ANTLR trở nên tiện dụng đến mức Microsoft hiện sử dụng nó trong tài liệu C # chính thức của mình.
Nhưng hãy quay lại độ phức tạp của SQL khi nói đến phân tích cú pháp. Tôi muốn so sánh kích thước ngữ pháp của các ngôn ngữ có sẵn công khai. Trong dbForge, chúng tôi sử dụng các phần ngữ pháp của mình. Họ hoàn thiện hơn những người khác. Thật không may, chúng bị quá tải với việc chèn mã C # để hỗ trợ các chức năng khác nhau.
Kích thước ngữ pháp cho các ngôn ngữ khác nhau như sau:
JS - 475 hàng phân tích cú pháp + 273 lexers =748 hàng
Java - 615 hàng phân tích cú pháp + 211 lexers =826 hàng
C # - 1159 hàng phân tích cú pháp + 433 lexers =1592 hàng
С ++ - 1933 hàng
MySQL - 2515 hàng phân tích cú pháp + 1189 lexers =3704 hàng
T-SQL - 4035 hàng phân tích cú pháp + 896 lexers =4931 hàng
PL SQL - 6719 hàng phân tích cú pháp + 2366 lexers =9085 hàng
Phần cuối của một số từ vựng có danh sách các ký tự Unicode có sẵn trong ngôn ngữ. Những danh sách đó là vô ích liên quan đến việc đánh giá độ phức tạp của ngôn ngữ. Do đó, số hàng tôi lấy luôn kết thúc trước các danh sách này.
Đánh giá mức độ phức tạp của phân tích cú pháp ngôn ngữ dựa trên số lượng hàng trong ngữ pháp ngôn ngữ là một điều còn tranh cãi. Tuy nhiên, tôi tin rằng điều quan trọng là phải đưa ra những con số thể hiện sự khác biệt lớn.
Đó không phải là tất cả. Vì chúng tôi đang phát triển IDE, chúng tôi sẽ xử lý các tập lệnh không hoàn chỉnh hoặc không hợp lệ. Chúng tôi đã phải nghĩ ra nhiều chiêu trò, nhưng khách hàng vẫn gửi nhiều kịch bản làm việc với những kịch bản còn dang dở. Chúng tôi cần giải quyết vấn đề này.
Dự đoán chiến tranh
Trong quá trình phân tích cú pháp mã, từ này đôi khi không cho bạn biết bạn nên chọn lựa chọn thay thế nào trong số hai lựa chọn thay thế. Cơ chế giải quyết loại lỗi không chính xác này là lookahead trong ANTLR. Phương thức phân tích cú pháp là chuỗi if’s được chèn , và mỗi người trong số họ đều đi trước một bước. Xem ví dụ về ngữ pháp tạo ra sự không chắc chắn thuộc loại này:
rule1:
'a' rule2 | rule3
;
rule2:
'b' 'c' 'd'
;
rule3:
'b' 'c' 'e'
;
Ở giữa rule1, khi mã thông báo ‘a’ đã được chuyển, trình phân tích cú pháp sẽ tiến hành hai bước để chọn quy tắc để tuân theo. Việc kiểm tra này sẽ được thực hiện lại một lần nữa, nhưng ngữ pháp này có thể được viết lại để loại trừ lookahead . Nhược điểm là những tối ưu hóa như vậy gây hại cho cấu trúc, trong khi việc tăng hiệu suất là khá nhỏ.
Có nhiều cách phức tạp hơn để giải quyết loại sự không chắc chắn này. Ví dụ: Vị từ cú pháp (SynPred) cơ chế trong ANTLR3 . Nó hữu ích khi phần kết thúc tùy chọn của một mệnh đề vượt qua phần đầu của mệnh đề tùy chọn tiếp theo.
Về mặt ANTLR3, một vị từ là một phương thức được tạo để thực hiện một mục nhập văn bản ảo theo một trong các lựa chọn thay thế . Khi thành công, nó trả về giá trị true giá trị và việc hoàn thành vị từ thành công. Khi đó là một mục nhập ảo, nó được gọi là backtracking mục nhập chế độ. Nếu một vị từ hoạt động thành công, thì mục nhập thực sự sẽ xảy ra.
Chỉ có vấn đề khi một vị từ bắt đầu bên trong một vị từ khác. Sau đó, một khoảng cách có thể bị vượt qua hàng trăm hoặc hàng nghìn lần.
Hãy xem lại một ví dụ đơn giản. Có ba điểm không chắc chắn:(A, B, C).
- Trình phân tích cú pháp nhập A, ghi nhớ vị trí của nó trong văn bản, bắt đầu một mục nhập ảo cấp 1.
- Trình phân tích cú pháp nhập B, ghi nhớ vị trí của nó trong văn bản, bắt đầu một mục nhập ảo cấp 2.
- Trình phân tích cú pháp nhập C, ghi nhớ vị trí của nó trong văn bản, bắt đầu một mục nhập ảo cấp 3.
- Trình phân tích cú pháp hoàn thành một mục nhập ảo cấp 3, quay lại cấp 2 và vượt qua C một lần nữa.
- Trình phân tích cú pháp hoàn thành một mục nhập ảo cấp 2, quay lại cấp 1 và vượt qua B và C một lần nữa.
- Trình phân tích cú pháp hoàn thành một mục nhập ảo, trả về và thực hiện một mục nhập thực thông qua A, B và C.
Do đó, tất cả các kiểm tra trong phạm vi C sẽ được thực hiện 4 lần, trong phạm vi B - 3 lần, trong phạm vi A - 2 lần.
Nhưng điều gì sẽ xảy ra nếu một giải pháp thay thế phù hợp nằm ở vị trí thứ hai hoặc thứ ba trong danh sách? Sau đó, một trong các giai đoạn vị ngữ sẽ bị lỗi. Vị trí của nó trong văn bản sẽ lùi lại và một vị từ khác sẽ bắt đầu chạy.
Khi phân tích lý do khiến ứng dụng bị đóng băng, chúng tôi thường tình cờ phát hiện ra dấu vết của SynPred thực hiện vài nghìn lần. SynPred s đặc biệt có vấn đề trong các quy tắc đệ quy. Đáng buồn thay, SQL là đệ quy về bản chất của nó. Khả năng sử dụng truy vấn con hầu như ở mọi nơi đều có giá của nó. Tuy nhiên, có thể thao tác quy tắc để biến vị ngữ biến mất.
SynPred làm giảm hiệu suất. Tại một số thời điểm, số lượng của họ bị kiểm soát chặt chẽ. Nhưng vấn đề là khi bạn viết mã ngữ pháp, SynPred có thể tỏ ra không biết trước đối với bạn. Hơn nữa, việc thay đổi một quy tắc có thể khiến SynPred xuất hiện trong một quy tắc khác và điều đó khiến cho việc kiểm soát chúng trên thực tế là không thể.
Chúng tôi đã tạo một biểu thức chính quy đơn giản công cụ để kiểm soát số lượng vị từ được chạy bởi Nhiệm vụ MSBuild đặc biệt . Nếu số lượng vị từ không khớp với số lượng được chỉ định trong một tệp, tác vụ ngay lập tức không thể xây dựng và cảnh báo về lỗi.
Khi gặp lỗi, nhà phát triển nên viết lại mã của quy tắc nhiều lần để loại bỏ các vị từ thừa. Nếu không thể tránh được các vị từ, nhà phát triển sẽ thêm nó vào một tệp đặc biệt để thu hút thêm sự chú ý cho bài đánh giá.
Trong những trường hợp hiếm hoi, chúng tôi thậm chí đã viết các vị từ của mình bằng C # chỉ để tránh các vị từ được tạo ANTLR. May mắn thay, phương pháp này cũng tồn tại.
Kế thừa ngữ pháp
Khi có bất kỳ thay đổi nào đối với các DBMS được hỗ trợ của chúng tôi, chúng tôi phải đáp ứng chúng trong các công cụ của mình. Hỗ trợ cho các cấu trúc cú pháp ngữ pháp luôn là điểm khởi đầu.
Chúng tôi tạo một ngữ pháp đặc biệt cho mỗi phương ngữ SQL. Nó cho phép lặp lại một số mã, nhưng sẽ dễ hơn việc cố gắng tìm điểm chung của chúng.
Chúng tôi đã viết bộ tiền xử lý ngữ pháp ANTLR của riêng mình có chức năng kế thừa ngữ pháp.
Rõ ràng là chúng ta cần một cơ chế cho tính đa hình - khả năng không chỉ xác định lại quy tắc trong thế hệ con cháu mà còn gọi quy tắc cơ bản. Chúng tôi cũng muốn kiểm soát vị trí khi gọi quy tắc cơ sở.
Công cụ là một điểm cộng rõ ràng khi chúng tôi so sánh ANTLR với các công cụ nhận dạng ngôn ngữ khác, Visual Studio và ANTLRWorks. Và bạn không muốn mất lợi thế này trong khi triển khai kế thừa. Giải pháp là chỉ định ngữ pháp cơ bản theo ngữ pháp kế thừa ở định dạng bình luận ANTLR. Đối với các công cụ ANTLR, đó chỉ là một nhận xét, nhưng chúng tôi có thể trích xuất tất cả thông tin cần thiết từ đó.
Chúng tôi đã viết một Nhiệm vụ MsBuild được nhúng vào hệ thống xây dựng toàn bộ dưới dạng hành động trước khi xây dựng. Nhiệm vụ là thực hiện công việc của một bộ xử lý trước cho ngữ pháp ANTLR bằng cách tạo ra ngữ pháp kết quả từ cơ sở của nó và các đồng nghiệp kế thừa. Ngữ pháp kết quả được xử lý bởi chính ANTLR.
Xử lý sau ANTLR
Trong nhiều ngôn ngữ lập trình, Không thể sử dụng Từ khóa làm tên chủ đề. Có thể có từ 800 đến 3000 từ khóa trong SQL tùy thuộc vào phương ngữ. Hầu hết chúng đều gắn liền với ngữ cảnh bên trong cơ sở dữ liệu. Do đó, việc cấm chúng làm tên đối tượng sẽ khiến người dùng thất vọng. Đó là lý do tại sao SQL có các từ khóa dành riêng và không đặt trước.
Bạn không thể đặt tên đối tượng của mình là từ dành riêng (CHỌN, TỪ, v.v.) mà không trích dẫn nó, nhưng bạn có thể thực hiện việc này đối với từ chưa được đặt trước (CHUYỂN ĐỔI, CÓ SN, v.v.). Sự tương tác này làm cho việc phát triển trình phân tích cú pháp khó hơn.
Trong quá trình phân tích từ vựng, ngữ cảnh là không xác định, nhưng trình phân tích cú pháp đã yêu cầu các số khác nhau cho số nhận dạng và từ khóa. Đó là lý do tại sao chúng tôi đã thêm một quá trình hậu xử lý khác vào trình phân tích cú pháp ANTLR. Nó đã thay thế tất cả các kiểm tra định danh rõ ràng bằng việc gọi một phương thức đặc biệt.
Phương pháp này có một kiểm tra chi tiết hơn. Nếu mục nhập gọi một số nhận dạng và chúng tôi hy vọng rằng số nhận dạng được đáp ứng trở đi, thì mọi việc đều tốt. Nhưng nếu một từ chưa được lưu trữ là một mục nhập, chúng ta nên kiểm tra lại nó. Kiểm tra bổ sung này xem xét tìm kiếm chi nhánh trong bối cảnh hiện tại mà từ khóa chưa được đặt trước này có thể là một từ khóa. Nếu không có nhánh nào như vậy, nó có thể được sử dụng làm mã định danh.
Về mặt kỹ thuật, vấn đề này có thể được giải quyết bằng phương tiện ANTLR nhưng quyết định này không phải là tối ưu. Cách ANTLR là tạo một quy tắc liệt kê tất cả các từ khóa chưa được đặt trước và một số nhận dạng lexeme. Hơn nữa, một quy tắc đặc biệt sẽ phân phát thay vì một số nhận dạng lexeme. Giải pháp này khiến nhà phát triển không quên thêm từ khóa vào nơi nó được sử dụng và trong quy tắc đặc biệt. Ngoài ra, nó còn tối ưu hóa thời gian sử dụng.
Lỗi trong phân tích cú pháp không có cây
Cây cú pháp thường là kết quả của công việc phân tích cú pháp. Nó là một cấu trúc dữ liệu phản ánh văn bản chương trình thông qua ngữ pháp chính thức. Nếu bạn muốn triển khai một trình chỉnh sửa mã với tính năng tự động hoàn thành ngôn ngữ, rất có thể bạn sẽ nhận được thuật toán sau:
- Phân tích cú pháp văn bản trong trình chỉnh sửa. Sau đó, bạn sẽ có một cây cú pháp.
- Tìm một nút dưới dòng xuống dòng và so khớp nó với ngữ pháp.
- Tìm hiểu những từ khoá và loại đối tượng nào sẽ có sẵn tại Point.
Trong trường hợp này, ngữ pháp dễ được hình dung như một Đồ thị hoặc một Máy trạng thái.
Thật không may, chỉ có phiên bản thứ ba của ANTLR khi dbForge IDE bắt đầu được phát triển. Tuy nhiên, nó không nhanh nhẹn và mặc dù bạn có thể cho ANTLR biết cách xây dựng một cái cây, nhưng việc sử dụng không được suôn sẻ.
Hơn nữa, nhiều bài viết về chủ đề này đã đề xuất sử dụng cơ chế 'hành động' để chạy mã khi trình phân tích cú pháp chuyển qua quy tắc. Cơ chế này rất tiện dụng, nhưng nó đã dẫn đến các vấn đề về kiến trúc và làm cho việc hỗ trợ chức năng mới trở nên phức tạp hơn.
Vấn đề là, một tệp ngữ pháp đơn lẻ bắt đầu tích lũy các "hành động" do số lượng lớn các chức năng đáng ra phải được phân phối cho các bản dựng khác nhau. Chúng tôi đã quản lý để phân phối các trình xử lý hành động cho các phiên bản khác nhau và tạo ra một biến thể mẫu trình thông báo người đăng ký lén lút cho biện pháp đó.
ANTLR3 hoạt động nhanh hơn gấp 6 lần so với ANTLR4 theo các phép đo của chúng tôi. Ngoài ra, cây cú pháp cho các tập lệnh lớn có thể chiếm quá nhiều RAM, đây không phải là tin tốt, vì vậy chúng tôi cần hoạt động trong không gian địa chỉ 32 bit của Visual Studio và SQL Management Studio.
Xử lý sau trình phân tích cú pháp ANTLR
Khi làm việc với chuỗi, một trong những thời điểm quan trọng nhất là giai đoạn phân tích từ vựng, nơi chúng tôi chia tập lệnh thành các từ riêng biệt.
ANTLR coi như ngữ pháp đầu vào chỉ định ngôn ngữ và xuất ra trình phân tích cú pháp bằng một trong các ngôn ngữ có sẵn. Tại một số điểm, trình phân tích cú pháp được tạo đã phát triển đến mức chúng tôi sợ phải gỡ lỗi nó. Nếu bạn nhấn F11 (bước vào) khi gỡ lỗi và truy cập tệp phân tích cú pháp, Visual Studio sẽ chỉ bị lỗi.
Hóa ra là nó không thành công do ngoại lệ OutOfMemory khi phân tích tệp phân tích cú pháp. Tệp này chứa hơn 200.000 dòng mã.
Nhưng gỡ lỗi trình phân tích cú pháp là một phần thiết yếu của quy trình làm việc và bạn không thể bỏ qua nó. Với sự trợ giúp của các lớp từng phần của C #, chúng tôi đã phân tích trình phân tích cú pháp được tạo bằng cách sử dụng biểu thức chính quy và chia nó thành một vài tệp. Visual Studio hoạt động hoàn hảo với nó.
Phân tích từ vựng không có chuỗi con trước API Span
Nhiệm vụ chính của phân tích từ vựng là phân loại - xác định ranh giới của các từ và kiểm tra chúng với từ điển. Nếu từ được tìm thấy, lexer sẽ trả về chỉ mục của nó. Nếu không, từ được coi là một định danh đối tượng. Đây là mô tả đơn giản của thuật toán.
Lexing nền trong khi mở tệp
Đánh dấu cú pháp dựa trên phân tích từ vựng. Thao tác này thường mất nhiều thời gian hơn so với việc đọc văn bản từ đĩa. Lợi ích là gì? Trong một chuỗi, văn bản đang được đọc từ tệp, trong khi phân tích từ vựng được thực hiện trong một chuỗi khác.
Lexer đọc văn bản theo từng hàng. Nếu nó yêu cầu một hàng không tồn tại, nó sẽ dừng lại và đợi.
BlockingCollection
- Đọc từ một tệp là Nhà sản xuất, trong khi lexer là Người tiêu dùng.
- Lexer đã là Nhà sản xuất và trình soạn thảo văn bản là Người tiêu dùng.
Bộ thủ thuật này cho phép chúng tôi rút ngắn đáng kể thời gian dành cho việc mở các tệp lớn. Trang đầu tiên của tài liệu được hiển thị rất nhanh, tuy nhiên, tài liệu có thể bị đóng băng nếu người dùng cố gắng di chuyển đến cuối tệp trong vài giây đầu tiên. Nó xảy ra bởi vì trình đọc nền và lexer cần phải đến cuối tài liệu. Tuy nhiên, nếu người dùng làm việc di chuyển từ đầu tài liệu đến cuối tài liệu một cách chậm rãi, sẽ không có bất kỳ sự cố đóng băng nào đáng chú ý.
Tối ưu hóa không rõ ràng:phân tích từ vựng từng phần
Phân tích cú pháp thường được chia thành hai cấp độ:
- luồng ký tự đầu vào được xử lý để nhận lexemes (mã thông báo) dựa trên các quy tắc ngôn ngữ - đây được gọi là phân tích từ vựng
- trình phân tích cú pháp sử dụng luồng mã thông báo để kiểm tra nó theo các quy tắc ngữ pháp chính thức và thường xây dựng một cây cú pháp.
Xử lý chuỗi là một hoạt động tốn kém. Để tối ưu hóa nó, chúng tôi quyết định không thực hiện phân tích toàn bộ từ vựng của văn bản mỗi lần mà chỉ phân tích lại phần đã bị thay đổi. Nhưng làm thế nào để đối phó với các cấu trúc nhiều dòng như nhận xét khối hoặc dòng? Chúng tôi đã lưu trữ trạng thái cuối dòng cho mỗi dòng:“không có mã thông báo nhiều dòng” =0, “phần đầu của nhận xét khối” =1, “phần bắt đầu của chuỗi nhiều dòng” =2. Phân tích từ vựng bắt đầu từ phần đã thay đổi và kết thúc khi trạng thái cuối dòng bằng trạng thái được lưu trữ.
Có một vấn đề với giải pháp này:cực kỳ bất tiện khi theo dõi số dòng trong cấu trúc như vậy trong khi số dòng là thuộc tính bắt buộc của mã thông báo ANTLR vì khi một dòng được chèn hoặc xóa, số dòng tiếp theo sẽ được cập nhật tương ứng. Chúng tôi đã giải quyết nó bằng cách đặt số dòng ngay lập tức, trước khi chuyển mã thông báo cho trình phân tích cú pháp. Các thử nghiệm chúng tôi thực hiện sau đó đã cho thấy hiệu suất được cải thiện từ 15-25%. Sự cải thiện thực tế thậm chí còn lớn hơn.
Số lượng RAM cần thiết cho tất cả những điều này hóa ra nhiều hơn chúng tôi mong đợi. Mã thông báo ANTLR bao gồm:điểm đầu - 8 byte, điểm cuối - 8 byte, liên kết đến văn bản của từ - 4 hoặc 8 byte (không đề cập đến chính chuỗi), liên kết đến văn bản của tài liệu - 4 hoặc 8 byte, và một loại mã thông báo - 4 byte.
Vậy chúng ta có thể kết luận điều gì? Chúng tôi tập trung vào hiệu suất và tiêu thụ quá nhiều RAM ở một nơi mà chúng tôi không mong đợi. Chúng tôi không cho rằng điều này sẽ xảy ra vì chúng tôi đã cố gắng sử dụng các cấu trúc nhẹ thay vì các lớp. Bằng cách thay thế chúng bằng các vật nặng, chúng tôi đã cố ý tăng thêm chi phí cho bộ nhớ để có được hiệu suất tốt hơn. May mắn thay, điều này đã dạy cho chúng tôi một bài học quan trọng, vì vậy bây giờ mỗi tối ưu hóa hiệu suất kết thúc bằng việc xác định mức tiêu thụ bộ nhớ và ngược lại.
Đây là một câu chuyện có đạo đức. Một số tính năng bắt đầu hoạt động gần như ngay lập tức và những tính năng khác chỉ nhanh hơn một chút. Rốt cuộc, sẽ không thể thực hiện thủ thuật phân tích từ vựng nền nếu không có đối tượng mà một trong các chuỗi có thể lưu trữ mã thông báo.
Tất cả các vấn đề khác xảy ra trong bối cảnh phát triển máy tính để bàn trên ngăn xếp .NET.
Sự cố 32 bit
Một số người dùng chọn sử dụng các phiên bản độc lập của sản phẩm của chúng tôi. Những người khác tiếp tục hoạt động bên trong Visual Studio và SQL Server Management Studio. Nhiều phần mở rộng được phát triển cho chúng. Một trong những phần mở rộng này là SQL Complete. Để làm rõ, nó cung cấp nhiều quyền hạn và tính năng hơn SSMS hoàn thành mã tiêu chuẩn và VS cho SQL.
Phân tích cú pháp SQL là một quá trình rất tốn kém, cả về tài nguyên CPU và RAM. Để nhắc danh sách các đối tượng trong tập lệnh người dùng, không có các lệnh gọi không cần thiết đến máy chủ, chúng tôi lưu trữ bộ đệm đối tượng trong RAM. Thông thường, nó không chiếm nhiều dung lượng, nhưng một số người dùng của chúng tôi có cơ sở dữ liệu chứa tới một phần tư triệu đối tượng.
Làm việc với SQL hoàn toàn khác với làm việc với các ngôn ngữ khác. Trong C #, thực tế không có tệp nào kể cả với hàng nghìn dòng mã. Trong khi đó, trong SQL, một nhà phát triển có thể làm việc với một kết xuất cơ sở dữ liệu bao gồm vài triệu dòng mã. Không có gì bất thường về nó.
DLL-Hell bên trong VS
Có một công cụ hữu ích để phát triển các plugin trong .NET Framework, đó là một miền ứng dụng. Mọi thứ được thực hiện một cách cô lập. Nó có thể được dỡ bỏ. Đối với hầu hết các phần, việc triển khai các tiện ích mở rộng có lẽ là lý do chính khiến các miền ứng dụng được giới thiệu.
Ngoài ra, còn có MAF Framework, được MS thiết kế để giải quyết vấn đề tạo tiện ích bổ sung cho chương trình. Nó cô lập các tiện ích bổ sung này đến mức có thể gửi chúng đến một quy trình riêng biệt và tiếp quản mọi thông tin liên lạc. Thành thật mà nói, giải pháp này quá cồng kềnh và không phổ biến nhiều.
Thật không may, Microsoft Visual Studio và SQL Server Management Studio được xây dựng dựa trên nó, triển khai hệ thống mở rộng khác nhau. Nó đơn giản hóa việc truy cập các ứng dụng lưu trữ cho các plugin, nhưng nó buộc chúng phải phù hợp với nhau trong một quy trình và miền với một quy trình khác.
Cũng giống như bất kỳ ứng dụng nào khác trong thế kỷ 21, ứng dụng của chúng ta có rất nhiều phụ thuộc. Phần lớn trong số chúng là các thư viện nổi tiếng, đã được kiểm chứng về thời gian và phổ biến trong thế giới .NET.
Kéo thư vào trong ổ khóa
Không nhiều người biết rằng .NET Framework sẽ bơm Windows Message Queue bên trong mỗi WaitHandle. Để đặt nó bên trong mọi khóa, bất kỳ trình xử lý nào của bất kỳ sự kiện nào trong ứng dụng đều có thể được gọi nếu khóa này có thời gian để chuyển sang chế độ hạt nhân và nó không được giải phóng trong giai đoạn chờ quay vòng.
Điều này có thể dẫn đến sự say mê trở lại ở một số nơi rất bất ngờ. Một vài lần nó dẫn đến các vấn đề như “Bộ sưu tập đã được sửa đổi trong quá trình liệt kê” và các ArgumentOutOfRangeException khác nhau.
Thêm một hợp ngữ vào một giải pháp sử dụng SQL
Khi dự án phát triển, nhiệm vụ thêm các tổ hợp, ban đầu đơn giản, phát triển thành hàng chục bước phức tạp. Một lần, chúng tôi phải thêm một tá các tổ hợp khác nhau vào giải pháp, chúng tôi đã thực hiện một cuộc tái cấu trúc lớn. Gần 80 giải pháp, bao gồm sản phẩm và giải pháp thử nghiệm, đã được tạo dựa trên khoảng 300 dự án .NET.
Dựa trên các giải pháp sản phẩm, chúng tôi đã viết các tệp Inno Setup. Chúng bao gồm danh sách các cụm được đóng gói trong bản cài đặt mà người dùng đã tải xuống. Thuật toán thêm một dự án như sau:
- Tạo một dự án mới.
- Thêm chứng chỉ vào đó. Thiết lập thẻ của công trình.
- Thêm tệp phiên bản.
- Định cấu hình lại các đường dẫn mà dự án đang diễn ra.
- Đổi tên thư mục để phù hợp với thông số nội bộ.
- Thêm dự án vào giải pháp một lần nữa.
- Thêm một số tập hợp mà tất cả các dự án cần liên kết đến.
- Thêm bản dựng vào tất cả các giải pháp cần thiết:thử nghiệm và sản phẩm.
- Đối với tất cả các giải pháp sản phẩm, hãy thêm các cụm vào cài đặt.
9 bước này phải được lặp lại khoảng 10 lần. Các bước 8 và 9 không phải là nhỏ, và bạn có thể dễ dàng quên thêm các bản dựng ở mọi nơi.
Đối mặt với một nhiệm vụ lớn và thường xuyên như vậy, bất kỳ lập trình viên bình thường nào cũng muốn tự động hóa nó. Đó chính xác là những gì chúng tôi muốn làm. Nhưng làm thế nào để chúng tôi chỉ ra những giải pháp và cài đặt chính xác để thêm vào dự án mới được tạo? Có rất nhiều kịch bản và còn hơn thế nữa, rất khó để dự đoán một số trong số đó.
Chúng tôi đã nảy ra một ý tưởng điên rồ. Các giải pháp được kết nối với các dự án như nhiều-nhiều, các dự án có cài đặt theo cùng một cách và SQL có thể giải quyết chính xác loại nhiệm vụ mà chúng tôi đã có.
Chúng tôi đã tạo một Ứng dụng .Net Core Console quét tất cả các tệp .sln trong thư mục nguồn, truy xuất danh sách các dự án từ chúng với sự trợ giúp của DotNet CLI và đưa nó vào cơ sở dữ liệu SQLite. Chương trình có một số chế độ:
- Mới - tạo dự án và tất cả các thư mục cần thiết, thêm chứng chỉ, thiết lập thẻ, thêm phiên bản, các tập hợp cần thiết tối thiểu.
- Add-Project - thêm dự án vào tất cả các giải pháp đáp ứng truy vấn SQL sẽ được cung cấp dưới dạng một trong các tham số. Để thêm dự án vào giải pháp, chương trình bên trong sử dụng DotNet CLI.
- Add-ISS - thêm dự án vào tất cả các bản cài đặt, đáp ứng các truy vấn SQL.
Mặc dù ý tưởng chỉ ra danh sách các giải pháp thông qua truy vấn SQL có vẻ phức tạp, nhưng nó hoàn toàn đóng tất cả các trường hợp hiện có và rất có thể là bất kỳ trường hợp nào có thể xảy ra trong tương lai.
Hãy để tôi chứng minh kịch bản. Tạo dự án “A” và thêm nó vào tất cả các giải pháp mà các dự án “B” được sử dụng:
dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"
Sự cố với LiteDB
Một vài năm trước, chúng tôi có nhiệm vụ phát triển một chức năng nền để lưu tài liệu của người dùng. Nó có hai luồng ứng dụng chính:khả năng đóng IDE ngay lập tức và rời đi, sau đó quay lại bắt đầu từ nơi bạn đã dừng lại và khả năng khôi phục trong các tình huống khẩn cấp như mất điện hoặc sự cố chương trình.
Để thực hiện tác vụ này, cần phải lưu nội dung của các tệp ở đâu đó bên cạnh, và thực hiện nó thường xuyên và nhanh chóng. Ngoài nội dung, cần phải lưu một số siêu dữ liệu, điều này làm cho việc lưu trữ trực tiếp trong hệ thống tệp trở nên bất tiện.
Tại thời điểm đó, chúng tôi tìm thấy thư viện LiteDB, gây ấn tượng với chúng tôi về tính đơn giản và hiệu suất của nó. LiteDB là một cơ sở dữ liệu nhúng nhanh gọn nhẹ, được viết hoàn toàn bằng C #. Tốc độ và sự đơn giản tổng thể đã chiến thắng chúng tôi.
Trong quá trình phát triển, cả nhóm đều hài lòng khi làm việc với LiteDB. Tuy nhiên, các vấn đề chính bắt đầu sau khi phát hành.
Tài liệu chính thức đảm bảo rằng cơ sở dữ liệu đảm bảo hoạt động thích hợp với quyền truy cập đồng thời từ nhiều luồng cũng như một số quy trình. Các thử nghiệm tổng hợp tích cực cho thấy rằng cơ sở dữ liệu không hoạt động chính xác trong môi trường đa luồng.
Để nhanh chóng khắc phục sự cố, chúng tôi đã đồng bộ hóa các quy trình với sự trợ giúp của ReadWriteLock liên quy trình tự viết. Bây giờ, sau gần ba năm, LiteDB đang hoạt động tốt hơn nhiều.
StreamStringList
Vấn đề này ngược lại với trường hợp phân tích từ vựng từng phần. Khi chúng ta làm việc với một văn bản, sẽ thuận tiện hơn khi làm việc với nó dưới dạng danh sách chuỗi. Các chuỗi có thể được yêu cầu theo thứ tự ngẫu nhiên, nhưng mật độ truy cập bộ nhớ nhất định vẫn còn. Tại một số thời điểm, cần phải chạy một số tác vụ để xử lý các tệp rất lớn mà không cần tải đầy bộ nhớ. Ý tưởng như sau:
- Để đọc từng dòng tệp. Ghi nhớ các phần bù trong tệp.
- Theo yêu cầu, hãy đưa ra dòng tiếp theo, đặt độ lệch bắt buộc và trả lại dữ liệu.
Nhiệm vụ chính đã hoàn thành. Cấu trúc này không chiếm nhiều dung lượng so với kích thước tệp. Ở giai đoạn thử nghiệm, chúng tôi kiểm tra kỹ lưỡng dung lượng bộ nhớ cho các tệp lớn và rất lớn. Các tệp lớn được xử lý trong một thời gian dài và các tệp nhỏ sẽ được xử lý ngay lập tức.
Không có tham chiếu nào để kiểm tra thời gian thực thi . RAM được gọi là Bộ nhớ truy cập ngẫu nhiên - đây là lợi thế cạnh tranh của nó so với SSD và đặc biệt là so với HDD. Các trình điều khiển này bắt đầu hoạt động không tốt để truy cập ngẫu nhiên. Hóa ra cách làm này đã làm chậm công việc gần 40 lần, so với việc tải hoàn toàn một tập tin vào bộ nhớ. Ngoài ra, chúng tôi đọc toàn bộ tệp 2,5-10 lần tùy thuộc vào ngữ cảnh.
Giải pháp rất đơn giản và cải tiến là đủ để thao tác sẽ chỉ mất nhiều thời gian hơn một chút so với khi tệp được tải hoàn toàn vào bộ nhớ.
Tương tự như vậy, mức tiêu thụ RAM cũng không đáng kể. Chúng tôi tìm thấy nguồn cảm hứng trong nguyên tắc tải dữ liệu từ RAM vào bộ xử lý bộ nhớ đệm. Khi bạn truy cập vào một phần tử mảng, bộ xử lý sẽ sao chép hàng chục phần tử lân cận vào bộ đệm của nó vì các phần tử cần thiết thường ở gần đó.
Nhiều cấu trúc dữ liệu sử dụng tối ưu hóa bộ xử lý này để đạt được hiệu suất cao nhất. Chính vì tính đặc thù này mà truy cập ngẫu nhiên vào các phần tử của mảng chậm hơn nhiều so với truy cập tuần tự. Chúng tôi thực hiện một cơ chế tương tự:chúng tôi đọc một tập hợp hàng nghìn chuỗi và ghi nhớ hiệu số của chúng. Khi chúng tôi truy cập chuỗi thứ 1001, chúng tôi thả 500 chuỗi đầu tiên và tải 500 chuỗi tiếp theo. Trong trường hợp chúng tôi cần bất kỳ chuỗi nào trong số 500 dòng đầu tiên, thì chúng tôi chuyển sang chuỗi đó riêng biệt, vì chúng tôi đã có phần bù.
Người lập trình không nhất thiết phải xây dựng và kiểm tra các yêu cầu phi chức năng một cách cẩn thận. Do đó, chúng tôi ghi nhớ cho các trường hợp sau này rằng chúng tôi cần làm việc tuần tự với trí nhớ bền bỉ.
Phân tích các ngoại lệ
Bạn có thể thu thập dữ liệu hoạt động của người dùng dễ dàng trên web. Tuy nhiên, nó không phải là trường hợp phân tích các ứng dụng máy tính để bàn. Không có công cụ nào có khả năng cung cấp một bộ số liệu và công cụ trực quan hóa đáng kinh ngạc như Google Analytics. Tại sao? Đây là giả định của tôi:
- Trong suốt phần chính của lịch sử phát triển ứng dụng dành cho máy tính để bàn, họ không có quyền truy cập ổn định và lâu dài vào Web.
- Có nhiều công cụ phát triển cho các ứng dụng máy tính để bàn. Do đó, không thể xây dựng công cụ thu thập dữ liệu người dùng đa năng cho tất cả các công nghệ và khuôn khổ giao diện người dùng.
Một khía cạnh quan trọng của việc thu thập dữ liệu là theo dõi các trường hợp ngoại lệ. Ví dụ:chúng tôi thu thập dữ liệu về sự cố. Trước đây, người dùng của chúng tôi phải tự viết thư cho email hỗ trợ khách hàng, thêm Dấu vết ngăn xếp về lỗi, được sao chép từ một cửa sổ ứng dụng đặc biệt. Rất ít người dùng đã làm theo tất cả các bước này. Dữ liệu thu thập được hoàn toàn ẩn danh, điều này làm chúng tôi mất cơ hội tìm ra các bước tái tạo hoặc bất kỳ thông tin nào khác từ người dùng.
Mặt khác, dữ liệu lỗi nằm trong cơ sở dữ liệu Postgres và điều này mở đường cho việc kiểm tra tức thì hàng chục giả thuyết. Bạn có thể ngay lập tức nhận được câu trả lời bằng cách thực hiện các truy vấn SQL đến cơ sở dữ liệu. Thường thì không rõ ràng chỉ từ một ngăn xếp hoặc loại ngoại lệ như thế nào mà ngoại lệ xảy ra, đó là lý do tại sao tất cả thông tin này đều rất quan trọng để nghiên cứu vấn đề.
Ngoài ra, bạn có cơ hội phân tích tất cả dữ liệu thu thập được và tìm ra các mô-đun và lớp có vấn đề nhất. Dựa vào kết quả phân tích, bạn có thể lập kế hoạch tái cấu trúc hoặc kiểm tra bổ sung để bao gồm các phần này của chương trình.
Dịch vụ giải mã ngăn xếp
Các bản dựng .NET chứa mã IL, có thể dễ dàng chuyển đổi lại thành mã C #, chính xác với người vận hành, sử dụng một số chương trình đặc biệt. Một trong những cách để bảo vệ mã chương trình là sự xáo trộn của nó. Các chương trình có thể được đổi tên; các phương thức, biến và lớp có thể được thay thế; Mã có thể được thay thế bằng mã tương đương, nhưng nó thực sự không thể hiểu được.
Sự cần thiết phải làm xáo trộn mã nguồn xuất hiện khi bạn phân phối sản phẩm của mình theo cách đề xuất rằng người dùng nhận được các bản dựng ứng dụng của bạn. Các ứng dụng máy tính để bàn là những trường hợp đó. Tất cả các bản dựng, bao gồm cả các bản dựng trung gian dành cho người thử nghiệm, đều được xáo trộn cẩn thận.
Đơn vị Đảm bảo Chất lượng của chúng tôi sử dụng các công cụ ngăn xếp giải mã từ nhà phát triển obfuscator. Để bắt đầu giải mã, họ cần chạy ứng dụng, tìm bản đồ giải mã do CI xuất bản cho một bản dựng cụ thể và chèn ngăn xếp ngoại lệ vào trường đầu vào.
Các phiên bản và trình chỉnh sửa khác nhau bị xáo trộn theo một cách khác nhau, điều này khiến nhà phát triển khó nghiên cứu vấn đề hoặc thậm chí có thể đưa anh ta đi sai hướng. Rõ ràng là quá trình này phải được tự động hóa.
Định dạng bản đồ giải mã hóa ra khá đơn giản. Chúng tôi dễ dàng giải mã nó và viết một chương trình giải mã ngăn xếp. Trước đó không lâu, giao diện người dùng web đã được phát triển để hiển thị các ngoại lệ theo phiên bản sản phẩm và nhóm chúng theo ngăn xếp. Đó là một trang web .NET Core với cơ sở dữ liệu trong SQLite.
SQLite là một công cụ gọn gàng cho các giải pháp nhỏ. Chúng tôi cũng đã cố gắng đặt bản đồ giải mã ở đó. Mỗi bản dựng đã tạo ra khoảng 500 nghìn cặp mã hóa và giải mã. SQLite không thể xử lý tỷ lệ chèn mạnh như vậy.
Trong khi dữ liệu trên một bản dựng được chèn vào cơ sở dữ liệu, hai bản khác đã được thêm vào hàng đợi. Không lâu trước vấn đề đó, tôi đã nghe báo cáo về Clickhouse và rất háo hức dùng thử. Nó được chứng minh là tuyệt vời, tốc độ chèn tăng hơn 200 lần.
Điều đó có nghĩa là, việc giải mã ngăn xếp (đọc từ cơ sở dữ liệu) chậm lại gần 50 lần, nhưng vì mỗi ngăn xếp mất chưa đến 1 mili giây, nên việc dành thời gian nghiên cứu vấn đề này sẽ không hiệu quả về mặt chi phí.
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.
Kết luận
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.