sql >> Base de Datos >  >> RDS >> Database

Completando SQL. Historias de éxito y fracaso

He estado trabajando para una empresa que desarrolla IDE para la interacción de bases de datos durante más de cinco años. Antes de comenzar a escribir este artículo, no tenía idea de cuántas historias fantásticas me esperaban.

Mi equipo desarrolla y admite las características del lenguaje IDE, y el autocompletado de código es la principal. Me enfrenté a muchas cosas emocionantes que sucedían. Algunas cosas las hicimos muy bien desde el primer intento, y otras fallaron incluso después de varios intentos.

SQL y análisis de dialectos

SQL es un intento de parecerse a un lenguaje natural, y el intento es bastante exitoso, debo decir. Dependiendo del dialecto, hay varios miles de palabras clave. Para distinguir una declaración de otra, a menudo necesita buscar una o dos palabras (tokens) por delante. Este enfoque se denomina anticipación. .

Hay una clasificación de analizador dependiendo de qué tan lejos puedan mirar hacia adelante:LA(1), LA(2) o LA(*), lo que significa que un analizador puede mirar hacia adelante tanto como sea necesario para definir la bifurcación correcta.

A veces, el final de una cláusula opcional coincide con el comienzo de otra cláusula opcional. Estas situaciones hacen que el análisis sea mucho más difícil de ejecutar. T-SQL no facilita las cosas. Además, algunas sentencias de SQL pueden tener, aunque no necesariamente, finales que pueden entrar en conflicto con el comienzo de sentencias anteriores.

¿No lo crees? Hay una forma de describir los lenguajes formales a través de la gramática. Puede generar un analizador a partir de él utilizando esta o aquella herramienta. Las herramientas y lenguajes más notables que describen la gramática son YACC y ANTLR.

YACC Los analizadores generados se utilizan en motores MySQL, MariaDB y PostgreSQL. Podríamos intentar tomarlos directamente del código fuente y desarrollar completado de código y otras funciones basadas en el análisis SQL utilizando estos analizadores. Además, este producto recibiría actualizaciones de desarrollo gratuitas y el analizador se comportaría de la misma manera que el motor fuente.

Entonces, ¿por qué seguimos usando ANTLR? ? Es firmemente compatible con C#/.NET, tiene un conjunto de herramientas decente, su sintaxis es mucho más fácil de leer y escribir. La sintaxis ANTLR se volvió tan útil que Microsoft ahora la usa en su documentación oficial de C#.

Pero volvamos a la complejidad de SQL cuando se trata de analizar. Me gustaría comparar los tamaños de gramática de los idiomas disponibles públicamente. En dbForge, usamos nuestras piezas de gramática. Son más completos que los demás. Desafortunadamente, están sobrecargados con las inserciones del código C# para admitir diferentes funciones.

Los tamaños de gramática para diferentes idiomas son los siguientes:

JS:475 filas de analizador + 273 lexers =748 filas

Java:615 filas de analizador + 211 lexers =826 filas

C#:1159 filas de analizador + 433 lexers =1592 filas

С++ – 1933 filas

MySQL:2515 filas de analizador + 1189 lexers =3704 filas

T-SQL:4035 filas de analizador + 896 lexers =4931 filas

PL SQL:6719 filas de analizador + 2366 lexers =9085 filas

Las terminaciones de algunos lexers presentan las listas de los caracteres Unicode disponibles en el idioma. Esas listas no sirven para evaluar la complejidad del lenguaje. Por lo tanto, el número de filas que tomaba siempre terminaba antes que estas listas.

Es discutible evaluar la complejidad del análisis del idioma en función del número de filas en la gramática del idioma. Aún así, creo que es importante mostrar los números que muestran una gran discrepancia.

Eso no es todo. Dado que estamos desarrollando un IDE, debemos lidiar con scripts incompletos o no válidos. Tuvimos que idear muchos trucos, pero los clientes todavía envían muchos escenarios de trabajo con guiones sin terminar. Necesitamos resolver esto.

Guerras predicadas

Durante el análisis del código, la palabra a veces no te dice cuál de las dos alternativas elegir. El mecanismo que soluciona este tipo de imprecisiones es el lookahead en ANTLR. El método analizador es la cadena insertada de if's , y cada uno de ellos mira un paso adelante. Vea el ejemplo de la gramática que genera la incertidumbre de este tipo:

rule1:
  'a' rule2 | rule3
;

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

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

En medio de la regla 1, cuando ya se ha pasado el token 'a', el analizador mirará dos pasos hacia adelante para elegir la regla a seguir. Esta verificación se realizará una vez más, pero esta gramática se puede volver a escribir para excluir la búsqueda anticipada . La desventaja es que tales optimizaciones dañan la estructura, mientras que el aumento de rendimiento es bastante pequeño.

Hay formas más complejas de resolver este tipo de incertidumbre. Por ejemplo, el predicado de sintaxis (SynPred) mecanismo en ANTLR3 . Ayuda cuando el final opcional de una cláusula cruza el comienzo de la siguiente cláusula opcional.

En términos de ANTLR3, un predicado es un método generado que realiza una entrada de texto virtual de acuerdo con una de las alternativas . Cuando tiene éxito, devuelve el verdadero y la finalización del predicado es satisfactoria. Cuando se trata de una entrada virtual, se denomina retroceso. entrada de modo. Si un predicado funciona con éxito, ocurre la entrada real.

Solo es un problema cuando un predicado comienza dentro de otro predicado. Entonces, una distancia podría cruzarse cientos o miles de veces.

Repasemos un ejemplo simplificado. Hay tres puntos de incertidumbre:(A, B, C).

  1. El analizador ingresa A, recuerda su posición en el texto, inicia una entrada virtual de nivel 1.
  2. El analizador ingresa B, recuerda su posición en el texto, inicia una entrada virtual de nivel 2.
  3. El analizador ingresa C, recuerda su posición en el texto, inicia una entrada virtual de nivel 3.
  4. El analizador completa una entrada virtual de nivel 3, regresa al nivel 2 y pasa C una vez más.
  5. El analizador completa una entrada virtual de nivel 2, vuelve al nivel 1 y pasa B y C una vez más.
  6. El analizador completa una entrada virtual, regresa y realiza una entrada real a través de A, B y C.

Como resultado, todas las comprobaciones dentro de C se realizarán 4 veces, dentro de B - 3 veces, dentro de A - 2 veces.

Pero, ¿y si una alternativa adecuada está en el segundo o tercer lugar de la lista? Entonces, una de las etapas predicadas fallará. Su posición en el texto retrocederá y otro predicado comenzará a ejecutarse.

Al analizar las razones por las que la aplicación se congela, a menudo nos topamos con el rastro de SynPred ejecutado varios miles de veces. SynPred s son especialmente problemáticos en las reglas recursivas. Lamentablemente, SQL es recursivo por naturaleza. La capacidad de usar subconsultas en casi todas partes tiene su precio. Sin embargo, es posible manipular la regla para hacer que un predicado desaparezca.

SynPred daña el rendimiento. En algún momento, su número fue puesto bajo un estricto control. Pero el problema es que cuando escribes código gramatical, SynPred puede parecerte poco obvio. Más aún, cambiar una regla puede hacer que SynPred aparezca en otra regla, y eso hace que el control sobre ellos sea prácticamente imposible.

Creamos una simple expresión regular herramienta para controlar el número de predicados ejecutados por la tarea MSBuild especial . Si la cantidad de predicados no coincidía con la cantidad especificada en un archivo, la tarea fallaba inmediatamente en la compilación y advertía sobre un error.

Al ver el error, un desarrollador debe volver a escribir el código de la regla varias veces para eliminar los predicados redundantes. Si uno no puede evitar los predicados, el desarrollador lo agregaría a un archivo especial que llama la atención adicional para la revisión.

En raras ocasiones, incluso escribimos nuestros predicados usando C# solo para evitar los generados por ANTLR. Por suerte, este método también existe.

Herencia gramatical

Cuando hay cambios en nuestros DBMS admitidos, debemos cumplir con ellos en nuestras herramientas. El soporte para construcciones de sintaxis gramatical es siempre un punto de partida.

Creamos una gramática especial para cada dialecto SQL. Permite cierta repetición de código, pero es más fácil que tratar de encontrar lo que tienen en común.

Optamos por escribir nuestro propio preprocesador de gramática ANTLR que hace la herencia de gramática.

También se hizo evidente que necesitábamos un mecanismo para el polimorfismo:la capacidad no solo de redefinir la regla en el descendiente sino también de llamar a la básica. También nos gustaría controlar la posición al llamar a la regla base.

Las herramientas son definitivamente una ventaja cuando comparamos ANTLR con otras herramientas de reconocimiento de idiomas, Visual Studio y ANTLRWorks. Y no quiere perder esta ventaja al implementar la herencia. La solución fue especificar la gramática básica en una gramática heredada en un formato de comentario ANTLR. Para las herramientas ANTLR es solo un comentario, pero podemos extraer toda la información requerida de él.

Escribimos una tarea de MsBuild que se incrustó en el sistema de compilación completa como acción previa a la compilación. La tarea era hacer el trabajo de un preprocesador para la gramática ANTLR generando la gramática resultante a partir de su base y sus pares heredados. La gramática resultante fue procesada por la propia ANTLR.

Postprocesamiento ANTLR

En muchos lenguajes de programación, las palabras clave no se pueden usar como nombres de sujeto. Puede haber de 800 a 3000 palabras clave en SQL dependiendo del dialecto. La mayoría de ellos están vinculados al contexto dentro de las bases de datos. Por lo tanto, prohibirlos como nombres de objetos frustraría a los usuarios. Es por eso que SQL tiene palabras clave reservadas y no reservadas.

No puede nombrar su objeto como palabra reservada (SELECCIONAR, DE, etc.) sin citarlo, pero puede hacerlo con una palabra no reservada (CONVERSACIÓN, DISPONIBILIDAD, etc.). Esta interacción dificulta el desarrollo del analizador.

Durante el análisis léxico, se desconoce el contexto, pero un analizador ya requiere números diferentes para el identificador y la palabra clave. Es por eso que agregamos otro procesamiento posterior al analizador ANTLR. Reemplazó todas las comprobaciones obvias de identificadores con la llamada a un método especial.

Este método tiene un control más detallado. Si la entrada llama a un identificador y esperamos que el identificador se cumpla en adelante, todo está bien. Pero si una palabra no reservada es una entrada, debemos verificarla dos veces. Esta verificación adicional revisa la búsqueda de sucursales en el contexto actual donde esta palabra clave no reservada puede ser una palabra clave. Si no existen tales sucursales, se puede utilizar como identificador.

Técnicamente, este problema podría resolverse por medio de ANTLR, pero esta decisión no es óptima. La forma de ANTLR es crear una regla que enumere todas las palabras clave no reservadas y un identificador de lexema. Más adelante servirá una regla especial en lugar de un identificador de lexema. Esta solución hace que un desarrollador no olvide agregar la palabra clave donde se usa y en la regla especial. Además, optimiza el tiempo empleado.

Errores en el análisis de sintaxis sin árboles

El árbol de sintaxis suele ser el resultado del trabajo del analizador. Es una estructura de datos que refleja el texto del programa a través de la gramática formal. Si desea implementar un editor de código con autocompletado de idioma, lo más probable es que obtenga el siguiente algoritmo:

  1. Analice el texto en el editor. Luego obtienes un árbol de sintaxis.
  2. Encuentre un nodo debajo del carro y compárelo con la gramática.
  3. Descubra qué palabras clave y tipos de objetos estarán disponibles en el Punto.

En este caso, la gramática es fácil de imaginar como un gráfico o una máquina de estados.

Desafortunadamente, solo la tercera versión de ANTLR estaba disponible cuando el IDE de dbForge había comenzado su desarrollo. Sin embargo, no era tan ágil y, aunque podía decirle a ANTLR cómo construir un árbol, el uso no era sencillo.

Además, muchos artículos sobre este tema sugirieron usar el mecanismo de "acciones" para ejecutar código cuando el analizador pasaba por la regla. Este mecanismo es muy útil, pero ha generado problemas arquitectónicos y ha hecho que admitir nuevas funciones sea más complejo.

La cuestión es que un solo archivo de gramática comenzó a acumular "acciones" debido a la gran cantidad de funcionalidades que deberían haberse distribuido a diferentes compilaciones. Logramos distribuir controladores de acciones a diferentes compilaciones y hacer una variación furtiva del patrón de notificador de suscriptor para esa medida.

ANTLR3 funciona 6 veces más rápido que ANTLR4 según nuestras mediciones. Además, el árbol de sintaxis para scripts grandes podía consumir demasiada RAM, lo que no era una buena noticia, por lo que necesitábamos operar dentro del espacio de direcciones de 32 bits de Visual Studio y SQL Management Studio.

Procesamiento posterior del analizador ANTLR

Cuando se trabaja con cadenas, uno de los momentos más críticos es la etapa de análisis léxico donde dividimos el guión en palabras separadas.

ANTLR toma como entrada la gramática que especifica el idioma y genera un analizador en uno de los idiomas disponibles. En algún momento, el analizador generado creció hasta tal punto que teníamos miedo de depurarlo. Si presiona F11 (paso a paso) durante la depuración y va al archivo del analizador, Visual Studio simplemente se bloqueará.

Resultó que falló debido a una excepción OutOfMemory al analizar el archivo del analizador. Este archivo contenía más de 200.000 líneas de código.

Pero depurar el analizador es una parte esencial del proceso de trabajo y no se puede omitir. Con la ayuda de las clases parciales de C#, analizamos el analizador generado mediante expresiones regulares y lo dividimos en unos pocos archivos. Visual Studio funcionó perfectamente con él.

Análisis léxico sin subcadena antes de Span API

La tarea principal del análisis léxico es la clasificación:definir los límites de las palabras y cotejarlas con un diccionario. Si se encuentra la palabra, el lexer devolvería su índice. Si no, la palabra se considera un identificador de objeto. Esta es una descripción simplificada del algoritmo.

Lexing de fondo durante la apertura de archivos

El resaltado de sintaxis se basa en el análisis léxico. Esta operación suele llevar mucho más tiempo en comparación con la lectura de texto del disco. ¿Cuál es el truco? En un hilo, el texto se lee desde el archivo, mientras que el análisis léxico se realiza en un hilo diferente.

El lexer lee el texto fila por fila. Si solicita una fila que no existe, se detendrá y esperará.

BlockingCollection de BCL funciona de manera similar y el algoritmo comprende una aplicación típica de un patrón productor-consumidor simultáneo. El editor que trabaja en el hilo principal solicita datos sobre la primera línea resaltada y, si no está disponible, se detendrá y esperará. En nuestro editor, hemos usado el patrón productor-consumidor y la colección de bloques dos veces:

  1. Leer de un archivo es un Productor, mientras que lexer es un Consumidor.
  2. Lexer ya es Productor y el editor de texto es Consumidor.

Este conjunto de trucos nos permite acortar significativamente el tiempo dedicado a abrir archivos grandes. La primera página del documento se muestra muy rápidamente, sin embargo, el documento puede bloquearse si los usuarios intentan pasar al final del archivo en los primeros segundos. Sucede porque el lector de fondo y el lexer necesitan llegar al final del documento. Sin embargo, si el trabajo del usuario se mueve lentamente desde el principio del documento hasta el final, no habrá bloqueos perceptibles.

Optimización ambigua:análisis léxico parcial

El análisis sintáctico se suele dividir en dos niveles:

  • el flujo de caracteres de entrada se procesa para obtener lexemas (tokens) en función de las reglas del idioma; esto se denomina análisis léxico
  • el analizador consume el flujo de tokens comprobándolo de acuerdo con las reglas gramaticales formales y, a menudo, crea un árbol de sintaxis.

El procesamiento de cadenas es una operación costosa. Para optimizarlo, decidimos no realizar un análisis léxico completo del texto cada vez, sino volver a analizar solo la parte que se modificó. Pero, ¿cómo lidiar con construcciones multilínea como comentarios de bloque o líneas? Almacenamos un estado de fin de línea para cada línea:"sin tokens multilínea" =0, "el comienzo de un comentario de bloque" =1, "el comienzo de un literal de cadena multilínea" =2. El análisis léxico comienza desde la sección modificada y finaliza cuando el estado de fin de línea es igual al almacenado.

Hubo un problema con esta solución:es extremadamente inconveniente monitorear los números de línea en dichas estructuras, mientras que el número de línea es un atributo requerido de un token ANTLR porque cuando se inserta o elimina una línea, el número de la siguiente línea debe actualizarse en consecuencia. Lo solucionamos estableciendo un número de línea inmediatamente, antes de entregar el token al analizador. Las pruebas que realizamos más tarde han demostrado que el rendimiento mejoró en un 15-25%. La mejora real fue aún mayor.

La cantidad de RAM requerida para todo esto resultó ser mucho más de lo que esperábamos. Un token ANTLR constaba de:un punto inicial:8 bytes, un punto final:8 bytes, un enlace al texto de la palabra:4 u 8 bytes (sin mencionar la cadena en sí), un enlace al texto del documento:4 u 8 bytes. y un tipo de token:4 bytes.

Entonces, ¿qué podemos concluir? Nos enfocamos en el rendimiento y obtuvimos un consumo excesivo de RAM en un lugar que no esperábamos. No asumimos que esto sucedería porque intentamos usar estructuras ligeras en lugar de clases. Al reemplazarlos con objetos pesados, buscamos a sabiendas gastos adicionales de memoria para obtener un mejor rendimiento. Afortunadamente, esto nos enseñó una lección importante, por lo que ahora cada optimización de rendimiento termina con el consumo de memoria de perfiles y viceversa.

Esta es una historia con moraleja. Algunas características comenzaron a funcionar casi instantáneamente y otras un poco más rápido. Después de todo, sería imposible realizar el truco del análisis léxico de fondo si no hubiera un objeto donde uno de los subprocesos pudiera almacenar tokens.

Todos los demás problemas se desarrollan en el contexto del desarrollo de escritorio en la pila .NET.

El problema de los 32 bits

Algunos usuarios optan por utilizar versiones independientes de nuestros productos. Otros se limitan a trabajar dentro de Visual Studio y SQL Server Management Studio. Muchas extensiones se desarrollan para ellos. Una de estas extensiones es SQL Complete. Para aclarar, proporciona más poderes y funciones que el SSMS de finalización de código estándar y VS para SQL.

El análisis de SQL es un proceso muy costoso, tanto en términos de recursos de CPU como de RAM. Para solicitar la lista de objetos en los scripts de usuario, sin llamadas innecesarias al servidor, almacenamos el caché de objetos en la RAM. A menudo, no ocupa mucho espacio, pero algunos de nuestros usuarios tienen bases de datos que contienen hasta un cuarto de millón de objetos.

Trabajar con SQL es bastante diferente de trabajar con otros lenguajes. En C#, prácticamente no hay archivos ni siquiera con mil líneas de código. Mientras tanto, en SQL, un desarrollador puede trabajar con un volcado de base de datos que consta de varios millones de líneas de código. No hay nada inusual en ello.

DLL-Infierno dentro de VS

Hay una herramienta útil para desarrollar complementos en .NET Framework, es un dominio de aplicación. Todo se realiza de forma aislada. Es posible descargar. En su mayor parte, la implementación de extensiones es, quizás, la razón principal por la que se introdujeron los dominios de aplicación.

Además, está MAF Framework, que fue diseñado por MS para resolver el problema de crear complementos para el programa. Aísla estos complementos hasta el punto de que puede enviarlos a un proceso separado y hacerse cargo de todas las comunicaciones. Hablando con franqueza, esta solución es demasiado engorrosa y no ha ganado mucha popularidad.

Desafortunadamente, Microsoft Visual Studio y SQL Server Management Studio se basaron en él, implementan el sistema de extensión de manera diferente. Simplifica el acceso a las aplicaciones de alojamiento de complementos, pero los obliga a encajar dentro de un proceso y dominio con otro.

