Как автоматизировать переводы во Frontend приложении

В текущем мире очень многие сайты имеют поддержку многих языков, в большинстве случаев это происходит с помощью i18n npm пакета. Чаще всего переводы хранятся в .js, .json файлах и очень часто есть языки для которых в компании нет переводчика, либо же он не предполагался в целом, и в таком случае приходит на помощь разработчик с Google Translate. И вот однажды я столкнулся с тем, что на проекте оказалось очень много текстов и все их нужно было переводить вручную, что занимало достаточно много времени и я задумался о том, как это можно автоматизировать.

Моя идея была в том, чтобы написать функцию, в которую можно было бы передать языковой код и json объект с текстом на исходном языке, а на выходе получить переведенный json/js file. С помощью этой функции я мог бы в ci/cd Pipeline переводить все текста на проекте, в исходных текстах которых были изменения, либо же следить за текстами в рантайме и на лету переводить их.

И я нашел два относительно простых метода, для того чтобы это сделать, один из них платный, другой бесплатный.

Способ №1

Я думаю у многих в компании сейчас активно используются всякие ИИ чат-боты в духе chatgpt и тд, и у многих из них есть api, в которых вы можете получить api key и с помощью него отправлять ему запросы. В моем случае я использовал chatgpt от OpenAI и у них есть специальный npm пакет для node js — тык, с помощью которого можно взаимодействовать с chat ботом, вот и тут я подключаю эту библиотеку и отправляю запрос со следующим prompt — translate object values to ${language} ${JSON.stringify(text)}, где language — языковой код нужного вам языка(языковые коды — тык), text — наш объект с переводами.

translate.js

const { Configuration, OpenAIApi } = require('openai'); const fs = require('fs'); const configuration = new Configuration({   apiKey: process.env.OPENAI_API_KEY, });  const openai = new OpenAIApi(configuration); async function getTranslation(languageCode, text) {   try {     const result = await openai.createChatCompletion({       model: 'gpt-3.5-turbo',       messages: [{ role: 'user', content: `translate object values to ${languageCode} ${JSON.stringify(text)}` }],     });      return result.data.choices[0].message.content;   } catch (error) {     console.error(error);      return null;   } };  getTranslation('zh', {   label: 'Article about translation',   title: 'Easy way to implement i18n',   description: 'Try to translate this description', }).then((result) => {   fs.writeFileSync('cn.json', result); });  module.exports = getTranslation;

Как итог мы получаем переведенный json и можем записать его в нужный нам файл.

Способ №2

Абсолютно бесплатный метод, в котором я использую другую библиотеку — тык, совершенно случайно наткнулся на эту библиотеку на GitHub и решил попробовать ее, по мне работает неплохо — она использует Google Translate. Здесь я реализую аналогичную функцию, за исключением того, что тут я не могу передать весь объект для перевода, поэтому здесь перебирается все значения в объекте и по очереди переводятся.

translate.js

const translate = require('node-google-translate-skidz'); const fs = require('fs');  async function getTranslation(languageCode, text) {   const getTranslate = async (value) => {     const t = await translate({       text: value,       source: 'en',       target: languageCode,     });     return t.translation;   };    const processObject = async (obj) => {     for (const key in obj) {       if (obj.hasOwnProperty(key)) {         const value = obj[key];         if (typeof value === 'object') {           await processObject(value);         } else {           try {             const processedValue = value ? await getTranslate(value) : value;             obj[key] = processedValue;           } catch (error) {             console.error(error);             obj[key] = value;           }         }       }     }   };    const copy = JSON.parse(JSON.stringify(text));   await processObject(copy);    return JSON.stringify(copy, null, 2); };  getTranslation('zh', {   label: 'Article about translation',   title: 'Easy way to implement i18n',   description: 'Try to translate this description', }).then((result) => {   fs.writeFileSync('cn.json', result); });  module.exports = getTranslation;

На выходе также получаем переведенный json файл.

Итого

У меня получился вот такой скрипт, в который я могу прокинуть файл откуда прочитать перевод и куда его записать, также здесь я сделал проверку на то, .json файл это или .js и написал парсер для этого:

const fs = require('fs'); const getTranslation = require('./translate.js');  // .js file имеет вид export const ${languageCode} = { ... }, соответственно для того, чтобы получить контент можно взять все в {} и с помощью eval(я знаю что eval это плохо) преобразовать это в js объект const parseScriptFile = (filePath) => {   const fileData = fs.readFileSync(filePath, 'utf-8');   const jsonStartIndex = fileData.indexOf('{');   const jsonEndIndex = fileData.lastIndexOf('}');   const jsonObjectString = fileData.slice(jsonStartIndex, jsonEndIndex + 1);   const obj = eval(`(${jsonObjectString})`);   return obj; };  const languageCode = 'zh'; const filePath = './en.js'; const destinationFilePath = './zh.js'; // Проверяю .js это файл или .json и по разному их обрабатываю const text = filePath.includes('.json') ? require(filePath) : parseScriptFile(filePath);  getTranslation(languageCode, text).then((res) => {   if (filePath.includes('.json')) {     fs.writeFileSync(destinationFilePath, JSON.stringify(res, null, 2));   } else {     fs.writeFileSync(destinationFilePath, `export const ${languageCode} = ${JSON.stringify(res, null, 2)}`);   } });

Далее вы можете пропатчить эти скрипты в зависимости от ваших потребностей и получить автоматизированные и удобные переводы во Frontend.

Наслаждайтесь 🙂

Если статья показалась вам интересной, то у меня есть Тг-канал, где я пишу про новые технологии во фронте, делюсь хорошими книжками и интересными статьями других авторов.


ссылка на оригинал статьи https://habr.com/ru/articles/751366/

Протоколы в Python

В Python 3.8. появилась новая примечательная возможность — протоколы (protocols). Протоколы — это альтернатива абстрактным базовым классам (abstract base classes, ABC). Они позволяют пользоваться структурной подтипизацией (structural subtyping), то есть — осуществлять проверку совместимости классов исключительно на основе анализа их атрибутов и методов. В этом материале мы поговорим о протоколах в Python и разберём практические примеры работы с ними.

Типизация в Python

Начнём с рассмотрения системы типизации в Python. Это — динамически типизированный язык, то есть — типы выводятся во время выполнения программы, что ведёт, например, к тому, что следующий код нормально запустится и отработает:

def add(x, y):     return x + y  print(add(2, 3)) print(add("str1", "str2"))

Первый вызов функции add() приводит к сложению целых чисел и к возврату числа 5. Второй вызов производит конкатенацию строк с возвратом строки str1str2. То, что такое возможно, отличает Python от статически типизированных языков, вроде C++, где типы необходимо объявлять:

int add(int x, int y) {     return x + y; }  std::string add(std::string x, std::string y) {     return x + y; }  int main() {     std::cout<<add(2, 3);     std::cout << add("str1", "str2");     return 0; }

Сильная сторона статической типизации — возможность выявления ошибок при компиляции кода. А в динамически типизированных языках подобные ошибки проявляются лишь во время выполнения кода. Но, с другой стороны, динамическая типизация может способствовать ускорению создания прототипов программ, может помочь в проведении различных экспериментов. Это — одна из причин того, что Python обрёл огромную популярность.

Динамическую типизацию ещё называют «утиной типизацией«. Такое название этот термин получил от определения „утиного теста“: „Если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка“. В нашем случае это означает следующее: если объекты предлагают пользователю одни и те же атрибуты и методы, то с ними можно работать похожим образом. То есть, например, если есть пара схожих типов, объекты одного из них можно передавать функциям, рассчитанным на объекты другого типа.

Но такая гибкость даёт больше минусов, чем плюсов. Особенно — в больших программных проектах, которые ближе к профессиональным продуктам, чем к прототипам. В результате в мире программирования наблюдается тренд на статическую проверку типов. В Python это, например, включение в код подсказок типов, рассчитанных на применение статического анализатора типов mypy.

Подтипизация

Тут есть один интересный вопрос, намёк на который был дан выше, в разговоре об утиной типизации. Речь идёт о подтипизации. Предположим, имеется функция с такой сигнатурой: foo(x: X). Какие классы, помимо X, mypy позволит передать этой функции?

Обратите внимание на то, что мы сейчас говорим лишь о типизации и о подсказках типов. Ведь Python, как уже было сказано, это язык с динамической типизацией, а это значит, что функции foo можно передать любой объект. Если этот объект имеет атрибуты и методы, которые ожидает увидеть функция, то всё будет работать, а если нет — программа даст сбой.

Итак, при обсуждении подобных вещей различают структурную и номинальную подтипизацию. Структурная подтипизация основана на иерархии классов, на отношениях наследования. Если класс B является наследником класса A — он является подтипом класса A, а значит — может быть использован везде, где ожидается класс A.

А номинальная подтипизация основана на анализе операций, доступных для данного класса. Если класс B предлагает все атрибуты и методы, предоставляемые классом A — его можно использовать везде, где ожидается наличие класса A.

Тут, сравнивая структурную и номинальную подтипизацию, можно сказать, что первая не такая «питонистичная», как вторая, так как вторая лучше соответствует идее утиной типизации. Но, тем не менее, протоколы в Python — это механизмы, основанные на структурной подтипизации.

Подтипизация на практике

До выхода Python 3.8 для подтипизации можно было использовать только наследование и, например, применять абстрактные базовые классы. Ниже мы определяем именно такой класс — «чертёж» для дочерних классов, а после этого определяем несколько классов‑потомков, являющихся наследниками абстрактного базового класса:

from abc import ABC, abstractmethod  class Animal(ABC):     @abstractmethod     def feed(self) -> None:         pass  class Duck(Animal):     def feed(self) -> None:         print("Duck eats")  def feed(animal: Animal) -> None:     animal.feed()  duck = Duck() feed(duck)

Здесь мы сначала определяем абстрактный базовый класс Animal, символизирующий живое существо, в котором описан абстрактный метод feed, который позволяет это существо покормить. Затем мы создаём класс Duck, представляющий утку, являющийся наследником класса Animal и реализующий абстрактный метод. Далее — мы определяем универсальную функцию feed, которая, в качестве параметра, принимает объект класса Animal и вызывает его метод feed, то есть — позволяет покормить то существо, которое ей передали.

Выглядит всё это вполне здраво. Так в чём же тогда проблема? На самом деле, тут можно найти даже несколько проблем, которые способны оттолкнуть программистов от использования подобного подхода к работе с типами

Во‑первых — базовые классы часто плохо подготовлены для их использования сторонним кодом, их тяжело включать в свои проекты из, например, сторонних библиотек. То есть — если нужно создать класс, являющийся наследником некого базового класса, существующего во внешнем модуле, может — в общедоступной библиотеке, этот класс сначала надо самостоятельно найти.

Во‑вторых — возможна ситуация, когда нельзя менять существующий код базовых классов, содержащийся в общедоступных или в любых других внешних по отношению к проекту библиотеках. Это становится проблемой в том случае, если нужно, чтобы типы, импортированные из этих вот внешних источников, выглядели бы в проекте как подтипы других типов, возможно — в комбинации с другими типами, созданными разработчиком проекта.

И наконец — этот подход противоречит самому духу Python и идее утиной типизации.

Протоколы

Итак, в Python 3.8 появились протоколы. Это позволило смягчить вышеописанные проблемы. Протоколы, как можно догадаться из названия, воздействуют на код неявным образом. Они определяют «интерфейсы», описывающие ожидаемые атрибуты и методы, и, при необходимости, организуют проверку наличия всего этого в соответствующих классах:

from typing import Protocol  class Animal(Protocol):     def feed(self) -> None:         pass  class Duck:     def feed(self) -> None:         print("Duck eats")  def feed(animal: Animal) -> None:     animal.feed()  duck = Duck() feed(duck)

Как видно, Animal — это теперь протокол (Protocol). Класс Duck не является наследником какого-либо базового класса. Но mypy всё это полностью устраивает.

Подтипизация протоколов

Протоколы, как и следовало ожидать, поддерживают создание подклассов, то есть — определение дочерних протоколов, являющихся наследниками родительских протоколов и расширяющих их возможности. При создании подклассов протоколов главное помнить о том, что наследственные отношения надо устанавливать и с родительским протоколом, и с typing.Protocol:

from typing import Protocol  class Animal(Protocol):     def feed(self) -> None:         pass  class Bird(Animal, Protocol):     def fly(self) -> None:         pass  class Duck:     def feed(self) -> None:         print("Duck eats")      def fly(self) -> None:         print("Duck flies")  def feed(animal: Animal) -> None:     animal.feed()  def feed_bird(bird: Bird) -> None:     bird.feed()     bird.fly()  duck = Duck() feed_bird(duck)

В этом коде мы создаём класс Bird (птица) в виде наследника Animal, а затем определяем функцию, реализующую следующий план действий: сначала птицу кормят, а после этого она улетает.

Краткая история протоколов

Весь код, который мы рассмотрели выше — это правильные Python‑программы, даже с точки зрения Python версий ниже 3.8 (не забывайте о том, что Python — это динамически типизированный язык). Тут лишь, чтобы не волновать mypy, нужно импортировать ABC. Кроме того, mypy жаловался бы на последние примеры, где ABC не используется. При этом надо сказать, что протоколы существовали и до Python 3.8. Просто раньше они были не такими заметными, они не были так чётко описаны, как сейчас. Дело в том, что большинство Python‑разработчиков использовало понятие «протокол», имея в виду соглашение соответствию определённому интерфейсу. Теперь в это понятие вкладывается тот же смысл. Один из известных примеров — протокол итератора (iterator protocol) — интерфейс, описывающий то, какие методы нужно реализовать пользовательскому итератору (custom iterator). Для того чтобы всё это, при отсутствии явным образом описанных протоколов, работало бы с mypy, существовало несколько «трюков», таких, как применение пользовательских типов:

from typing import Iterable  class SquareIterator:     def __init__(self, n: int) -> None:         self.i = 0         self.n = n      def __iter__(self) -> "SquareIterator":         return self      def __next__(self) -> int:         if self.i < self.n:             i = self.i             self.i += 1             return i**2         else:             raise StopIteration()  def iterate(items: Iterable[int]) -> None:     for x in items:         print(x)  iterator = SquareIterator(5) iterate(iterator)

Сравнение абстрактных базовых классов и протоколов

Мы уже обсудили возможные проблемы, связанные с абстрактными базовыми классами (сложности с внешними модулями и интерфейсами, «непитонистический» код). Но протоколы не должны рассматриваться как замена абстрактных базовых классов. Пользоваться стоит и тем и другим. Например, абстрактные базовые классы — это хороший механизм многократного использования кода: весь общий функционал можно реализовать в виде базового класса, а в классах‑наследниках можно реализовывать лишь небольшие уникальные возможности программы. Протоколы же подобного не позволяют.

Итоги

В этом материале мы обсудили статическую и динамическую типизацию (утиную типизацию), поговорили о том, как mypy относится к подтипизации. До Python 3.8 применение подтипизации означало необходимость использования абстрактных классов. А с появлением протоколов в Python появился изящный механизм определения «интерфейсов». И у mypy появилась возможность проверки классов на соответствие этим «интерфейсам». Протоколы, отличаясь более «питонистическим» стилем, чем абстрактные базовые классы, позволяют указывать то, какие атрибуты и методы должны реализовывать классы. Такие классы можно использовать в качестве подтипов протокола, которому они соответствуют.

О, а приходите к нам работать? 🤗 💰

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде


ссылка на оригинал статьи https://habr.com/ru/companies/wunderfund/articles/751424/

Самый быстрый и безопасный PNG декодер в мире

TL;DR: декодер изображений PNG из стандартной библиотеки языка программирования Wuffs работает в 1.22–2.75 раза быстрее, чем libpng (широко используемая реализация PNG декодера на C с открытым исходным кодом), C-библиотеки libspng, lodepng и stb_image, а также самые популярные библиотеки для работы с PNG на Go и Rust.

Декодирование PNG с помощью Wuffs обсуждалось на Hacker News №1, Hacker News №2, /r/programming, /r/rust и lobste.rs.

Вступление

PNG (Portable Network Graphics) — это один из самых распространенных форматов для сжатия изображений без потерь, основанный на алгоритме сжатия zlib. Он был изобретен в 1990-х годах, когда доминировали 16-битные CPU, а максимальный размер оперативной памяти в 64 Кб все еще был насущной проблемой. Более современные форматы изображений (например, WebP) и алгоритмы сжатия (например, Zstandard) могут сжимать изображения более эффективно при сравнимых скоростях декодирования, но миллиарды существующих PNG затрудняют миграцию. По одной из метрик PNG по-прежнему является наиболее используемым форматом изображений в Интернете. Кроме того, телеметрия Mozilla IMAGE_DECODE_SPEED_XXX (выборка от 03.04.2021, Firefox Desktop nightly 89) ставит PNG на второе место после JPEG:

В большинстве случаев для декодирования PNG применяется libpng — широко распространенная рефернсная open-source реализация энкодера и декодера PNG, построенная на базе библиотеки zlib, аналогично широко распространенной реализации формата (алгоритма) сжатия zlib.

Wuffs — это memory-safe язык программирования XXI века со стандартной библиотекой, написанной на нем самом. Он предназначен для безопасной работы с «небезопасными» форматами файлов (Wrangling Untrusted File Formats Safely).

Прим. пер.: Wuffs можно использовать либо как самостоятельный язык программирования (Wuffs-the-language), либо как C-бибилиотеку, так как исходный код на Wuffs транспилируется в C (Wuffs-the-library). В статье будет идти речь об использовании Wuffs как C библиотеки.

На ноутбуке средней ценовой категории с x86_64 CPU (актуально на 2021 год — прим. пер.) Wuffs может декодировать PNG изображения в 1.5-2.75 раза быстрее, чем libpng:

libpng_decode_19k_8bpp                            58.0MB/s ± 0%  1.00x libpng_decode_40k_24bpp                           73.1MB/s ± 0%  1.00x libpng_decode_77k_8bpp                             177MB/s ± 0%  1.00x libpng_decode_552k_32bpp_ignore_checksum           146MB/s ± 0%  (*) libpng_decode_552k_32bpp_verify_checksum           146MB/s ± 0%  1.00x libpng_decode_4002k_24bpp                          104MB/s ± 0%  1.00x  libpng                                                  1.00x to 1.00x  ----  wuffs_decode_19k_8bpp/clang9                       131MB/s ± 0%  2.26x wuffs_decode_40k_24bpp/clang9                      153MB/s ± 0%  2.09x wuffs_decode_77k_8bpp/clang9                       472MB/s ± 0%  2.67x wuffs_decode_552k_32bpp_ignore_checksum/clang9     370MB/s ± 0%  2.53x wuffs_decode_552k_32bpp_verify_checksum/clang9     357MB/s ± 0%  2.45x wuffs_decode_4002k_24bpp/clang9                    156MB/s ± 0%  1.50x  wuffs_decode_19k_8bpp/gcc10                        136MB/s ± 1%  2.34x wuffs_decode_40k_24bpp/gcc10                       162MB/s ± 0%  2.22x wuffs_decode_77k_8bpp/gcc10                        486MB/s ± 0%  2.75x wuffs_decode_552k_32bpp_ignore_checksum/gcc10      388MB/s ± 0%  2.66x wuffs_decode_552k_32bpp_verify_checksum/gcc10      373MB/s ± 0%  2.55x wuffs_decode_4002k_24bpp/gcc10                     164MB/s ± 0%  1.58x

(*) В «упрощенном API» libpng нет возможности отключить проверку контрольной суммы, поэтому для libpng в кейсах *_ignore_checksum мы используем результаты для кейсов *_verify_checksum.

В бенчмарках используется следующая нотация: 77k_8bpp означает PNG изображение шириной 160 пикселей, высотой 120 пикселей и цветовой моделью размером 8 бит (размер индекса палитры) на один пиксель. При декодировании в формат 32bpp BGRA получается 160 × 120 × 4 = 76800 байт, сокращенно 77k. Другие примеры (dst — выходной формат, src — входной формат):

Обработка 4002k байт со скоростью 104 Мб/с или 164 Мб/с означает, что libpng или Wuffs требуется около 38 миллисекунд или 24 миллисекнуды соответственно для декодирования данного изображения размером 1165 × 859.

Замеры для некоторых других реализаций PNG (libspng, lodepng, stb_image, image/png из стандартной библиотеки языка Go и реализации PNG на Rust) представлены в Приложении.

Примеры команд из статьи получены с помощью примеров кода, доступных в репозитории Wuffs.

Используемые техники оптимизации

Сначала пара слов о том, как вообще устроен формат PNG. Процесс декодирования PNG состоит из следующих основных этапов (приведены самые ресурсозатратные операции):

  1. Два алгоритма хэширования для вычисления контрольных сумм — CRC-32 и Adler-32. Оба создают 32-битные хэши, но сами алгоритмы отличаются.

  2. Декомпрессия с помощью алгоритма DEFLATE.

  3. 2D фильтрация (своего рода подготовка данных для сжатия, позволяющая учесть двумерность данных — прим. пер.): зачастую эффективнее сжимать разность цветов соседних пикселей нежели сами цвета пикселей.

Каждый из этих этапов может быть оптимизирован.

Вычисление и проверка контрольных сумм

CRC-32

В статье 2009 года «Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction» рассказывается о том, как реализовать вычисление CRC-32 используя x86_64 SIMD-инструкции. Реализация этой техники в Wuffs выглядит примерно так. Код для ARM еще проще, так как ARM имеет отдельные инструкции для вычисления CRC-32.

Для оценки производительности различных реализаций хэш-функций существует набор тестов и бенчмарков SMHasher. Согласно этим бенчмаркам, наша реализация CRC-32 с SIMD в 47 раз быстрее, чем наивная реализация CRC-32 в SMHasher.

Что касается реальной производительности, то пример example/crc32 из Wuffs более-менее эквивалентнен программе /bin/crc32 из Debian с точки зрения функциональности, но при этом этом он в 7.3 раз быстрее (0.056 секунд против 0.410 секунд) на этом файле размером 178 Мб:

$ ls -lh linux-5.11.3.tar.gz | awk '{print $5 " " $9}' 178M linux-5.11.3.tar.gz $ g++ -O3 wuffs/example/crc32/crc32.cc -o wcrc32 $ time ./wcrc32   /dev/stdin < linux-5.11.3.tar.gz 05b309fb real    0m0.056s $ time /bin/crc32 /dev/stdin < linux-5.11.3.tar.gz 05b309fb real    0m0.410s

Adler-32

Об ускорении вычисления хэш-функции Adler-32 с помощью SIMD-инструкций не написано статей (актуально на 2021 год — прим. пер.), однако это возможно: вот реализации для ARM и x86_64 в Wuffs.

Реализация Adler-32 из Wuffs примерно в 6.4 раза быстрее (11.3 ГБ/с против 1.76 ГБ/с), чем реализация из zlib (указана как mimic_*) согласно benchstat:

$ cd wuffs $ # ¿ is just an unusual character that's easy to search for. By $ # convention, in Wuffs' source, it marks build-related information. $ grep ¿ test/c/std/adler32.c // ¿ wuffs mimic cflags: -DWUFFS_MIMIC -lz $ gcc -O3 test/c/std/adler32.c -DWUFFS_MIMIC -lz $ # Run the benchmarks. $ ./a.out -bench | benchstat /dev/stdin name                      speed wuffs_adler32_10k/gcc10   11.3GB/s ± 0% wuffs_adler32_100k/gcc10  11.6GB/s ± 0% mimic_adler32_10k/gcc10   1.76GB/s ± 0% mimic_adler32_100k/gcc10  1.72GB/s ± 0%

Игнорируем контрольные суммы

Очевидно, что самая быстрый способ проверить контрольную сумму — это не проверять ее вообще, пропуская эти значения при чтении PNG файла. Согласно результатам бенчмарка для *_ignore_checksum против *_verify_checksum, приведенных ранее, разница в производительности составляет около 4%. Разница не слишком большая, однако, как было уже сказано, отключение проверки контрольных сумм в libpng составляет проблему, но для Wuffs это можно сделать одной строкой. Однако, если вы используете альтернативный декодер PNG (не Wuffs), то отключение проверки контрольных сумм может ускорить декодирование даже больше, чем на 4% в случае, если декодер не использует SIMD-оптимизированную реализацию хэш-функций. При этом следует помнить, что отключение проверки контрольных сумм — это компромисс: мы теряем возможность обнаружить повреждение данных и таким образом, вообще говоря, отклоняемся от спецификации PNG, которая прямо предписывает это делать.

