Los usuarios activos de Telegram, especialmente aquellos que están suscritos a Pavel Durov, probablemente escucharon algo sobre el hecho de que Telegram celebró un concurso para desarrolladores de iOS, Android y JavaScript, así como para diseñadores, en estos sitios de Internet suyos. A pesar del hecho de que fue un evento bastante épico con la distribución de premios sólidos (uno de los participantes recibió $ 50k por el primer lugar, después de haber escrito la aplicación de Android más rápida y fácil), de alguna manera escribieron poco al respecto, al menos en Runet. Mi primera publicación intentará arreglar la situación.

Como soy un desarrollador de JavaScript de pila completa (para ser exactos, un desarrollador de TypeScript), decidí probarme a mí mismo. Manila no es solo un fondo de premios, sino también el formato en sí mismo: esta no es una competencia de programación donde la abstracción y la velocidad de pensamiento son importantes. Aquí, todo en el complejo era importante: experiencia, velocidad de desarrollo a mediano plazo, gusto por los problemas de la interfaz de usuario, conocimiento de la informática en general y autocrítica. Según los términos del concurso, era necesario desarrollar una biblioteca para mostrar gráficos para una de las plataformas: iOS, Android o Web.

Los desarrolladores de diferentes plataformas no competían entre sí, y cada plataforma tenía sus propios ganadores. Los criterios principales fueron: velocidad de trabajo (incluso en dispositivos más antiguos), coherencia con el diseño, animación fluida y tamaño de aplicación mínimo. Ya no se podían usar las soluciones y bibliotecas existentes, todo tenía que escribirse desde cero.
Antes de eso, participé en concursos para desarrolladores, donde no se asignaron más de 5 horas para todas las tareas, estas horas tuvieron que gastarse en un tremendo estrés. A pesar de que el Telegram no requirió tanta tensión para completar la tarea, es uno de los concursos más difíciles en los que tuve que participar. La tarea aparentemente sin complicaciones resultó ser tan amplia que si me hubieran pagado, podría cortar estos "gráficos" durante meses, tratando de encontrar un compromiso entre el rendimiento del código y su armonía arquitectónica. Ayudó a que se
asignaran tres (
upd: dos, gracias a
vlad2711 por la enmienda) semanas para la solución. Algunos de los rivales se despidieron específicamente para dedicar más tiempo al concurso, y decidí combinar el desarrollo del concurso por las tardes y los fines de semana con el trabajo en "
Ontanta " como de costumbre.
LONA versus SVG
El problema arquitectónico más importante que enfrentamos a todos fue la elección de una herramienta de representación gráfica. Actualmente, los estándares web nos ofrecen dos enfoques: a través de la generación de gráficos svg sobre la marcha y el buen lienzo antiguo. Aquí están los pros y los contras de cada uno.
Lienzo
+ Versatilidad absoluta: al tener la capacidad de cambiar el color de cualquier píxel en el lienzo, puede dibujar lo que quiera.
+ [Potencial] Alto rendimiento: si puede preparar el lienzo, puede mostrar un buen rendimiento. Sería genial usar webgl, pero su soporte en teléfonos inteligentes es deficiente.
- Todos los cálculos y todo el renderizado a mano: a diferencia de SVG, donde los puntos intermedios de la polilínea se pueden configurar una vez, y luego puede manipular la vista para mover la "cámara" a lo largo de las secciones de la polilínea, con el lienzo todo es más complicado: no hay "cámaras" aquí, hay solo coordenadas desde la esquina superior izquierda; Si necesita "mover" el área de visualización actual del gráfico, debe volver a calcular todas las coordenadas de todos sus puntos en relación con la nueva posición del área de visualización. En otras palabras, el cuadro de vista, que en svg está listo para usar, debe implementarse en el lienzo manualmente.
- Toda la animación es manual: según el párrafo anterior, todas las animaciones posibles se realizan recalculando las coordenadas, los valores de color y transparencia y redibujando toda la escena N-ésima cantidad de veces por segundo, y cuantas más veces sea posible contar y volver a dibujar la escena, más suave será la animación.
Svg
+ Dibujo simple: solo agregue las líneas necesarias, las formas y más a SVG una vez, manipulando los parámetros de ventana gráfica, color y transparencia, proporcione la navegación del gráfico.
+ Implementación simple de animaciones: de nuevo, según el párrafo anterior, es suficiente Ne para especificar nuevos valores para el número de caja de vista, color y transparencia por segundo, y la imagen se volverá a dibujar, el navegador se ocupará de esto. Además, no olvide que las formas y las primitivas en SVG se pueden diseñar en CSS, por lo que se pueden animar usando animaciones CSS3, lo que abre las más amplias posibilidades para obtener animaciones geniales con un mínimo esfuerzo.
+ Buen rendimiento por defecto: si puede poner fácilmente algo lento y consumir cientos de recursos en el lienzo, el resultado basado en SVG siempre se verá bastante ligero, decente y suave.
Pero hay una otra cara de la moneda.
- Oportunidades modestas para la optimización: dado que no estamos dibujando svg, sino el navegador, entonces es imposible controlar este proceso; si desea aumentar el rendimiento, por ejemplo, al almacenar en caché elementos dibujados individuales, no puede hacerlo de ninguna manera. Lo más probable es que esto ya lo haga el navegador, pero no podemos estar seguros hasta el final.
- Herramientas limitadas: en SVG ya no controlamos cada píxel del lienzo, sino que pensamos y codificamos dentro del marco de las primitivas vectoriales. Sin embargo, para esta tarea, este es un inconveniente insignificante, que impone algunas restricciones, nuevamente insignificantes, en el contexto de la tarea de competencia.
Nunca tuve que preocuparme por elegir una herramienta, porque tengo un rasgo de carácter desagradable: soy maximalista y solía usar solo mi herramienta favorita en mi trabajo. Sucedió que desde mis días de estudiante, cuando me estaba divirtiendo con DirectDraw, mi herramienta favorita siempre fue un lienzo en el que "haz lo que quieras". Y el lienzo para resolver un problema competitivo realmente resultó ser bueno, pero realmente jugó en mis manos solo una de sus ventajas: las posibilidades más amplias para las optimizaciones, ya que el criterio principal todavía era el rendimiento de la aplicación.
Un buen código no es bueno
La tarea es clara: debe dibujar puntos en el lienzo en el lugar correcto y en el momento adecuado. Queda por escribir el código. Una vez más, fue necesario elegir, esta vez entre escribir un código compacto productivo con un "paño" en un estilo de procedimiento o no muy productivo e incluso menos compacto en mi orientado a objetos favorito. Probablemente ya haya adivinado que elegí la segunda opción, sazonándola con otra de mis favoritas: TypeScript.
Y esta elección no fue muy correcta. Debido al uso de abstracciones y encapsulaciones, no siempre es posible guardar, transmitir y reutilizar resultados de cálculo intermedios, lo que afecta el rendimiento de manera deficiente. Y debido al uso generalizado de esto, sin el cual OOP en JS es imposible, el código está mal minimizado, mientras que el tamaño también importaba.
Es hora de dar un enlace al github:
github.com/native-elements/telechart . Si está interesado, le recomiendo que preste atención al historial de confirmaciones; mantiene un recuerdo de pruebas de optimización e intentos fallidos de exprimir un par de marcos de representación adicionales por segundo.
Bueno, en la competencia no me llevé el premio. Y el problema, como sucede a menudo con nosotros los programadores, resultó no ser una experiencia insuficiente, ingenio rápido o velocidad, sino una autocrítica insuficiente: el hecho de que logré hacerlo funciona y se ve como en la imagen, estaba satisfecho, pero En cuanto a los frenos de renderizado, pensé que hice todo lo que pude, el resto probablemente hizo lo mismo. Me da vergüenza hablar de esto, pero estaba seguro de que tomaría el primer o segundo lugar. De hecho, resultó que escribí un programa de frenado y con errores, no el peor, pero lejos de ser el mejor. Cuando vi el trabajo de otros desarrolladores, me di cuenta de que no tenía ninguna posibilidad y que solo podía morderme los codos. Si fuera imparcial con mi trabajo, estaría involucrado en la productividad, la parte más importante de la tarea de competencia.
Una de las lecciones más valiosas en mi vida profesional que no me canso de aprender es que un buen ingeniero, a diferencia de, por ejemplo, un artista, está obligado a evaluar objetivamente la calidad de su trabajo, descartando la confianza en sí mismo, porque el resultado de su trabajo no solo debe agradar la vista. pero debería funcionar correctamente y bien.
Esta fue la primera etapa de la competencia. Los ganadores fueron generosamente recompensados. Para mi alegría indescriptible, la historia no terminó allí, porque se anunció la segunda etapa:
Era necesario refinar su oficio, en solo una semana implementando tipos adicionales de cartas. Enseguida les mostraré lo que sucedió y a continuación les contaré cómo sucedió.
En mi caso, antes de agregar una nueva funcionalidad, tenía que entender el rendimiento de la anterior. El primer problema que resolví es
Animación espasmódicaIncluso si tiene suficiente potencia para producir 60 cuadros por segundo, la animación no será uniforme si la posición del elemento o su transparencia no está determinada por el tiempo transcurrido desde el comienzo de la animación. Esto se debe a intervalos de tiempo desiguales entre ticks: por ejemplo, un tick funcionó después de 10 ms, y el segundo después de 40, mientras que para el primer y segundo ticks el objeto se movió a la izquierda 1 píxel, es decir, su velocidad de movimiento flota constantemente, visualmente parece un "tic". En otras palabras, debes hacer algo mal:
let left = 10, interval = setInterval(() => { left += 1 if (left >= 90) { clearInterval(interval) } }, 10)
Y entonces:
let left = 10, startLeft = 10, targetLeft = 90, startTime = Date.now(), duration = 1000, interval = setInterval(() => { left = startLeft + (targetLeft - startLeft) * (Date.now() - startTime) / duration if (left >= targetLeft) { left = targetLeft clearInterval(interval) } })
Dado que hay muchos parámetros animados en el código, filmé una
clase universal que facilita la tarea y también agrega un toque a la animación. Es bastante fácil de usar:
let left = Telemation.create(10, 90, 1000) … drawVerticalLine(left.value)
Entonces entra en juego la regla de 60 fps. Los jugadores de PC me entenderán: para que una animación se vea perfecta, debe representarse a una velocidad de al menos 60 fps. En consecuencia, cada renderizado del marco no debería tomar más de 1/60 de segundo. Esto requiere un hardware potente y un buen código.
Investigaciones posteriores mostraron que
Pintar lienzo se ralentiza si hay elementos html encima del lienzo .
Inicialmente, utilicé elementos html "vacíos" para implementar el control sobre la ventana gráfica actual:
Estos elementos se colocaron en la parte superior del lienzo y, a pesar del hecho de que no tenían contenido, se usaron solo para rastrear eventos del mouse, como resultado de los experimentos resultó que su sola presencia reduce el rendimiento del renderizado. Al eliminarlos y complicar un poco la lógica de determinar eventos para controlar el área de visualización, aumenté la velocidad de renderizado del marco.
Quedaba por sacar el último clavo de la tapa del ataúd de la actuación: lo hice
Almacenamiento en caché de minimapaAntes de esto, para las líneas del minimapa se dibujaban cada cuadro nuevamente. Esta es una operación costosa porque muestra la programación completa del año (365 puntos por línea). La solución obvia, que era demasiado flojo para implementar desde el principio, era dibujar las líneas del gráfico para el minimapa una vez, guardar el resultado en la caché y usar esta caché en el futuro. Después de esta optimización, el rendimiento de la aplicación ya no es vergonzoso.
Que sigue
Todavía hubo muchas luchas exitosas y no muy importantes por el rendimiento: intentos de almacenar en caché los resultados de los cálculos de coordenadas, experimentos con los parámetros de la línea CanvasRenderingContext2D lineJoin (inglete más rápido), pero no son tan interesantes, porque no dieron una ganancia de rendimiento notable o no lo dieron en absoluto.
De los ocho días, pasé cinco en acelerar el código, y solo tres en completar la nueva funcionalidad. Sí, me tomó solo tres días agregar nuevos tipos de gráficos, y aquí OOP resultó ser muy útil, con lo que la base de código aumentó ligeramente. No tuve tiempo suficiente para completar la tarea de bonificación (+5 gráficos adicionales). Creo que esos cinco días que pasé en eliminar las consecuencias de mi confianza en mí mismo, podría dedicarlos a resolver el problema de los bonos.
Sin embargo, mi trabajo arrojó el resultado: cuarto lugar y un premio de "consolación" de mil dólares:
Por cierto, la competencia continuó, pero sin mí.
Estoy satisfecho con la participación: además de ser una aventura interesante y interesante, obtuve una buena experiencia profesional y una lección de vida.
Además, utilicé esta biblioteca en el desarrollo de nuestro rastreador de tiempo corporativo, del que también planeo hablar en un futuro cercano.
Para el debate, propongo la siguiente pregunta: ¿por qué Telegram necesita todo esto? Creo que por un dinero adecuado, Telegram recibirá la mejor biblioteca del mundo para mostrar gráficos: el mejor resultado de cientos de intentos de hacerlo mejor que otros. El principio competitivo le permite obtener un nivel de calidad tan alto que nadie puede hacerlo por pedido y sin dinero.
Y algunos enlaces: