Шось про айтішку – Telegram
Шось про айтішку
1.46K subscribers
447 photos
162 videos
2 files
599 links
Фронтенд, ШІ, 3D друк, FPV, історії з життя та роботи
Download Telegram
В Reagent есть хитрый макрос with-let, который создаёт локальные биндинги в компоненте, но при этом они создаются только один раз, во время инициализации компонента.

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

https://github.com/roman01la/uix/commit/3fdcc82388294812c56d6b866f4a7db276d9d567
В ClojureScript есть хитрый метатег ^not-native, который используется как маркер для генерирования прямого вызова имплементации протокола, в обход рефлексии. Эта оптимизация имеет смысл в «горячем» коде, который часто отрабатывает, например в цикле.

Допустим вот такой пример

(def v [1 2 3])
(nth v 0)
(-nth v 0)
(-nth ^not-native v 0)

В первом случае вызывается функция cljs.core/nth, которая работает не только с IIndexed, но и с JS массивами, строками, и неиндексированными последовательностями.

В данном случае, с вектором, вызов делегируется на фукцию cljs.core/-nth в протоколе IIndexed. То есть зная, что тип данных реализует конкретный протокол можно напрямую вызывать его реализацию (как во втором примере).

Однако такой вызов все равно пройдет через проверку типов, чтобы найти реализацию протокола для конкретного типа данных.

Маркер ^not-native заставит компилятор сгенерировать прямой вызов реализации протокола. То есть вместо чего-то такого cljs.core._nth.invoke(v, 0), будет сгенерирован код типа v.cljs.core.IIndexed_nth(0).

Этот маркер часто применяеться в стандартной библиотеке cljs. В будущем проверки с implements? в коде будут автоматически маркировать проверяемые значения.

(if (implements? IIndexed v)
(-nth v) ;; неявный ^not-native
(nth v))
Недавно нужно было поднять браузерные тесты на CI. В итоге оказалось проще сделать что-то свое, чем разбираться в настройке существующих решений.

Скрипт в 30 строк запускающий тесты Puppeteer https://github.com/roman01la/uix/blob/master/core/noscripts/test.js и конфиг для Circle CI https://github.com/roman01la/uix/blob/master/.circleci/config.yml

profit
В Babel есть два плагина оптимизирующие React компоненты.

Первый инлайнит вызов React.createElement в объект, описывающий ноду в виртуальном DOM, который по сути в рантайме возвращается этим вызовом https://babeljs.io/docs/en/babel-plugin-transform-react-inline-elements

Второй плагин выносит константные React элементы за пределы компонента. Таким образом они создаются только один раз, а не на каждый вызов компонента, но висят в памяти всегда https://babeljs.io/docs/en/babel-plugin-transform-react-constant-elements

Для своей кложурной обертки с Hiccup было несложно сделать инлайнинг элементов, он по сути такой же, как и в том плагине. А с хоистингом элементов вышло интереснее.

В компиляторе ClojureScript есть оптимизация константных значений, сейчас только для keyword и symbol, когда все эти значения выносятся в отдельный неймспейс и по ссылке шарятся в местах использования в коде.

Оказалось несложно включиться в этот механизм и добавить хоистинг константных элементов.

Таким образом имея несколько идентичных кусков Hiccup в разных компонентах и из разных неймспейсов, в скомпилированном JS этот код будет заменен ссылкой на один и тот же элемент.
Недавно наткнулся на минималистичную имплементацию инкрементальных вычислений Adapton https://arxiv.org/abs/1609.05337

Портировал код из пейпера в cljc https://github.com/roman01la/adapton и использую в качестве базы для альтернативы re-frame в UIx https://github.com/roman01la/uix/blob/master/core/src/xframe/core/alpha.cljc

Adapton сам по себе не имеет ничего общего с re-frame и co, но с помощью его примитивов можно строить граф зависимых вычислений (подписок), в котором вершиной будет состояние приложения.

Примитивов там всего два:
Вычисление (thunk) — это функция обернутая в инстанс Adapton ноды.
Adapton ref — мутабельная нода, в которую можно складывать состояние (например как atom).

Таким образом при изменении состояния Adapton инвалидирует зависимые ноды в графе, UIx вызывает ноды на которые подписан UI в данный момент и происходит перевычисление цепочек нод в графе. Важно, что вычисляются только зависимости активных нод в UI. Поэтому ноды которые использутся в других частях приложения, которые в данный момент не отрисованы, не будут обновлены пока не отрисуется необходимый UI.

Так же перевычисление нод не означает, что код в них будет выполняться. Adapton мемоизирует ноды. С другой стороны т.к. там используется простая мемоизация это означает, что кэш в памяти будет расти постоянно. Что не очень хорошо для UI приложений с долгой пользовательской сессией. Нужно прикручивать какой-нибудь LRU кэш.

Другие реализации этого подхода есть на OCaml https://github.com/janestreet/incremental и Rust https://github.com/salsa-rs/salsa