Декомпрессия DEFLATE

Основная часть данных после сжатия с помощью DEFLATE состоит из последовательности кодов: либо «символьных» кодов (фактически исходных данных), либо кодов копирования. Существует 256 возможных символьных кодов, по одному на каждый возможный распакованный байт. Каждый код копирования состоит из длины (сколько байт нужно скопировать, от 3 до 258 включительно) и расстояния (с какого места в «истории» или в ранее распакованном выводе нужно скопировать, от 1 до 32768 включительно).

Например, слово «banana» может быть сжато в виде такой последовательности:

  • Символ ‘b’

  • Символ ‘a’

  • Символ ‘n’

  • Скопировать 3 байта, начиная с 2 байта назад: «ana»

Коды кодируются по Хаффману, что означает, что они занимают переменное (но целое) число бит (от 1 до 48 включительно) и не обязательно начинаются или заканчиваются на границах байтов. Символьные коды всегда порождают один байт информации. Копирующие коды же выдают до 258 байт. Таким образом, максимальное количество выходных байт одного кода (будь то символьный код или код копирования) равно 258 (запомните это число, мы вернемся к нему позже).

Wuffs v0.2 предоставлял реализацию DEFLATE, аналогичную библиотеке zlib, и показывал сопоставимую производительность, по крайней мере, на x86_64. В Wuffs v0.3 были добавлены две существенные оптимизации для современных процессоров, поддерживающих 64-битной записи и чтения из памяти без выравнивания: ввод и вывод чанками по 8 байт.

Ввод чанками по 8 байт

Как было отмечено выше, коды копирования DEFLATE занимают от 1 до 48 бит. Реализация процедуры обработки одного такого кода в zlib считывает входные биты в нескольких местах цикла. В inffast.c имеется 7 экземпляров hold += (unsigned long)(*in++) << bits; bits += 8;, загружающих входные биты по 1 байту (8 бит) за раз. Вместо этого мы можем читать по 64 бита за раз. Некоторые из прочитанных битов будут отброшены, если в буфере уже есть необработанные биты, но это не страшно: чтение этих битов приведет к битовому сдвигу нулями, а побитовое ИЛИ с нулями — это no-op. Более подробно эти тонкости при работе с битами рассматриваются статье Фабиана «ryg» Гизена в этом посте 2018 года.

Для Wuffs чтение 64 битов за раз увеличило производительность микробенчмарка DEFLATE в 1.30 раз.

Вывод чанками по 8 байт

Рассмотрим последовательность кодов DEFLATE для сжатия фразы TO BE OR NOT TO BE. THAT IS ETC. Второй TO BE может быть представлен кодом копирования с длиной 5 и расстоянием 13. Копирование 5 байт можно реализовать с помощью следующей последовательности из пяти команд (при этом чтение и запись мы делаем без выравнивания): чтение 4 байтов; запись 4 байтов; чтение 1 байта; запись 1 байта; out_ptr += 5. При достаточно большом расстоянии кода копирования такая последовательность команд будет работать корректно даже без выравнивания. Очевидной оптимизацией в плане количества команд будет «копировать излишние данные» (чтение 8 байт; запись 8 байт; out_ptr += 5).

: TO_BE_OR_NOT_?????????????????????? : ^            ^ : out_ptr-13   out_ptr : : :              [1234567) copy 8 bytes :              v       v : TO_BE_OR_NOT_TO_BE_OR?????????????? :              ^    ^ :                   out_ptr += 5 : : :                   [) write 1 byte :                   vv : TO_BE_OR_NOT_TO_BE.OR?????????????? :                   ^^ :                    out_ptr += 1

Запись результатов обработки последующих кодов (например, символьного кода '.') будет делаться поверх «излишне скопированных» данных, то есть наша оптимизация не сломает алгоритм. Заметим, что API библиотеки zlib не дает возможность применить такую оптимизацию: функция inflateBack использует единый буфер для «истории» и вывода, поэтому перезапись 8 байт может изменить буфер «истории», и сделать результаты декодирования некорректными.

Для Wuffs округление длины кодов копирования до кратного 8 ускорило работу микробенчмарка DEFLATE в 1.48 раз.

Сравнение с gzip

Формат gzip — это, грубо говоря, сочетание алгоритма DEFLATE с проверкой контрольной суммы с помощью CRC-32. Как и пример example/crc32, пример example/zcat более-менее эквивалентнен программе /bin/zcat из Debian с точки зрения функциональности, но при этом этом он в 3.1 раз быстрее (2.680 секунд против 8.389 секунд) на том же файле размером 178 Мб:

$ gcc -O3 wuffs/example/zcat/zcat.c -o wzcat $ time ./wzcat   < linux-5.11.3.tar.gz > /dev/null real    0m2.680s $ time /bin/zcat < linux-5.11.3.tar.gz > /dev/null real    0m8.389s

Очевидно, что контрольная сумма, вычисленная нашей программой, должна быть равна контрольной сумме, вычисленной с помощью /bin/zcat:

$ ./wzcat   < linux-5.11.3.tar.gz | ./wcrc32   /dev/stdin 750d1011 $ /bin/zcat < linux-5.11.3.tar.gz | /bin/crc32 /dev/stdin 750d1011 $ tail --bytes=8 linux-5.11.3.tar.gz | hd 00000000  11 10 0d 75 00 78 70 3f

All-at-once декомпрессия

Для объяснения второй техники оптимизации шага декомпрессии рассмотрим следующую аналогию. Тактика бега на скорость из точки А в точку Б по ровной дороге тривиальна: нужно бежать изо всех сил. А теперь предположим, что точка Б находится на краю обрыва, внизу которого плавают голодные акулы, питающиеся бегунами. Тогда нам придется разделить наш забег на два этапа: начальный этап бега изо всех сил (обозначен синим) и этап осторожного добегания или вообще ходьбы, в котором главной целью является не свалиться с обрыва (обозначен красным).

Декомпрессия DEFLATE предполагает запись распакованных данных в некий буфер (наш забег), где вылезание за его границы представляет собой классический buffer overflow (обрыв с акулами). Чтобы избежать этого, библиотека zlib имеет две реализации декомпрессии: быструю «синюю» (используется когда, например, до конца буфера остается 258 или более байт) и медленную «красную» (используется во всех других случаях).

Кроме того, libpng выделяет два буфера (для текущего и предыдущего ряда пикселей) и обращается к zlib N раз, где N — высота изображения в пикселях. Каждый раз буфер назначения имеет размер ровно в одну строку (ширина в пикселях, умноженная на количество байт на пиксель, плюс байт для хранения конфигурации фильтра), что означает, что zlib распаковывает последние 258 или более байт каждой строки с помощью медленной «красной» реализации. Для наглядности, это примерно четверть пикселей RGB изображения размером 300 × 200, а с точки зрения скорости декодирования потери будут еще больше.

Алгоритм декомпресии формата zlib в Wuffs также использует это разделение на медленную и быструю реализации, так как это критично для безопасности, но в отличие от libpng, декомпрессия выполняется сразу для всего изображения (all-at-once), а не построчно. Другими словами, почти все (более 99% пикселей RGB изображения размером 300 × 200) пиксели теперь находятся в быстрой «синей» зоне. Это хорошо уже само по себе, но кроме того позволяет избежать кэш-промахов и branch misprediction при чередовании «синего» и «красного» кода.

Очевидно, что такой метод требует O(ширина изображения × высота изображения) промежуточной памяти вместо O(ширина изображения) памяти, но это не очень страшно, так как если изображение декодируется в оперативную память, а не на диск, то это само по себе уже требует O(ширина изображения × высота изображения) памяти. Кроме того, API Wuffs предоставляет вызывающей стороне некоторый выбор в отношении использования памяти. Вместо запроса «мне нужно M байт памяти для декодирования этого изображения» Wuffs говорит «мне нужно от M0 до M1 (включительно) байт памяти, и чем больше вы мне дадите, тем быстрее я буду работать». В текущей версии (0.3.0 — прим. пер.) Wuffs всегда устанавливает M0 равным M1 (выбора нет, all-at-once декомпрессиия используется всегда), но в будущих версиях может быть реализована возможность построчной декомпрессии, предлагая выбор между реализациями.