Como cualquier otra aplicación del siglo XXI, la nuestra tiene muchas dependencias. La mayoría de ellas son bibliotecas conocidas, probadas y populares en el mundo .NET.

Hacer mensajes dentro de un candado

No es muy conocido que .NET Framework bombee la Cola de mensajes de Windows dentro de cada WaitHandle. Para colocarlo dentro de cada bloqueo, se puede llamar a cualquier controlador de cualquier evento en una aplicación si este bloqueo tiene tiempo para cambiar al modo kernel y no se libera durante la fase de espera de giro.

Esto puede resultar en la reentrada en algunos lugares muy inesperados. Algunas veces dio lugar a problemas como "La colección se modificó durante la enumeración" y varias ArgumentOutOfRangeException.

Agregar un ensamblado a una solución usando SQL

Cuando el proyecto crece, la tarea de agregar ensamblajes, simple al principio, se convierte en una docena de pasos complicados. Una vez, tuvimos que agregar una docena de ensamblajes diferentes a la solución, realizamos una gran refactorización. Se crearon cerca de 80 soluciones, incluidas las de producto y de prueba, basadas en alrededor de 300 proyectos .NET.

Basados ​​en soluciones de productos, escribimos archivos de configuración de Inno. Incluían listas de ensamblajes empaquetados en la instalación que el usuario descargó. El algoritmo para agregar un proyecto fue el siguiente:

  1. Cree un nuevo proyecto.
  2. Agréguele un certificado. Configure la etiqueta de la compilación.
  3. Añadir un archivo de versión.
  4. Reconfigurar las rutas por donde va el proyecto.
  5. Cambie el nombre de la carpeta para que coincida con la especificación interna.
  6. Agregue el proyecto a la solución una vez más.
  7. Agregue un par de ensamblajes a los que todos los proyectos necesitan vínculos.
  8. Agregue la compilación a todas las soluciones necesarias:prueba y producto.
  9. Para todas las soluciones de productos, agregue los ensamblajes a la instalación.

Estos 9 pasos tuvieron que repetirse unas 10 veces. Los pasos 8 y 9 no son tan triviales y es fácil olvidarse de agregar compilaciones en todas partes.

Ante una tarea tan grande y rutinaria, cualquier programador normal querría automatizarla. Eso es exactamente lo que queríamos hacer. Pero, ¿cómo indicamos qué soluciones e instalaciones exactamente agregar al proyecto recién creado? Hay tantos escenarios y lo que es más, es difícil predecir algunos de ellos.

Se nos ocurrió una idea loca. Las soluciones están conectadas con proyectos como muchos a muchos, proyectos con instalaciones de la misma manera, y SQL puede resolver exactamente el tipo de tareas que teníamos.

Creamos una aplicación de consola .Net Core que escanea todos los archivos .sln en la carpeta de origen, recupera la lista de proyectos de ellos con la ayuda de DotNet CLI y los coloca en la base de datos SQLite. El programa tiene algunos modos:

  • Nuevo:crea un proyecto y todas las carpetas necesarias, agrega un certificado, configura una etiqueta, agrega una versión, ensamblajes esenciales mínimos.
  • Add-Project:agrega el proyecto a todas las soluciones que satisfacen la consulta SQL que se proporcionará como uno de los parámetros. Para agregar el proyecto a la solución, el programa interno usa DotNet CLI.
  • Add-ISS:agrega el proyecto a todas las instalaciones que satisfacen consultas SQL.

Aunque la idea de indicar la lista de soluciones a través de la consulta SQL puede parecer engorrosa, cerró por completo todos los casos existentes y muy probablemente cualquier caso posible en el futuro.

Permítanme demostrar el escenario. Crear un proyecto “A” y agréguelo a todas las soluciones donde los proyectos “B” se usa:

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

Un problema con LiteDB