Salsa например используется в компиляторе Rust для инкрементальной компиляции.
Выделил минимальную реализацию async/await в компиляторе cljs в библиотеку https://github.com/roman01la/cljs-async-await

По факту — это расширение анализатора и компилятора cljs, поэтому использовать эту штуку нерекомендуется. К тому же существуют определенные ограничения описанные в секции limitations в readme.

(require '[async-await.core :refer [async await]])

(defn http-get [url]
(async
(let [response (await (js/fetch url))
json (await (.json response))]
(.log js/console json))))
Мы делаем ивент посвященный Clojure, 7-8 декабря, в Киеве. Регистрация уже открыта, больше информации на сайте https://clojureday.in.ua/
В ClojureScript есть type inference, хотя язык динамический, который используется для генерирования externs, вместо того чтобы писать их руками, и для генерирования оптимального кода.

Тип можно определить вручную, с помощью type hints, которые добавляют значение в поле :tag в метаданных этого значения

(def ^number x 1)

Благодаря type inference часто явный type hint не нужен. Компилятор сам распознает и добавит тип значения.

Помимо тегов значений есть теги возвращаемых значений из функции

(defn ^js/Promise http-get [url]
(js/Promise. ...))

Для (.then (http-get url) on-ok) компилятор использует информацию о возвращаемоем типе чтобы сгенерировать extern Promise.prototype.then, который позже будет использован в Closure Compiler для генерирования корректного интероп кода в процессе оптимизации.

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

(defn f [] "123")

Благодаря тому, что возвращаемые типы пробрасываются через функции, например имея в кодовой базе одну функцию которая возвращает Promise, автоматический externs inference покроет все использования этой функции и компилятор сгенерирует корректный интероп код.

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

Например известно, что (str 1 2) вернёт string, но тип (apply str [1 2]) уже неизвестен. apply здесь — функция высшего порядка, которая вызовет str и тогда уже вернёт строку.

Поэтому я начал работать над патчем для return type inference в функциях высшего порядка. По факту это абстрактное выполнение кода на этапе компиляции, когда компилятор повторяет как конкретная функция отрабатывает в рантайме.

Например зная, что str всегда возвращает строку, можно утверждать, что (apply str xs) тоже вернёт строку.

Интереснее становиться, когда у multiarity функций методы имеют разные типы возвращаемых значений.

(defn f
([a b] (+ a b))
([a b c] (str a b c))

В примере выше f имеет тип #{number string}, но зная количество аргументов на этапе компиляции, можно определить конкретный тип.

Например для (apply f a b c xs) можно утверждать, что возвращаемый тип будет string, но никак не number.

Эту информацию можно использовать чтобы определить несовпадение количества аргументов с арностью методов. Для (apply f a b c d xs) можно точно сказать, что f не имеет метода с арностью 4 или больше.

Type hints можно использовать, чтобы помочь компилятору сгенерировать оптимальный код. В целом разница будет заметна только для hot path кода.

Например в проверках на правдивость механизм приведения типов в ClojureScript отличается от JS. Поэтому тестовое выражение будет обернуто в вызов функции, которая приведет тип согласно этому механизму.

(when x true)

if (cljs.core._truth(x)) {
true
}

Или например аргументы в str будут обернуты в вызов функции которая приводит значение к строке

(str a b "s")

[str(b), str(a), "s"].join("")

Если в первом случае x действительно Boolean и во втором случае a и b строки, то об этом можно сообщить компилятору с помощью type hints и сгенерированный код будет более оптимальным.

(when ^boolean x true)

if (x) {
true
}

(str ^string a ^string b "s")

[a, b, "s"].join("")
Более интересные оптимизации, так называемые intrinsics, используют type inference для переписывания кода в более оптимальный для конкретного типа данных.

Например для выражения (count x), где известно, что x имеет тип string или array, компилятор сгенирирует x.length, вместо рантайм вызова протокола ICounted. Или для выражения (:x rec), где rec это Record, компилятор сгенирирует rec.x, вместо доступа к полю через вызов протокола.

Но такие оптимизация не всегда дают прирост в производительности.

Например, на первый взгляд (apply str a b xs) будет медленнее, чем аналогичная конструкция (clojure.string/join (cons a (cons b xs))), потому что apply соберёт аргументы редьюсом, а потом str соберёт строку в цикле. В то время, как str/join соберёт строку в одном цикле. Выходит, что первый вариант в 2-3 раза медленнее.

Однако, прогнав скомпилированный код мы увидим, что вариант с apply наоборот, в 2-3 раза быстрее str/join. Причина в том, что функция str/join достаточно коротка, чтобы компилятор заинлайнил ее в месте вызова (что говорит о том, что такой код будет ещё быстрее), но из-за отличий правил скоупинга в ClojureScript от JS заинлайненый код будет обернут в самовызывающуюся функцию. И тогда выходит, что на каждый вызов такого выражения будет создана новая функция, что и бьёт по производительности.
Начал серию скринкастов о разработке нативного UI под десктоп на ClojureScript, React, Node и Qt. Стек технологий достаточно необычный, поэтому должно быть интересно. Тема затрагивает нативные аддоны для Node.js, кастомный reconciler для React и разработку UI на cljs. https://www.youtube.com/user/roman01la
Итак, новости спустя почти два года:

В конце 2019го мы запустили бету, в конце 2020го запустились для всех и недавно отпраздновали первый год после релиза.

Где-то перед релизом поняли, что нужно вкладывать больше времени в производительность приложения, по итогам решили создать отдельную инфраструктурную команду. Я переключился на роль «менеджер/все ещё нужно писать код». Сейчас ищу больше инженеров в Team Performance, в основном занимаемся производительностью фронтенда. Пишите в личку если вам интересно этим заняться, расскажу подробнее. Про перфоманс позже напишу ещё сюда, за год накопилась информация которой хочется поделиться.
О перфомансе во фронтенде

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

Пользовательские ивенты отслеживаются через RUM (Real User Monitoring) в DataDog. В основном это перфоманс метрики различных действий, которые производят пользователи. Например: открытие презентации, переключение слайдов, перетаскивание блоков или печатание текста в редакторе. Каждая из метрик имеет набор трешхолдов в формате Good, Needs improvement и Poor. Отслеживая эти метрики мы понимаем для кого и когда приложение тормозит. Бывает на графике сразу видно, как после очередного релиза линия ползет вверх, значит в релиз попала регрессия. В идеале для этого нужны синтетические перф тесты, которые бы блокировали PRы, так же, как остальные виды тестов, но практика показывает, что в браузерном окружении разброс результатов достаточно большой, т.е. на конкретном пулл-реквесте практически невозможно определить отклонение, если это конечно не очевидная регрессия, но такое бывает редко.

Большинство перф метрик замеряют время от пользовательского действия (нажатия клавишей мыши или клавиатуры) до момента, когда желаемый результат отобразился на экране. Под это описание попадают практически все действия пользователя: навигация, создание или удаление чего либо, открытие/закрытие окон и т.д.

Такие метрики стартуют запись времени с помощью performance.mark() внутри обработчика события и останавливают вызывая performance.measure() в useLayoutEffect хуке в реакте, в компоненте который должен появится на экране. Именно этот хук вызывает колбек после того, как браузер отрисовал изменение на экране.

Временные промежутки записанные с помощью mark/measure отображаются в Performance панели в Chrome DevTools. Это сильно упрощает дебаг конкретного действия, т.к. эти фреймы отображаются вместе с JS стек трейсом.

В DataDog метрики выглядят вот так
Я не добавлял комментарии в этот канал, так что если есть вопросы, пишите в личку. Отвечу туда же или постом в канал 👍
Сегодня узнал, что в Performance панели в Chrome DevTools можно замерить время декодирования и отрисовки изображений. Что это такое? Скажем после того, как изображение загружено браузером из сети в память, массив данных нужно распарсить, сделать декомпрессию (большинство растровых форматов сжимают изображения для уменьшения размера файла) и отрисовать данные, на CPU или GPU.

Каждый новый формат имеет преимущества над более старым. Допустим размер файла в WEBP обычно меньше, чем JPEG, но не всегда. AVIF еще меньше WEBP, но опять же не всегда, и создает больше визуальных артефактов т.к. алгоритм сжатия построен поверх кодека для сжатия видео, где артефакты менее заметны глазу. Собственно из-за этих отличий в алгоритмах декодирвания и заключается разница во времени обработки изображения браузером.

Прогнав JPEG, WEBP и AVIF разных размеров и настроек качества я увидел, что быстрее всего декодируется JPEG, AVIF примерно в 2 раза медленее, а WEBP где-то между ними, иногда ближе к AVIF. Еще одна особенность WEBP (или того, как это делает Chrome) в том, что изображение высокого разрешения (по крайней мере 3x5k) декодируется и отрисовывается прогрессивно, сверху вниз, как прогрессивный JPEG.

Когда это может быть важным? Когда важно показывать изображение без задержки. Например в Pitch при переключении слайдов мы заметили задержку в отрисовке уже предзагруженных изображений. Обошли это путем рендеринга изображений для ближайших слайдов в скрытых DOM элементах.
Вітаю. Через чотири місяці після початку війни я та мій друг трохи зібрали свої кабіни і вирішили запустити подкаст на ютубі та тг канал під нього. Приєдуйтесь, там ми будемо робити айтішний контент та збирати донати на ЗСУ. Сподіваюсь наш контент допоможе вам відволіктись від всього піздеца хоча б на півгодини кожного тижня.
https://news.1rj.ru/str/droptarget
https://www.youtube.com/channel/UCtdUJ9f7v43dQaciP42P40Q