Фильтрация

И Wuffs, и libpng используют SIMD для ускорения 2D фильтрации. Например, вот реализация фильтрации в Wuffs для x86. Однако, на этом этапе libpng может показывать немного бóльшую производительность, поскольку, в отличии от Wuffs, он может гарантировать, что все строки изображения выровнены по оптимальным для SIMD границам. Wuffs же дает куда меньше гарантий относительно выравнивания, отчасти потому, что Wuffs не может самостоятельно выделять память, но в основном потому что all-at-once декомпрессия требует отказа от возможности, например, выравнивать начало каждой строки по 4 байта. Тем не менее, профилирование показывает, что на шаг декомпрессии тратится намного больше времени, чем на фильтрацию, так что преимущества all-at-once оптимизации перевешивают оверхед на фильтрацию без выравнивания.

Почему бы не оптимизировать так же уже существующие реализации PNG декодеров вместо создания еще одной с нуля?

В теории это можно было сделать, но есть ряд причин, почему стоило написать все с нуля.

Почему нет смысла патчить libpng?

А точнее, почему это бесперспективно. Как минимум по нескольким причинам:

  1. libpng написан на C, а значит он скорее всего имеет проблемы с memory safety. Более того, его API для обработки ошибок построен на setjmp и longjmp, а сотни goto усложняют статический или формальный анализ.

  2. Несмотря на то, что формат файла практически не менялся с 1999 года (версия 1.2 была формализована в 2003 году, а APNG — это неофициальное расширение), в libpng было обнаружено 74 CVE с 2002 по 2021 год, 9 из которых — с 2018 года.

  3. В исходниках libpng встречаются такие вещи как, например, лаконичный комментарий «TODO: WARNING: TRUNCATION ERROR: DANGER WILL ROBINSON», добавленный больше десяти лет назад. Думаю, не нужно пояснять, что здесь не так.

  4. Код libpng на самом деле невероятно сложен. Например грубая прикидка с помощью команды wc -l .c arm/.c intel/*.c в репозитории libpng даст нам 35182 строки кода (без учета заголовочных файлов). Аналогичная команда для Wuffs покажет нам 2110 строк. Конечно, libpng также реализует энкодер PNG, но даже с учетом этого остается разница примерно на порядок.

Попытки пропатчить zlib

Я пытался пропатчить zlib несколько лет назад, но это оказалось сложнее, чем я думал, из-за упомянутой выше проблемы с API inflateBack. Так или иначе, существуют форки zlib zlib-ng/zlib-ng, и cloudflare/zlib которые решают часть проблем с производительностью с помощью похожих оптимизаций. Однако, использование, например zlib-ng вместо ванильного zlib в libpng дает совсем небольшой прирост производительности:

libpng_decode_19k_8bpp                            58.0MB/s ± 0%  1.00x libpng_decode_40k_24bpp                           73.1MB/s ± 0%  1.00x libpng_decode_77k_8bpp                             177MB/s ± 0%  1.00x libpng_decode_552k_32bpp_ignore_checksum           146MB/s ± 0%  (†) libpng_decode_552k_32bpp_verify_checksum           146MB/s ± 0%  1.00x libpng_decode_4002k_24bpp                          104MB/s ± 0%  1.00x  libpng                                                  1.00x to 1.00x  ----  zlibng_decode_19k_8bpp/gcc10                      63.8MB/s ± 0%  1.10x zlibng_decode_40k_24bpp/gcc10                     74.1MB/s ± 0%  1.01x zlibng_decode_77k_8bpp/gcc10                       189MB/s ± 0%  1.07x zlibng_decode_552k_32bpp_ignore_checksum/gcc10                 skipped zlibng_decode_552k_32bpp_verify_checksum/gcc10     177MB/s ± 0%  1.21x zlibng_decode_4002k_24bpp/gcc10                    113MB/s ± 0%  1.09x  zlibng                                                  1.01x to 1.21x

Почему стоит попытаться пропатчить Go и Rust

И Go, и Rust — это современные memory safe языки, однако для существующих проектов на C/C++ проще внедрить Wuffs, который транспилируется в C. Тем не менее, вполне возможно, что внедрение оптимизаций, описанных в этой статье в реализации декодера PNG на Go или Rust имеет смысл. Например, ни в Go, ни в Rust реализация Adler-32 не использует SIMD. Возможно, также стоит попробовать оптимизировать DEFLATE: реализация DEFLATE в Go читает только один байт за раз, а реализация miniz_oxide в Rust хоть и читает 4 байта за раз, но это все еще хуже, чем читать по 8 байт. Кроме того, ни в Go, ни в Rust декодер PNG не использует all-at-once декомпрессию.

Однако стоит отметить, что в отличие от Go и Rust, memory safety в Wuffs обеспечивается во время компиляции, а не с помощью проверок в рантайме (например, что индекс i при доступе к a[i] не выходит за границы массива или что (x + y) не переполняет u32). Компиляторы Go и Rust выполняют часть этих проверок во время компиляции (например, при итерации по элементам массива), но часть проверок все равно делается в рантайме, например, при декодировании DEFLATE-кодов. Очевидно, такие проверки могут значительно влиять на производительность. В этом плане мне нравится девиз языка Zig “Performance and Safety: Choose Two”, но, в отличие от Zig, в Wuffs нет отдельных режимов сборки «Release Fast» и «Release Safe»: единственный режим сборки (компиляция кода на C с флагом -O3) дает и безопасность, и производительность.

Заключение

Только что вышла версия Wuffs 0.3.0-beta.1, которая содержит самый быстрый и безопасный в мире декодер PNG. Примеры использования из C и C++ доступны здесь. PNG декодер пока не поддерживает цветовые пространства и гамма-коррекцию (см. Issue 39), но даже без этого Wuffs может представлять некоторую ценность.

Прим. пер.: состояние Wuffs на 2023 год обсуждается ниже в абзаце «Комментарий переводчика».

Приложение

Подробные результаты бенчмарков

libpng здесь — это /usr/lib/x86_64-linux-gnu/libpng16.so из дистрибутив Debian Bullseye.

libpng_decode_19k_8bpp                            58.0MB/s ± 0%  1.00x libpng_decode_40k_24bpp                           73.1MB/s ± 0%  1.00x libpng_decode_77k_8bpp                             177MB/s ± 0%  1.00x libpng_decode_552k_32bpp_ignore_checksum           146MB/s ± 0%  (†) libpng_decode_552k_32bpp_verify_checksum           146MB/s ± 0%  1.00x libpng_decode_4002k_24bpp                          104MB/s ± 0%  1.00x  libpng                                                  1.00x to 1.00x  ----  wuffs_decode_19k_8bpp/clang9                       131MB/s ± 0%  2.26x wuffs_decode_40k_24bpp/clang9                      153MB/s ± 0%  2.09x wuffs_decode_77k_8bpp/clang9                       472MB/s ± 0%  2.67x wuffs_decode_552k_32bpp_ignore_checksum/clang9     370MB/s ± 0%  2.53x wuffs_decode_552k_32bpp_verify_checksum/clang9     357MB/s ± 0%  2.45x wuffs_decode_4002k_24bpp/clang9                    156MB/s ± 0%  1.50x  wuffs_decode_19k_8bpp/gcc10                        136MB/s ± 1%  2.34x wuffs_decode_40k_24bpp/gcc10                       162MB/s ± 0%  2.22x wuffs_decode_77k_8bpp/gcc10                        486MB/s ± 0%  2.75x wuffs_decode_552k_32bpp_ignore_checksum/gcc10      388MB/s ± 0%  2.66x wuffs_decode_552k_32bpp_verify_checksum/gcc10      373MB/s ± 0%  2.55x wuffs_decode_4002k_24bpp/gcc10                     164MB/s ± 0%  1.58x  wuffs                                                   1.50x to 2.75x  ----  libspng_decode_19k_8bpp/clang9                    59.3MB/s ± 0%  1.02x libspng_decode_40k_24bpp/clang9                   78.4MB/s ± 0%  1.07x libspng_decode_77k_8bpp/clang9                     189MB/s ± 0%  1.07x libspng_decode_552k_32bpp_ignore_checksum/clang9   236MB/s ± 0%  1.62x libspng_decode_552k_32bpp_verify_checksum/clang9   203MB/s ± 0%  1.39x libspng_decode_4002k_24bpp/clang9                  110MB/s ± 0%  1.06x  libspng_decode_19k_8bpp/gcc10                     59.6MB/s ± 0%  1.03x libspng_decode_40k_24bpp/gcc10                    77.5MB/s ± 0%  1.06x libspng_decode_77k_8bpp/gcc10                      189MB/s ± 0%  1.07x libspng_decode_552k_32bpp_ignore_checksum/gcc10    223MB/s ± 0%  1.53x libspng_decode_552k_32bpp_verify_checksum/gcc10    194MB/s ± 0%  1.33x libspng_decode_4002k_24bpp/gcc10                   109MB/s ± 0%  1.05x  libspng                                                 1.02x to 1.62x  ----  lodepng_decode_19k_8bpp/clang9                    65.1MB/s ± 0%  1.12x lodepng_decode_40k_24bpp/clang9                   72.1MB/s ± 0%  0.99x lodepng_decode_77k_8bpp/clang9                     222MB/s ± 0%  1.25x lodepng_decode_552k_32bpp_ignore_checksum/clang9               skipped lodepng_decode_552k_32bpp_verify_checksum/clang9   162MB/s ± 0%  1.11x lodepng_decode_4002k_24bpp/clang9                 70.5MB/s ± 0%  0.68x  lodepng_decode_19k_8bpp/gcc10                     61.1MB/s ± 0%  1.05x lodepng_decode_40k_24bpp/gcc10                    62.5MB/s ± 1%  0.85x lodepng_decode_77k_8bpp/gcc10                      176MB/s ± 0%  0.99x lodepng_decode_552k_32bpp_ignore_checksum/gcc10                skipped lodepng_decode_552k_32bpp_verify_checksum/gcc10    139MB/s ± 0%  0.95x lodepng_decode_4002k_24bpp/gcc10                  62.3MB/s ± 0%  0.60x  lodepng                                                 0.60x to 1.25x  ----  stbimage_decode_19k_8bpp/clang9                   75.1MB/s ± 1%  1.29x stbimage_decode_40k_24bpp/clang9                  84.6MB/s ± 0%  1.16x stbimage_decode_77k_8bpp/clang9                    234MB/s ± 0%  1.32x stbimage_decode_552k_32bpp_ignore_checksum/clang9  162MB/s ± 0%  1.11x stbimage_decode_552k_32bpp_verify_checksum/clang9              skipped stbimage_decode_4002k_24bpp/clang9                80.7MB/s ± 0%  0.78x  stbimage_decode_19k_8bpp/gcc10                    73.3MB/s ± 0%  1.26x stbimage_decode_40k_24bpp/gcc10                   81.8MB/s ± 0%  1.12x stbimage_decode_77k_8bpp/gcc10                     214MB/s ± 0%  1.21x stbimage_decode_552k_32bpp_ignore_checksum/gcc10   145MB/s ± 0%  0.99x stbimage_decode_552k_32bpp_verify_checksum/gcc10               skipped stbimage_decode_4002k_24bpp/gcc10                 79.7MB/s ± 0%  0.77x  stbimage                                                0.77x to 1.32x  ----  go_decode_19k_8bpp/go1.16                         39.4MB/s ± 1%  0.68x go_decode_40k_24bpp/go1.16                        46.7MB/s ± 1%  0.64x go_decode_77k_8bpp/go1.16                         78.3MB/s ± 0%  0.44x go_decode_552k_32bpp_ignore_checksum/go1.16                    skipped go_decode_552k_32bpp_verify_checksum/go1.16        118MB/s ± 0%  0.81x go_decode_4002k_24bpp/go1.16                      50.5MB/s ± 0%  0.49x  go                                                      0.44x to 0.81x  ----  rust_decode_19k_8bpp/rust1.48                     89.8MB/s ± 0%  1.55x rust_decode_40k_24bpp/rust1.48                     122MB/s ± 0%  1.67x rust_decode_77k_8bpp/rust1.48                      158MB/s ± 0%  0.89x rust_decode_552k_32bpp_ignore_checksum/rust1.48                skipped rust_decode_552k_32bpp_verify_checksum/rust1.48    136MB/s ± 0%  0.93x rust_decode_4002k_24bpp/rust1.48                   122MB/s ± 0%  1.17x  rust                                                    0.89x to 1.67x

Воспроизведение

Wuffs транспилируется в код на C, а точнее в C библиотеку без зависимостей, состоящую из одного файла. Таким образом, для запуска бенчмарков вам не нужно ничего, кроме компилятора C:

$ cd wuffs $ # ¿ is just an unusual character that's easy to search for. By $ # convention, in Wuffs' source, it marks build-related information. $ grep ¿ test/c/std/png.c // ¿ wuffs mimic cflags: -DWUFFS_MIMIC -lm -lpng -lz $ gcc -O3 test/c/std/png.c -DWUFFS_MIMIC -lm -lpng -lz $ # Run the tests. $ ./a.out $ # Run the benchmarks. $ ./a.out -bench

Бенчмарки для Go и Rust вынесены в отдельные программы.

Используемое железо

Все метрики были получены на бюджетном ноутбуке с x86_64 CPU:

$ cat /proc/cpuinfo | grep model.name | uniq model name: Intel(R) Core(TM) m3-6Y30 CPU @ 0.90GHz

Ниже также приведены результаты бенчмарков для Raspberry Pi 4 (32-битный armv7l), которые не столь впечатляющи как результаты для x86_64, но тем не менее разница в производительности между libpngи Wuffs все равно заметная:

libpng_decode_19k_8bpp                            44.1MB/s ± 0%  1.00x libpng_decode_40k_24bpp                           54.6MB/s ± 0%  1.00x libpng_decode_77k_8bpp                             123MB/s ± 0%  1.00x libpng_decode_552k_32bpp_ignore_checksum           101MB/s ± 0%  (†) libpng_decode_552k_32bpp_verify_checksum           101MB/s ± 0%  1.00x libpng_decode_4002k_24bpp                         82.1MB/s ± 0%  1.00x  libpng                                                  1.00x to 1.00x  ----  wuffs_decode_19k_8bpp/clang9                      82.5MB/s ± 0%  1.87x wuffs_decode_40k_24bpp/clang9                      105MB/s ± 0%  1.92x wuffs_decode_77k_8bpp/clang9                       303MB/s ± 0%  2.46x wuffs_decode_552k_32bpp_ignore_checksum/clang9     180MB/s ± 0%  1.78x wuffs_decode_552k_32bpp_verify_checksum/clang9     174MB/s ± 0%  1.72x wuffs_decode_4002k_24bpp/clang9                    100MB/s ± 0%  1.22x  wuffs_decode_19k_8bpp/gcc8                        79.8MB/s ± 0%  1.81x wuffs_decode_40k_24bpp/gcc8                        106MB/s ± 0%  1.94x wuffs_decode_77k_8bpp/gcc8                         271MB/s ± 0%  2.20x wuffs_decode_552k_32bpp_ignore_checksum/gcc8       177MB/s ± 0%  1.75x wuffs_decode_552k_32bpp_verify_checksum/gcc8       170MB/s ± 0%  1.68x wuffs_decode_4002k_24bpp/gcc8                      100MB/s ± 0%  1.22x  wuffs                                                   1.22x to 2.46x

Комментарий переводчика (актуальное состояние Wuffs)

Несмотря на то, что с момента публикации оригинального поста прошло больше двух лет, Wuffs не только не уступил позиций по части декодирования PNG (кто-то даже называет его «ridiculously fast»), но и обзавелся рядом новых возможностей:

Последняя версия Wuffs на момент написания поста — 0.3.3. Для нее доступны следующие интерфейсы:


ссылка на оригинал статьи https://habr.com/ru/articles/751462/

В Go 1.21 существенно расширяется стандартная библиотека

// теперь в Go так можно! slices.Contains(s, v)

Год назад в блоге Каруны мы писали про дженерики в Go, и там упоминалось, что гошное сообщество разделилось на две части. Не всем это нововведение было нужно, особенно в простом продуктовом коде. И надо сказать, это до сих пор так, дженерики по-прежнему используют далеко не все проекты.

Однако для стандартной библиотеки Go это было по-настоящему царским подарком. Появились новые стандартные обобщенные функции, и, отстоявшись в экспериментальном репозитории golang.org/x/exp, теперь появятся в Go 1.21. Релиз буквально через месяц.

TLDR: появилось множество функций по работе со слайсами, мапами, а также новый логгер с (почти) всеми нужными фишечками.

Лично для меня знаковым событием стало появление возможности поиска элемента в слайсе и получение ключей мапы, потому что ну давно пора, 10 лет языку.

Но давайте обо всём по порядку.

1. Работа со слайсами (пакет «slices»)

Как говорится, свершилось то, о чём так долго говорили большевики. Для наглядности покажу несколько примеров в стиле «было-стало», чтобы те, кто плохо знают Go, прочувствовали ситуацию.

Поиск элемента в слайсе (любой элемент comparable-типа)

// было: var contains bool for i := range s {     if v == s[i] {         contains = true;         break;     } }  // стало: slices.Contains(s, v) 

Сортировка

Раньше по-простому можно было отсортировать слайсы только трёх типов, для чего были отдельные функции пакета sort: Ints, Float64s, Strings.

Для других типов, например float32 или int64, приходилось городить что-то такое:

// было sort.Slice(s, func(i, j int) bool {     return s[i] < s[j] })  // стало (для любых ordered типов) slices.Sort(s)

Поиск максимума

// было  max := s[0] for _, v := range s {     if v > max {         max = v     } }  // стало maxVal := slices.Max(s)

Сравнение двух слайсов

// было func Equal(a, b []int) bool {     if len(a) != len(b) {         return false     }     for i, v := range a {         if v != b[i] {             return false         }     }     return true }   // стало areEqual := slices.Compare(a, b)

и множество других функций, таких как бинарный поиск, вставка внутрь слайса, и т. д. Посмотреть полный список можно здесь.

2. Работа с map (пакет «maps»)

Из интересного это maps.Keys(m) и maps.Values(m), которые возвращают соответственно ключи и значения мапы. Те, кто прорешивал задачки на литкоде, знают, как это надоедает — писать бессмысленные циклы и отвлекаться от основной задачи.

ages := map[string]int{"John": 21, "Jack": 32}  // было: var names []string for name, _ := range ages {     names = append(names, name) }  // стало: names := maps.Keys(ages) 

Также в пакете maps есть функции для сравнения мап, клонирования, копирования, и т. д., см https://pkg.go.dev/maps

3. Новый логгер (пакет «log/slog»)

За новость и информацию о логгере спасибо tg-каналу Cross Join

Я как-то изучал различные опросники для собеседований по Go, где один из стандартных вопросов был «расскажите, почему встроенный логгер никто не использует».

Ну так вот, его никто не использует, потому что он ничего не может. В итоге в 99.9% проектов появились сторонние решения zerolog, logrus, zap, и т. д.

В новой версии Go добавили пакет log/slog (игра слов: slog переводится как «вкалывать», «утомительный»), в котором есть:

  • уровни severity: Debug, Info, Warn, Error (или можно использовать любое integer-число)

  • возможность использовать Handler: textHandler, jsonHandler или свой собственный, удовлетворяющий интерфейсу Handler

  • встроенная возможность передачи ключ-значение:

    // здесь message - это "hello", // и еще добавляется ключ-значение count=3 logger.Info("hello", "count", 3)

    если использовать вместе с json-хандлером, то вывод будет такой:

    {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}

  • эти ключи-значения можно группировать, и получать вложенный джейсон (slog.Group)

  • можно выводить лог с контекстом

    InfoCtx(ctx, "message", "key", val)

  • всякие фишечки для удобства и производительности, например метод With, который возвращает sub-логгер, при создании которого можно сразу задать некоторые атрибуты. Встроенные хандлеры отформатируют их только один раз.

Чего не хватает:

Нет сэмплирования, а многим это важно. Надеюсь, допилят в будущем.
Пока не очень понятно, что со скоростью. Говорят, что zap быстрее.

Выводы

Стандартная библиотека существенно расширилась, пополнилась функциями, которые давно уже должны были быть, но без дженериков их было реализовать намного труднее. Как сказал один мой знакомый, «если еще и обработку ошибок сделают нормальную, то точно перейду на Go».

Что касается меня — скорее всего, попробую выкинуть сторонний логгер в одном из проектов, чтобы уменьшить ненужные зависимости. Ну, и лишних циклов по слайсам и мапам будет существенно меньше.

Вообще ситуация напоминает появление grid в стандарте CSS: люди 20+ лет верстали html-страницы бог знает как: таблицами, флоатами, флексами, чем попало, и вот наконец кто-то догадался, что людям надо как-то располагать элементы на странице. И появились гриды, где можно показать «вот здесь у меня колонки». Спасибо, что дошло. Как по мне, это должно было быть в самой первой версии CSS, а остальные свистелки можно было и отложить.

Так же и с Go: понятно, что стандартная библиотека намеренно небольшая, но уж поискать элемент в слайсе — это то, что нужно всем с самого начала, это есть, наверно, во всех языках.

Поэтому я и написал в заголовке, что библиотека расширилась «существенно», потому что в мире Go это биг дил.


ссылка на оригинал статьи https://habr.com/ru/companies/karuna/articles/747726/

Как генерировать модели интерфейсов на основе спецификации на стороне frontend-приложений

На связи снова Архитектурный комитет компании SimbirSoft, и мы продолжаем наш цикл статей, посвященных Design API First. Ранее мы уже писали о том, что представляет собой этот подход, приводили пример спецификации для сервиса аутентификации и рассказывали, как мы интегрируем этот паттерн в наш конвейер разработки.

Сегодня мы немного отвлечемся от бэкенда и разберем автоматизацию одной из рутинных задач на стороне frontend-разработки, а именно — описание моделей интерфейсов для взаимодействия фронта с беком, также написание API-сервисов, в которых фиксируются endpoints, методы запросов и формат передачи данных (query-параметры, заголовки, тело).

Инструменты кодогенерации

В нашей компании активно применяется стандарт OpenAPI. Несмотря на свою объемность в сравнении с более молодыми форматами, он является проверенным и наиболее отлаженным. Для него существует множество инструментов для поддержания документации. Поэтому рассмотрим несколько популярных инструментов, позволяющих работать с этим стандартом:

  1. swagger-typescript-api — генерирует модели и сервисы (Fetch или Axios), есть возможность написания своих шаблонов кодогенерации.

  2. openapi-typescript очень легковесный инструмент, заточенный под фронтендера, с хорошей поддержкой и без лишних зависимостей. Раскладывает спецификацию на набор интерфейсов, далее либо применяем самостоятельно данный инструмент, либо пользуемся вспомогательными — openapi-fetch или openapi-typescript-fetch, которые примут в качестве дженерика полученные ранее интерфейсы. Клиентов для axios или angular под openapi-typescript, к сожалению, не существует.

  3. openapi-typescript-codegen – инструмент, аналогичный предыдущему, но дающий чуть больше кастомизации для шаблонов, в том числе тут есть шаблоны клиентов под axios или angular.

  4. openapitools/openapi-generator-cli — это по сути Node.js-обертка над Swagger Codegen, который поддерживается создателями Swagger и стандарта OpenApi. Невероятно мощный инструмент: может генерировать клиентов не только для frontend, но и для backend. Есть возможность передачи кастомных шаблонов, можно настроить кодогенерацию под конкретный формат клиентов (Fetch, Angular, RxJS и другие).

Рисунок 1. Популярные npm-пакеты для кодогенерации

Рисунок 1. Популярные npm-пакеты для кодогенерации

Из npm-трендов (Рис. 1) видно, что openapi-typescript догоняет по популярности openapitools/openapi-generator-cli. Почему так происходит?

Во-первых, последний инструмент в зависимостях имеет Java — frontend-разработчику приходится ставить JRE или запускать генерацию в докере. Для запуска в докере есть специальный флаг в конфиге генератора, поэтому писать докер-файл не нужно, все запускается «под капотом». 

Во-вторых, openapi-typescript предоставляет лишь корректную конвертацию в интерфейсы ТS, дальше с ними можно сделать что угодно. В то время как последний инструмент генерит еще и клиентов, и, если они не подойдут, приходится изучать громоздкую документацию и перенастраивать шаблоны кодогенерации.

Разберем пример на основе openapi-typescript-codegen и спеки сервиса авторизации из первой статьи.

Выполняем команды:

$ npm i openapi-typescript-codegen $ npx openapi --input=spec.yml --output=api --client=fetch

И получаем следующую структуру:

 . └── api/     ├── core/     ├── models/     ├── services/     └── index.ts

Директория core содержит базовые переиспользуемые модели, связанные непосредственно с кодогенертором. Папка models содержит все упомянутые в спеке модели, services содержит клиенты (URL для endpoints в связке с нужными моделями). Примеры моделей и сервиса приведены ниже.

Примеры моделей
export type Auth_Request_Model_AuthAccount = {   /**    * Email, привязанный к аккаунту пользователя    */   userLogin: string; }; export type Auth_Response_Model_AbstractSuccessAccessAccount = {     accessToken: string;     /**      * ID аккаунта пользователя      */     accountId?: string;     message: string; };