Hace un par de años, recibimos la tarea de desarrollar una función en segundo plano para guardar documentos de usuario. Tenía dos flujos de aplicación principales:la capacidad de cerrar instantáneamente el IDE y salir, y al volver a comenzar desde donde lo dejó, y la capacidad de restaurar en situaciones urgentes como apagones o fallas del programa.

Para implementar esta tarea, era necesario guardar el contenido de los archivos en algún lugar al costado, y hacerlo con frecuencia y rapidez. Además del contenido, fue necesario guardar algunos metadatos, lo que hizo que el almacenamiento directo en el sistema de archivos fuera un inconveniente.

En ese momento, encontramos la biblioteca LiteDB, que nos impresionó por su simplicidad y rendimiento. LiteDB es una base de datos integrada ligera y rápida, que se escribió completamente en C#. La velocidad y la simplicidad general nos convencieron.

En el transcurso del proceso de desarrollo, todo el equipo quedó satisfecho con el trabajo con LiteDB. Sin embargo, los principales problemas comenzaron después del lanzamiento.

La documentación oficial garantizaba que la base de datos garantizaba un trabajo adecuado con acceso simultáneo desde varios subprocesos y varios procesos. Las pruebas sintéticas agresivas mostraron que la base de datos no funciona correctamente en un entorno de subprocesos múltiples.

Para solucionar rápidamente el problema, sincronizamos los procesos con la ayuda del interproceso ReadWriteLock autoescrito. Ahora, después de casi tres años, LiteDB funciona mucho mejor.

Lista de cadenas de transmisión

Este problema es el opuesto al caso del análisis léxico parcial. Cuando trabajamos con un texto, es más conveniente trabajar con él como una lista de cadenas. Las cadenas se pueden solicitar en orden aleatorio, pero todavía está presente cierta densidad de acceso a la memoria. En algún momento, fue necesario ejecutar varias tareas para procesar archivos muy grandes sin cargar la memoria por completo. La idea era la siguiente:

  1. Para leer el archivo línea por línea. Recuerde las compensaciones en el archivo.
  2. A pedido, emita la siguiente línea, establezca un desplazamiento requerido y devuelva los datos.

La tarea principal está completa. Esta estructura no ocupa mucho espacio en comparación con el tamaño del archivo. En la etapa de prueba, verificamos minuciosamente la huella de la memoria en busca de archivos grandes y muy grandes. Los archivos grandes se procesaron durante mucho tiempo y los pequeños se procesarán de inmediato.

No había ninguna referencia para comprobar el tiempo de ejecución . La RAM se llama memoria de acceso aleatorio:es su ventaja competitiva sobre SSD y especialmente sobre HDD. Estos controladores comienzan a funcionar mal para el acceso aleatorio. Resultó que este enfoque ralentizaba el trabajo casi 40 veces, en comparación con la carga completa de un archivo en la memoria. Además, leemos el archivo 2,5 -10 veces completas dependiendo del contexto.

La solución fue simple y la mejora fue suficiente para que la operación solo tomara un poco más de tiempo que cuando el archivo está completamente cargado en la memoria.

Asimismo, el consumo de RAM también fue insignificante. Encontramos inspiración en el principio de cargar datos de la RAM en un procesador de caché. Cuando accede a un elemento de matriz, el procesador copia docenas de elementos vecinos en su caché porque los elementos necesarios suelen estar cerca.

Muchas estructuras de datos utilizan esta optimización del procesador para obtener el máximo rendimiento. Es por esta peculiaridad que el acceso aleatorio a los elementos de la matriz es mucho más lento que el acceso secuencial. Implementamos un mecanismo similar:leímos un conjunto de mil cadenas y recordamos sus compensaciones. Cuando accedemos a la cadena 1001, soltamos las primeras 500 cadenas y cargamos las siguientes 500. En caso de que necesitemos alguna de las primeras 500 líneas, vamos a ella por separado, porque ya tenemos el desplazamiento.

El programador no necesariamente necesita formular y verificar cuidadosamente los requisitos no funcionales. Como resultado, recordamos para casos futuros que necesitamos trabajar secuencialmente con memoria persistente.

Análisis de las excepciones

Puede recopilar datos de actividad del usuario fácilmente en la web. Sin embargo, no es el caso con el análisis de aplicaciones de escritorio. No existe tal herramienta que sea capaz de brindar un conjunto increíble de métricas y herramientas de visualización como Google Analytics. ¿Por qué? Aquí mis suposiciones son:

  1. A lo largo de la mayor parte de la historia del desarrollo de aplicaciones de escritorio, no tuvieron acceso estable y permanente a la Web.
  2. Existen muchas herramientas de desarrollo para aplicaciones de escritorio. Por lo tanto, es imposible crear una herramienta de recopilación de datos de usuario multipropósito para todos los marcos y tecnologías de interfaz de usuario.

Un aspecto clave de la recopilación de datos es realizar un seguimiento de las excepciones. Por ejemplo, recopilamos datos sobre accidentes. Anteriormente, nuestros usuarios tenían que escribir ellos mismos al correo electrónico de atención al cliente, agregando un Stack Trace de un error, que se copiaba desde una ventana especial de la aplicación. Pocos usuarios siguieron todos estos pasos. Los datos recopilados se anonimizan por completo, lo que nos priva de la oportunidad de conocer los pasos de reproducción o cualquier otra información del usuario.

Por otro lado, los datos de error están en la base de datos de Postgres, y esto allana el camino para una verificación instantánea de docenas de hipótesis. Puede obtener las respuestas de inmediato simplemente haciendo consultas SQL a la base de datos. A menudo no está claro a partir de una sola pila o tipo de excepción cómo ocurrió la excepción, por eso toda esta información es fundamental para estudiar el problema.

Además de eso, tiene la oportunidad de analizar todos los datos recopilados y encontrar los módulos y clases más problemáticos. Basándose en los resultados del análisis, puede planificar la refactorización o pruebas adicionales para cubrir estas partes del programa.

Servicio de decodificación de pilas

Las compilaciones .NET contienen código IL, que se puede volver a convertir fácilmente en código C#, preciso para el operador, utilizando varios programas especiales. Una de las formas de proteger el código del programa es su ofuscación. Los programas se pueden renombrar; se pueden reemplazar métodos, variables y clases; el código se puede reemplazar con su equivalente, pero es realmente incomprensible.

La necesidad de ofuscar el código fuente aparece cuando distribuye su producto de una manera que sugiere que el usuario obtiene las compilaciones de su aplicación. Las aplicaciones de escritorio son esos casos. Todas las compilaciones, incluidas las compilaciones intermedias para evaluadores, están cuidadosamente ofuscadas.

Nuestra Unidad de control de calidad utiliza herramientas de descodificación del desarrollador ofuscador. Para comenzar a decodificar, deben ejecutar la aplicación, encontrar mapas de desofuscación publicados por CI para una compilación específica e insertar la pila de excepciones en el campo de entrada.

Las diferentes versiones y editores se ofuscaron de manera diferente, lo que dificultó que un desarrollador estudiara el problema o incluso podría ponerlo en el camino equivocado. Era obvio que este proceso tenía que ser automatizado.

El formato del mapa de desofuscación resultó ser bastante sencillo. Lo desanalizamos fácilmente y escribimos un programa de decodificación de pila. Poco antes de eso, se desarrolló una interfaz de usuario web para representar excepciones por versiones de productos y agruparlas por pila. Era un sitio web .NET Core con una base de datos en SQLite.

SQLite es una buena herramienta para pequeñas soluciones. Intentamos poner mapas de desofuscación allí también. Cada compilación generó aproximadamente 500 000 pares de cifrado y descifrado. SQLite no pudo manejar una tasa de inserción tan agresiva.

Si bien los datos de una compilación se insertaron en la base de datos, se agregaron dos más a la cola. No mucho antes de ese problema, estaba escuchando un informe sobre Clickhouse y estaba ansioso por probarlo. Resultó excelente, la tasa de inserción se aceleró más de 200 veces.

Dicho esto, la decodificación de pila (lectura de la base de datos) se ralentizó casi 50 veces, pero como cada pila tardaba menos de 1 ms, no era rentable dedicar tiempo a estudiar este problema.

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.

Conclusió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.