Пример сервиса
import type {Auth_Request_Model_AuthAccount} from "../models/Auth_Request_Model_AuthAccount"; import type {Auth_Response_Model_Account} from "../models/Auth_Response_Model_Account"; import type {Auth_Response_Model_WaitingAccessAccount} from "../models/Auth_Response_Model_WaitingAccessAccount";   import type {CancelablePromise} from "../core/CancelablePromise"; import {OpenAPI} from "../core/OpenAPI"; import {request as __request} from "../core/request";   export class AuthService {   /**    * Auth/R2. Метод получения доступа к аккаунту пользователя    * Метод предназначен для аутентификации пользователя под указанным email, соответствующим аккаунту в БД    * @param requestBody    * @param acceptLanguage https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Accept-Language    * @param correlationId    * @param platform    * @returns Auth_Response_Model_WaitingAccessAccount - Ожидается активация созданного аккаунта пользователя    * - Ожидается подтверждение входа пользователя в свой аккаунт    * - Ожидается подтверждение восстановления доступа пользователя к своему аккаунту    *    * @throws ApiError    */   public static authAccount(requestBody: Auth_Request_Model_AuthAccount, acceptLanguage?: string, correlationId?: string, platform?: "MOBILE" | "WEB"): CancelablePromise<Auth_Response_Model_WaitingAccessAccount> {     return __request(OpenAPI, {       method: "POST",       url: "/v1/signin",       headers: {         "Accept-Language": acceptLanguage,         CorrelationID: correlationId,         Platform: platform,       },       body: requestBody,       mediaType: "application/json;charset=UTF-8",       errors: {         400: `Некорректные входные данные. Возвращается список атрибутов с ошибками`,         403: `Пользователю с текущими правами доступ отклонён`,         500: `Внутренняя ошибка сервера`,       },     });   }     /**    * Auth/R3. Метод получения информации об аккаунте    * Метод предназначен для получения информации из БД об аккаунте текущего пользователя    * @param acceptLanguage https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Accept-Language    * @param correlationId    * @returns Auth_Response_Model_Account Сформирован ответ с информацией об аккаунте текущего пользователя    * @throws ApiError    */   public static getAccount(acceptLanguage?: string, correlationId?: string): CancelablePromise<Auth_Response_Model_Account> {     return __request(OpenAPI, {       method: "GET",       url: "/v1/auth",       headers: {         "Accept-Language": acceptLanguage,         CorrelationID: correlationId,       },       errors: {         401: `Пользователь не был аутентифицирован`,         500: `Внутренняя ошибка сервера`,       },     });   } }

Рассмотрим несколько вариантов реализации инфраструктуры.

Разработка в монорепозитории

Поскольку для кодогенерации нужна спецификация, которую никто не хранит в директории с кодом frontend, то одним из естественных решений для такой связности является использование монорепозитория. Это в каком-то плане классический подход (Рис. 2).

Рисунок 2. Реализация в монорепозитории

Рисунок 2. Реализация в монорепозитории

Backend и frontend разрабатываются вместе, что дает удобный доступ к спецификации. При этом не важно, что первично: спецификация или код —  мы либо сразу генерируем модели, либо запускаем ещё промежуточную команду по экстракции спеки из кода.

Разработка в раздельных репозиториях

Кодогенерация моделей при хранении кода frontend и backend в раздельных репозиториях сильно зависит от выбранного воркфлоу на проекте.

1. С тестированием в общей ветке

Тестирование в общей ветке применяется достаточно часто. Тут сказывается ограниченность ресурсов располагаемых серверов, не всегда есть возможность раскатать множество инстансов бекенда с непустой базой для разработки и тестирования.

1.1. Для случаев, когда тестирование делается в общей ветке, можно дождаться, когда сборку backend накатят на стенд Dev, отчего спецификация перезальется, и frontend-специалист сможет ее спокойно скачать по известному URL (Рис. 3).

Рисунок 3. Хранение спецификации в репозитории бекенда

Рисунок 3. Хранение спецификации в репозитории бекенда

1.2. Если на проекте проектированию отдельных кусков функциональности уделяется должное внимание, происходит согласование форматов взаимодействия frontend и backend. И/или есть сильная потребность запускать разработку параллельно (вести разработку frontend вместе с backend, а не после оного), то вам может потребовать отдельный репозиторий для хранения спецификаций. Отличия от предыдущего варианта минимальны (Рис. 4) — тут backend генерирует и для себя клиентов.

Рисунок 4. Хранение спецификации в отдельном репозитории

Рисунок 4. Хранение спецификации в отдельном репозитории

1.3. Если спецификация лежит статично и не нуждается в какой-либо компиляции, то можно «сходить» в соседний репозиторий и забрать ее оттуда, не дожидаясь деплоя на стенд (Рис. 5). Для авторизации curl-a в gitlab или bitbacket используем Access Token, сгенерированный под конкретного человека, в Git не храним (добавляем в gitignore).

Рисунок 5. Модифицированный способ хранения спецификации

Рисунок 5. Модифицированный способ хранения спецификации

2. С тестированием отдельных веток на отдельных стендах

Если перед вливанием в общую ветку ведется отдельное тестирование на отдельном стенде, то все становится интересней и сложней. Подходы 1.1-1.3 можно переиспользовать (и в 90% случаев их хватает), но уже в отношении отдельных стендов. В зависимости от задачи надо только не забывать менять URL на спецификацию.

Как вариант, для особо сложных случаев можно кодогенерацию вынести в CI, клиенты будут артефактами сборки. В дальнейшем клиенты публикуются в корпоративном npm. Для версионирования используются «версия продукта» + «имя ветки» (например, @project-scope/api@1.2.3-task_123). Если пакета с таким номером задачи не существует, то ставим по дефолту последний пакет из общей ветки (например, @project-scope/api@1.2.3-dev). Отметим, что ставить пакеты нужно с флагом «no-save», так как от задачи к задаче будут появляться новые версии пакетов (Рис. 6).

Рисунок 6. Вариант реализации с тестированием отдельных веток

Рисунок 6. Вариант реализации с тестированием отдельных веток

Заключение

Итак, в этой статье мы разобрали несколько инструментов кодогенерации, которые могут полезны frontend-разработчику, описали типовые схемы реализации инфраструктуры. Конечно, выбор определенной схемы зависит от конкретных задач, но вывод все равно очевиден: если вы используете на проекте подход Design API First, то пользуйтесь кодогенераторами — это позволит автоматизировать небольшую работу в пределах одной задачи, но огромную в рамках всего проекта. Также поможет избавиться от ошибок из-за человеческого фактора при интеграции с backend.

При наличии удобного доступа к спецификации пользуйтесь подходами 1.1-1.3 — просто забирайте ее из соседней директории или curl-ом.

В противном случае можно присмотреться к npm как к очень удобной системе доставке кода. Например, на проектах с микросервисами и микрофронтами.

Пользуетесь ли вы подобной кодогенерацией? Как доставляете спецификацию или готовый код на фронт? Какие интересные кейсы вам попадались? Пишите в комментариях, будем рады обсудить это вместе.

Другие полезные материалы для разработчиков и архитекторов в IT публикуем в наших соцсетях — ВК и Telegram


ссылка на оригинал статьи https://habr.com/ru/companies/simbirsoft/articles/751406/