Русский
Русский
English
Статистика
Реклама

Быстрые серверы

Перевод Send My Произвольная передача данных по сети Apple Find My

07.06.2021 12:06:42 | Автор: admin
  • Можно загружать произвольные данные сустройств, не подключенных к интернету, широковещательно посылая сообщения Find My по технологии BLE (Bluetooth с низким энергопотреблением)на расположенные поблизости устройства Apple, которые затем загружают данные для вас
  • Мы выпустили прошивку дляESP32, превращающую микроконтроллер в модем (работающий только на загрузку) и приложение дляmacOS, предназначенное для извлечения, декодирования и отображения загруженных данных: https://github.com/positive-security/send-my
  • В силу природы той парадигмы приватности и безопасности, на которых основан дизайн системы оффлайн-поиска Find My, представляетсямаловероятным, что такое злоупотребление ею можно будет полностью предотвратить




Find My modem (ESP32) // Модем Find My (ESP32)

Nearby Apple Devices // Расположенные поблизости устройства с Apple

Mobile Tower or Wifi Access Point // Сотовая вышка или точка доступа Wifi

macOS device with data retrieval app // Устройство macOS с приложением для извлечения данных

Data flow // поток данных

.

Введение


После состоявшегося недавно релиза технологии AirTags от Apple я заинтересовался, можно ли злоупотреблять системой оффлайнового поиска Find My, чтобы загружать в Интернет произвольные данные с устройств, не подключенных к WiFi или мобильному интернету. Эти данные могли бы широковещательно транслироваться по Bluetooth с низким энергопотреблением и подхватываться устройствами Apple, расположенными поблизости. Эти устройства, стоит им подключиться к Интернету, сразу переправляли бы эти данные на сервера Apple, откуда их впоследствии можно извлечь. Такой прием мог бы использоваться небольшими сенсорами в неконтролируемых окружениях, чтобы не тратить лишнюю энергию на использование мобильного Интернета. Кроме того, она могла бы быть интересна для кражи данных из мест, защищенных клеткой Фарадея, стоит туда только заглянуть человеку с айфоном.

Теоретически такое должно быть возможным: если удастся эмулировать два AirTags, то можно закодировать данные, активировав в конкретный момент времени лишь один из них. В таком случае устройство-получатель должно проверить, какой AirTag был активен в какое время, и декодировать данные обратно в исходный вид. Однако такая схема представляется исключительно ненадежной и, пожалуй, непригодной к использованию в реальных практических ситуациях по причине очень узкой полосы передачи данных (особенно с учетом ограничения в 16 AirTag на Apple IDпредставлялось, что объем передаваемых данных не может превышать нескольких бит в час).

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


Результат: извлечение ранее переданных/загруженных данных при помощи специального приложения Mac

Описание сети оффлайнового поиска


К счастью, этот протокол уже был в значительной степени воспроизведен методом реверс-инжиниринга группой исследователей из Дармштадтского Технического Университета, опубликовавших статью Who CanFind MyDevices? в марте 2021 и для проверки концепции выпустивших реализациюOpenHaystack с открытым исходным кодом. При помощи этой реализации можно создавать собственные компоненты, отслеживаемые в сети Apple Find My. Огромная благодарность этой команде! Именно благодаря их работе смогла состояться наша, и на OpenHaystack основана как наша прошивка (также сделанная для проверки концепции), так и приложение для Mac.

В слегка упрощенном виде принцип работы оффлайновой системы поиска Find My таков:

  1. При сочетании метки AirTag с устройством Apple, совместно генерируется пара ключей на эллиптических кривых, причем, открытый ключ остается на AirTag (а также используется разделенный секрет для генерации сменяющихся открытых ключей)
  2. Каждые 2 секунды AirTag посылает широковещательное сообщение по низкоэнергетическому протоколу Bluetooth, причем, содержимым этого сообщения является открытый ключ (детерминированно меняется каждые 15 минут при помощи заранее разделенного секрета)
  3. Расположенные поблизости айфоны, макбуки, т.д., распознают широковещательное сообщение Find My, определяют свое текущее местоположение, шифруют его широковещательно переданным открытым ключом (при помощи механизмаECIES) и загружают на сервер зашифрованный отчет о местоположении
  4. В ходе поиска устройств сопряженное Устройство Владельца генерирует список сменных открытых ключей, которые AirTag, должно быть, использовал в последние дни, и запрашивает у сервиса Apple их хеши SHA256. Бэкенд Apple возвращает зашифрованные отчеты о местоположении для тех ключей, чьи идентификаторы были запрошены.
  5. Устройство владельца дешифрует отчеты о местоположении и выводит приблизительное местоположение.



Apple's servers // Серверы Apple

Finder devices // Ищущие устройства

Owner device // Устройство владельца

Lost device // Потерянное устройство

  1. // Сопряжение при начальной настройке
  2. // Широковещание. Представление Bluetooth с открытым ключом
  3. // Загрузка зашифрованных отчетов о местоположении
  4. // Скачивание и дешифрование отчетов о местоположении

Рис. 1. Упрощенная схема потока задач, решаемых при оффлайновом нахождении устройств

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

  • Защита от отслеживания, направленная против находящихся поблизости злоумышленников, обеспечивается сменными открытыми ключами
  • Невозможность определить местоположения пользователей Apple

Однако для нас в данном случае наиболее интересно вот что: Apple не знает, какие открытые ключи относятся к вашему AirTag, и, соответственно, какие отчеты о местоположении адресованы вам. Таким образом, оконечная точка, запрашивая отчеты о местоположении по конкретному id ключа, не выполняет при этом никакой авторизации (но вы должны аутентифицироваться по вашему Apple ID, чтобы получить доступ к оконечной точке).

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

Проектирование протокола кражи данных


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

В следующем разделе будем считать бекенд Apple разделяемым хранилищем значений открытых ключей, где в качестве ключа применяется хеш SHA256, а в качестве значения зашифрованный отчет о местоположении, причем, основные операции таковы:

  • Можно проверить, существуют ли отчеты о местоположении для конкретного хеша whether SHA256
  • Можно добавить отчеты о местоположении к конкретному хешу SHA256, широковещательно транслируя соответствующий открытый ключ

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

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

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

Итак, отправляя конкретный бит, мы создаем 28-байтный массив вида [4b индекс бита] [4b ID сообщения] [4b ID модема] [заполнение нулями...] [значение бита], оперируем им как открытым ключом и отправляем представления BLE, например, для широковещательной передачи информации бит 0 сообщения 0 равен 1.

Для отправки полного сообщения программа просто перебирает все свои биты и отправляет по представлению на бит с открытым ключом, в котором зашифрованы индекс и значение этого бита.



Message to encode // Сообщение, которое нужно закодировать

Generated public key to broadcast // Сгенерированный открытый ключ для широковещательной передачи

Bit index // Индекс бита

Bit value // Значение бита



Кодирование бит сообщения в широковещательно передаваемую полезную нагрузку

При выборке данных приложение-получатель будет генерировать такие же 28-байтные массивы (по два на бит, где возможными значениями бита являются 0 и 1) и запрашивать сервис Apple с хешами SHA256 этих открытых ключей. Только к одному из этих ключей должны быть прикреплены отчеты о местоположении, что затем поддается интерпретации (напр., бит с индексом 0 равен 1).



Potential bits to query // Биты, которые потенциально можно запрашивать

Potential public keys to test // Открытые ключи, которые потенциально можно протестировать

Query Apple Backend // Запрос к бекенду Apple

Decode public key existence back to original data // Расшифровываем открытый ключ, получая исходные данные


Извлечение ранее отправленных данных с устройства с macOS, подключенного к Интернету

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

Реализация


Сторона отправителя


На стороне отправителя я решил использовать ESP32 совершенно обычный и дешевый микроконтроллер (а быстрый тест показал, что он может менять адрес BT MAC гораздо быстрее, чем, скажем, Raspberry Pi, основанная на Linux). При загрузке прошивка, основанная на OpenHaystack, широковещательно передает жестко закодированное заданное по умолчанию сообщение, а затем слушает последовательный интерфейс (в виде цикла), не поступят ли какие-то новые данные для широковещательной передачи и так продолжается, пока не будет получено новое сообщение. При широковещательной передаче открытого ключа его требуется разделить на части и закодировать первые 6 байт в адресе Bluetooth MAC (все биты кроме первых двух, поскольку стандарт Bluetooth требует, чтобы первые два бита были установлены в 1). Отсылаем вас к разделу 6.2 из статьи Дармштадтского Технического Университета,где эта самодельная кодировка описана подробнее.

Я добавил к моей полезной нагрузке статический префикс, чтобы не нарваться на проблемы со спецификацией BT, а также включил инкрементный битовый индекс в первые 6 байт открытого ключа, в результате чего получилось так, что у каждого передаваемого бита свой адрес BT MAC, просто на случай, если где-нибудь в стеке стоит ограничение частоты, основанное на MAC-адресах.


Вывод модема ESP32 в последовательной консолиа

Сторона извлечения данных


Приложение для Mac также основано на OpenHaystack и использует тот же фокус с плагином AppleMail, чтобы отправлять к бекенду Apple правильно аутентифицированные запросы на извлечение местоположения. Пользователь получает приглашение ввести 4-байтный ID модема (может быть установлен в процессе прошивки ESP), после чего приложение автоматически выберет, декодирует и отобразит сообщение с id 0. Затем пользователь может выбрать другие сообщения или сменить модем.

Сообщение выбирается по 16 байт (128 бит) за раз (при этом запрашивается 256 id ключей), пока не останется ненайденных отчетов (за целый байт).

Небольшое осложнение: срок действия открытого ключа


Реализовав и сторону отправителя, и сторону получателя, я провел первый тест, широковещательно передав и попытавшись получить 32-битное значение. Спустя пару минут, я смог достать 23 из 32 бит, каждый однозначно верный и примерно с 100 отчетами о местоположении, но не получил ни одного такого отчета на оставшиеся 9 бит.

Заподозрил, что некоторые из сгенерированных открытых ключей были отклонены расположенными поблизости устройствами Apple на этапе шифрования ECIES как недействительные открытые ключи и это удалось быстро подтвердить, попытавшись загрузить каждую из сгенерированных полезных нагрузок как закодированные при помощи SEC-1 открытые ключи на кривой P224 при помощи пакета Python fastecdsa: для каждого бита, по которому у меня не нашлось отчетов о местоположении, микроконтроллер широковещательно передал открытый ключ, выбрасывающий исключение InvalidSEC1PublicKey при импорте ключа fastecdsa.

Небольшой контекст применяемого здесь шифрования:

  • 28-байтный EC public представляет координату X конкретной точки, закодированную при помощи SEC1.
  • У открытого ключа SEC1 обычно также есть знаковый бит, от которого зависит, какая из двух возможных координат Y для данной конкретной координаты X должна кодироваться. Этот бит при широковещании не передается и не важен применительно к сроку действия открытого ключа
  • При декодировании сжатого открытого ключа соответствующая координата Y вычисляется с использованием фиксированных параметров кривой и проверяется, действителен ключ. Некоторые сгенерированные открытые ключи этот тест не проходят. Подробнее об этом рассказано в разделе 3.2.2 в статье Валидация открытых ключей на эллиптических кривых:



Такая проблема с недействительными открытыми ключами решается, как минимум, двумя способами:

  1. Перед широковещательной отправкой полезной нагрузки проверьте, на самом ли деле та точка эллиптической кривой, что ей соответствует, является валидной для используемой кривой. Если нет, то увеличивайте счетчик на единицу до тех пор, пока не найдется действительный открытый ключ. Это детерминированный процесс, схожим образом он выполним и оффлайн при помощи приложения для извлечения данных, перед тем, как запрашивать id ключа
  2. Интерпретировать полезную нагрузку как закрытый ключ (а не открытый). Тогда как сжатый 28-байтный открытый ключ интерпретируется как координата X потенциальной точки на кривой, 28-байтный закрытый ключ интерпретируется как скаляр, участвующий в умножении точки эллиптической кривой на скаляр и, следовательно, всегда результирует в точку, действительно расположенную на кривой (открытый ключ)

Преимущество второго варианта заключается в том, что на каждый полученный бит мы также могли бы дешифровать и отчеты о местоположении, чтобы определить, в какой точке был получен данный бит, но для этого требуется несколько больше вычислительной обработки. Реализуя такой вариант, я обнаружил, что из-забагов в реализации умножения эллиптических кривыхв используемой для этого библиотеке uECC, для некоторых закрытых ключей ESP вычисляла иные открытые ключи, нежели BoringSSL на Mac и fastecdsa на Python (случайно вкрался дифференциальный фаззинг?). Эти открытые ключи даже считались недействительными собственной функцией uECC uECC_valid_public_key(). Следовательно, в этом пилотном проекте я остановился на варианте 1.



Message to encode // Кодируемое сообщение

Generate public key to encode // Генерируем открытый ключ для кодирования

BT MAC address // адрес BT MAC

Test validity, else increment counter // Проверить, действителен ли ключ, если нет увеличить счетчик на единицу

Advertisement payload // Полезная нагрузка для представления

Кодирование и отправка сообщения

Тестирование / Производительность


Когда у нас реализована проверка валидности открытого ключа, все работает безупречно. Я не делал обширного тестирования производительности и не вдавался в измерения, но вот некоторые оценки:

  • Скорость передачина микроконтроллере в настоящее время составляет ~3 байт в секунду. Можно было бы достичь и более высоких скоростей, просто кэшируя результаты кодирования или кодируя всего один байт на каждое представление
  • В моих тестахскорость приемабыла ограничена из-за медленного аппаратного обеспечения Mac. На извлечение16 байтодним запросом уходит~5 секунд
  • Задержкаобычно составляетот 1 до 60 минут, в зависимости от того, сколько вокруг устройств, и от других случайных факторов. На следующем графике показано распределение задержки между широковещательной передачей открытого ключа и загрузкой соответствующего отчета о местоположении. Правда, отметьте, что кривая построена по данным о загрузке отдельных отчетов, и не дает точных данных о том, когда именно такой отчет можно будет скачать (чтобы это определить, обычно достаточно уже первого отчета о местоположении от любых расположенных поблизости устройств Apple)



CDF // Кумулятивная функция распределения

Median min // Медиана 26,3 мин.

Publication delay (min) // Задержка до публикации (мин.)

Рис. 8. Задержки при поступлении всех отчетов, рассмотренных в 7.1 как кумулятивная функция распределения


Задержки при поступлении отчетов, измеренные командой из Дармштадского Технического Университета, по материалам Who can Find My Devices?

Потенциальные способы применения


Притом, что в основном мне было интересно проверить осуществимость описанного, полагаю, потенциально наиболее распространенное практическое применение загрузка показаний датчиков или любых данныхсустройств IoT без широкополосного модема, SIM-карты, тарифного плана или Wifi-соединяемости. Учитывая, что Amazonиспользует подобную сеть под названием Sidewalk, использующую устройства Echo, на такие технологии вполне может быть спрос. Поскольку в кэше ищущих устройств скапливаются полученные широковещательные сообщения, оставаясь там, пока не будет установлено подключение к Интернету, датчики могут отправлять данные даже из районов, где нет покрытия мобильными сетями, если в таких местах ходят люди с устройствами.

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

Также представляется, что протокол оффлайнового обнаружения может использоваться для истощения трафика на расположенных поблизости айфонах, подключенных к небезлимитным тарифам. При ограничении количества отчетов о местоположении, которые можно отправить с одного ищущего устройства (255 отчетов/отправок, поскольку счетный байт равен 1), а также притом, что размер каждого отчета превышает 100 байт, широковещательная передача множества уникальных открытых ключей может приводить к значительному увеличению трафика, исходящего со смартфона. Хотя, я и не заметил никакого ограничения по частоте для исходящих отчетов о местоположении, я также не тестировал, сколько данных при этом может быть потреблено.

Как сгладить проблему


Как я упоминал в самом начале, Apple будет сложно защититься от злоупотреблений такого рода, если они пожелают это сделать.

Apple проектировала систему, исходя из принципа экономии данных. Они не могут читать незашифрованные местоположения и не знают, ни какие открытые ключи относятся к вашему AirTag, ни даже с каким открытым ключом связан конкретный зашифрованный отчет о местоположении (поскольку получают только хеш SHA256 открытого ключа).

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

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

  • Аутентификация представления BLE.В настоящее время ищущие устройства не различают, например, AirTag и его клон на основе OpenHaystack, тем самым допуская спуфинг в виде многих тысяч несуществующих AirTag для кодирования и передачи данных. Можно попробовать подписывать открытые ключи, но, учитывая, что размер представления BLE уже исчерпан, отметим, что AirTag низкоэнергетические и не подключены к Интернету, а широковещательно передаваемые ключи постоянно проходят ротацию, так что задача не из легких.
  • Ограничение частоты при получении отчетов о местоположении Тогда как Apple не знает, связан ли id запрошенного ключа к id одного из запрашивающих пользовательских AirTag, можно кэшировать id запрошенных ключей и обеспечивать, чтобы в течение 15 минут можно было запросить не более 16 новых id ключей и Apple ID (но для первичного поиска в последнее время допустимое количество id стало гораздо больше). Притом, что такой подход реализовать проще, здесь есть лазейка: использовать по принципу карусели множество свободных Apple ID и задействовать их для извлечения данных.


Заключение


В этой статье мы ответили на вопрос о том, можно ли загружать произвольные данные при помощи устройств с Apple, принадлежащих другим пользователям: однозначно да.

Реализована прошивка модема ESP32 и приложение для извлечения данных под macOS, они выложены на Github,можете с ними поэкспериментировать.

Обращаю внимание, что эта реализация лишь доказательство осуществимости, и сам протокол ни шифруется, ни аутентифицируется. Например, можно исследовать данные модема с ID 0x42424242, просто введя его ID (может быть, тем временем кто-то также сможет продемонстрировать, что в этом протоколе отсутствует аутентификация ).

Последнее замечание: Готовя этот пост, я заметил, что байт состояния, включаемый в представление BLE очевидно используется, например, как индикатор уровня заряда. В сочетании с детерминированно генерируемыми закрытыми ключами, которые проходят ротацию, здесь, вероятно, открывается еще одна брешь для утечки данных (по байту на представление), но этот подход я не тестировал.



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Оптимизируем производительность JavaScript (V8) vs AssemblyScript (WebAssembly)

28.04.2021 14:06:12 | Автор: admin


Чтобы повысить производительность web-приложений, используйте WebAssembly в связке с AssemblyScript, чтобы переписать критически важные для производительности компоненты web-приложения, написанные на JavaScript. И это действительно поможет?, спросите вы.

К сожалению, на этот вопрос нет чёткого ответа. Всё зависит от того, как вы будете их использовать. Вариантов много: в одних случаях ответ будет отрицательный, в других положительный. В одной ситуации лучше выбрать JavaScript вместо AssemblyScript, а в другой наоборот. На это влияет множество различных условий.

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

Кто я такой и почему занимаюсь этой темой?


(Данный раздел можете пропустить, это несущественно для понимания дальнейшего материала).

Мне по-настоящему нравится язык AssemblyScript. Я даже в какой-то момент начал помогать разработчикам финансово. У них небольшая команда, в которой каждый серьёзно увлечён этим проектом. AssemblyScript очень молодой язык, похожий на TypeScript и способный компилироваться в WebAssembly (Wasm). Именно в этом и заключается одно из его преимуществ. Раньше, чтобы использовать Wasm, web-разработчик должен был учить чуждые ему языки типа С, C++, C#, Go или Rust, так как в WebAssembly изначально могли компилироваться именно такие высокоуровневые языки со статической типизацией.

Хотя AssemblyScript (ASC) и похож на TypeScript (TS), он не связан с этим языком и не компилируется в JS. Схожесть в синтаксисе и семантике нужна, чтобы облегчить процесс портирования с TS на ASC. Такое портирование в основном сводится к добавлению аннотаций типов.

Мне всегда было интересно взять код на JS, портировать его на ASC, скомпилировать в Wasm и сравнить производительность. Когда мой коллега Ингвар прислал мне фрагмент JavaScript-кода для размытия изображений, я решил использовать его. Я провёл небольшой эксперимент, чтобы понять, стоит ли более глубоко изучать эту тему. Оказалось, стоит. В результате появилась эта статья.

Чтобы лучше познакомиться с AssemblyScript, можно изучить материалы на официальном сайте, присоединиться к каналу в Discord или посмотреть вводное видео на моём Youtube-канале. А мы идём дальше.

Преимущества WebAssembly


Как я уже писал выше, долгое время главной задачей Wasm оставалась возможность компиляции кода, написанного на высокоуровневых языках общего назначения. Например, в Squoosh (онлайн-инструмент для обработки изображений) мы используем библиотеки из экосистемы C/C ++ и Rust. Изначально эти библиотеки не были предназначены для использования в web-приложениях, но благодаря WebAssembly это стало возможным.

Кроме того, согласно распространённому мнению, компиляция исходного кода в Wasm нужна ещё и потому, что использование Wasm-бинарников позволяет ускорить работу web-приложения. Я соглашусь, как минимум, с тем, что в идеальных (лабораторных) условиях WebAssembly и JavaScript-бинарники могут обеспечить примерно равные значения пиковой производительности. Вряд ли это возможно на боевых web-проектах.

На мой взгляд, разумнее рассматривать WebAssembly как один инструментов оптимизации средних, рабочих значений производительности. Хотя недавно у Wasm появилась возможность использовать SIMD-инструкции и потоки с разделяемой памятью. Это должно повысить его конкурентоспособность. Но в любом случае, как я писал выше, всё зависит от конкретной ситуации и начальных условий.

Ниже рассмотрим несколько таких условий:

Отсутствие разогрева


JS-движок V8 обрабатывает исходный код и представляет его в виде абстрактного синтаксического дерева (АСТ). Основываясь на построенном АСТ, оптимизированный интерпретатор Ignition генерирует байткод. Полученный байткод забирает компилятор Sparkplug и на выходе выдаёт пока ещё не оптимизированный машинный код, с большим объёмом футпринта. В процессе исполнения кода V8 собирает информацию о формах (типах) используемых объектов и затем запускает оптимизирующий компилятор TurboFan. Он формирует низкоуровневые машинные инструкции, оптимизированные для целевой архитектуры с учётом собранной информации об объектах.

С тем, как устроены JS-движки можно разобраться, изучив перевод этой статьи.


Пайплайн JS-движка. Общая схема

С другой стороны, в WebAssembly используется статическая типизация, поэтому из него можно сразу сгенерировать машинный код. У движка V8 есть потоковый компилятор Wasm под названием Liftoff. Он, как и Ignition, помогает быстро подготовить и запустить неоптимизированный код. И после этого просыпается всё тот же TurboFan и оптимизирует машинный код. Он будет работать быстрее, чем после компиляции Liftoff, но для его генерации потребуется больше времени.

Принципиальное отличие пайплайна JavaScript от пайплайна WebAssembly: движку V8 незачем собирать информацию об объектах и типах, так как у Wasm типизация статическая и всё известно заранее. Это экономит время.

Отсутствие деоптимизации


Машинный код, который TurboFan генерирует для JavaScript, можно использовать только до тех пор, пока сохраняются предположения о типах. Допустим, TurboFan сгенерировал машинный код, например, для функции f с числовым параметром. Тогда, встретив вызов этой функции с объектом вместо числа, движок опять задействует Ignition или Sparkplug. Это называется деоптимизацией.

У WebAssembly в процессе выполнения программы типы измениться не могут. Поэтому в такой деоптимизации нет необходимости. А сами типы, которые поддерживает Wasm, органично переводятся в машинный код.

Минимизация бинарников для больших проектов


Wasm изначально спроектирован с учётом формата компактных бинарных файлов. Поэтому такие бинарники быстро загружаются. Но во многих случаях они всё-таки получаются больше, чем хотелось бы (по крайней мере, с точки зрения объёмов, принятых в сети). Однако с помощью gzip или brotli эти файлы хорошо сжимаются.

За годы своей жизни JavaScript много чего научился делать из коробки: массивы, объекты, словари, итераторы, обработка строк, прототипное наследование и так далее. Всё это встроено в его движок. А язык С++, например, может похвастаться гораздо большим размахом. И каждый раз, когда вы используете любую из таких абстракций языка при компиляции в WebAssembly, соответствующий код из-под капота должен быть включен в ваш бинарный файл. Это одна из причин разрастания двоичных файлов WebAssembly.

Wasm толком ничего не знает про C++ (или любой другой язык). Поэтому среда исполнения Wasm не предоставляет стандартную библиотеку C++ и компилятору её приходится добавлять в каждый бинарный файл. Но такой код достаточно подключить всего один раз. Поэтому для более крупных проектов это не сильно влияет на результирующий размер Wasm-бинарника, который в итоге часто оказывается меньше других бинарников.

Понятно что, не во всех случаях можно принять взвешенное решение, сравнивая только размеры бинарников. Если, например, исходный код на AssemblyScript скомпилировать в Wasm, то бинарник действительно получится очень компактным. Но насколько быстро он будет работать? Я поставил перед собой задачу сравнить разные варианты JS- и ASC-бинарников сразу по двум критериям скорость работы и размер.

Портирование на AssemblyScript


Как я уже писал, TypeScript и ASC сильно похожи по синтаксису и семантике. Легко предположить, что есть сходство и с JS.Поэтому портирование в основном сводится к добавлению аннотаций типов (или к замене типов). Для начала портируем glur, JS-библиотеку для размытия изображений.

Сопоставление типов данных


Встроенные типы AssemblyScript реализованы по аналогии с типами виртуальной машины Wasm (WebAssembly VM). Если в TypeScript, например, тип Number реализован как 64-битное число с плавающей запятой (по стандарту IEEE754), то в ASC есть целый ряд числовых типов: u8, u16, u32, i8, i16, i32, f32 и f64. Кроме того, в стандартной библиотеке AssemblyScript можно обнаружить распространённые составные типы данных (string, Array, ArrayBuffer, Uint8Array и так далее), которые, с определёнными оговорками, присутствуют в TypeScript и JavaScript. Рассматривать здесь таблицы соответствия типов AssemblyScript, TypeScript и Wasm VM я не буду, это тема другой статьи. Единственное, что хочу отметить: в ASC реализован тип StaticArray, для которого я не нашёл аналогов в JS и WebAssembly VM.

Переходим, наконец, к нашему примеру кода из библиотеки glur.

JavaScript:

function gaussCoef(sigma) {if (sigma < 0.5)sigma = 0.5;var a = Math.exp(0.726 * 0.726) / sigma;/* ... more math ... */return new Float32Array([a0, a1, a2, a3,b1, b2,left_corner, right_corner]);}AssemblyScript:function gaussCoef(sigma: f32): Float32Array {if (sigma < 0.5)sigma = 0.5;let a: f32 = Mathf.exp(0.726 * 0.726) / sigma;/* ... more math ... */const r = new Float32Array(8);const v = [a0, a1, a2, a3,b1, b2,left_corner, right_corner];for (let i = 0; i < v.length; i++) {r[i] = v[i];}return r;}


Фрагмент кода на AssemblyScript содержит дополнительный цикл в конце, так как нет возможности инициализировать массив через конструктор. В ASC не реализована перегрузка функций, поэтому в данном случае у нас есть только один конструктор Float32Array (lengthOfArray: i32). В AssemblyScript есть callback-функции, но отсутствуют замыкания, поэтому нет возможности использовать .forEach() для заполнения массива значениями. Вот и пришлось использовать обычный цикл for для копирования по одному элементу.

Возможно вы заметили, что во фрагменте кода наAssemblyScript я вызываю функции не из библиотеки Math, а из Mathf. Дело в том, что первая предназначена для 64-битных чисел с плавающей запятой, а вторая для 32-битных. Я мог бы использовать Math и каждый раз выполнять приведение типов. Но операции для чисел с двойной точностью всё-таки работают чуть медленнее, а мне это не нужно, так как всюду использую тип f32. Хотя в принципе можно было сделать и так. В данном случае это не является узким местом.

На всякий случай: следите за знаками


Мне потребовалось много времени, чтобы понять: выбор типов очень важен. Размытие изображения включает операции свёртки, а это целая куча циклов for, пробегающих все пиксели. Наивно было думать, что, если все индексы пикселей положительны, счётчики цикла тоже будут положительными. Зря я выбрал для них тип u32 (32-битное целое без знака). Если какой-либо из этих циклов будет бежать в обратном направлении, он станет бесконечным (программа зациклится):

let j: u32;// ... many many lines of code ...for (j = width  1; j >= 0; j--) {// ...}


Других сложностей при портировании я не обнаружил.

Бенчмарки с командной оболочкой d8


Хорошо, фрагменты кода на двух языках готовы. Теперь можно компилировать ASC в Wasm и запускать первые тесты производительности.

Несколько слов про d8: это командная оболочка для движка V8 (сам он не имеет своего интерфейса), позволяющая выполнять все необходимые действия как с Wasm, так и с JS. В принципе, d8 можно сравнить с Node, которому вдруг отрубили стандартную библиотеку и остался только чистый ECMAScript. Если у вас нет скомпилированной версии V8 на локале (как её скомпилировать описано здесь), использовать d8 вы не сможете. Чтобы установить d8, используйте инструмент jsvu.

Однако, поскольку в заголовке этого раздела есть слово Бенчмарки, я считаю важным дать здесь своего рода дисклеймер: полученные мною цифры и результаты относятся к коду, который я написал на выбранных мною языках, запущенном на моем компьютере ( MacBook Air M1 2020 года), используя созданные мной тестовые скрипты. Результаты в лучшем случае являются приблизительными ориентирами. Поэтому было бы опрометчиво на их основе давать обобщённые количественные оценки производительности AssemblyScript с WebAssembly или JavaScript с V8.

У вас может возникнуть ещё один вопрос: почему я выбрал d8 и не стал запускать скрипты в браузере или Node? Я считаю, что и браузер, и Node, скажем так, недостаточно стерильны для моих экспериментов. Помимо необходимой стерильности, d8 даёт возможность управлять пайплайном движка V8. Я могу зафиксировать любой сценарий оптимизации и использовать, например, только Ignition, только Sparkplug или Liftoff, чтобы характеристики производительности не изменились в середине теста.

Методика эксперимента


Как я уже писал выше, у нас есть возможность разогреть движок JavaScript перед запуском теста производительности. В процессе такого разогрева V8 делает необходимую оптимизацию. Поэтому я запускал программу размытия изображения 5 раз, прежде чем начать измерения, затем выполнял 50 запусков и игнорировал 5 самых быстрых и самых медленных прогонов, чтобы удалить потенциальные выбросы и слишком сильные отклонения в цифрах.

Посмотрите, что получилось:



С одной стороны, я обрадовался, что Liftoff выдал более быстрый код в сравнении с Ignition и Sparkplug. Но то, что AssemblyScript, скомпилированный в Wasm с применением оптимизации, оказался в несколько раз медленнее связки JavaScript TurboFan, меня озадачило.

Хотя позже я всё-таки признал, что силы изначально не равны: над JS и его движком V8 много лет работает огромная команда инженеров, реализующих оптимизацию и другие умные вещи. AssemblyScript относительно молодой проект с небольшой командой. Компилятор ASC, сам по себе, однопроходный и перекладывает все усилия по оптимизации на библиотеку Binaryen. Это означает, что оптимизация выполняется на уровне байт-кода Wasm VM после того, как большая часть семантики высокого уровня уже скомпилирована. V8 имеет здесь явное преимущество. Однако код размытия очень прост это обычные арифметические действия со значениями из памяти. Казалось, с этой задачей ASC и Wasm должны были справиться лучше. В чём же тут дело?

Копнём глубже


Я быстро проконсультировался с умными ребятами из команды V8 и с не менее умными парнями из команды AssemblyScript (спасибо Дэниелу и Максу!). Выяснилось, что при компиляции ASC не запускается проверка границ (граничных значений).

V8 может в любой момент посмотреть исходный JS-код и разобраться в его семантике. Он использует эту информацию для повторной или дополнительной оптимизации. Например, у вас есть объект типа ArrayBuffer, содержащий набор бинарных данных. В этом случае V8 ожидает, что наиболее разумным будет не просто хаотично бегать по ячейкам памяти, а использовать итератор посредством цикла for ...of.

for (<i>variable</i> of <i>iterableObject</i>) {<i>statement</i>}

Семантика этого оператора гарантирует, что мы никогда не выйдем за границы массива. Соответственно, компилятор TurboFan не занимается проверкой граничных значений. Но перед компиляцией ASC в Wasm семантика языка AssemblyScript не используется для такой оптимизации: вся оптимизация выполняется на уровне виртуальной машины WebAssembly.

К счастью, у ASC всё-таки есть козырь в рукаве аннотация unchecked(). Она указывает на то, какие значения нужно проверять на возможность выхода за границы.

prev_prev_out_r = prev_src_r * coeff[6];

line[line_index] = prev_out_r;

Предыдущие 2 строки нужно переписать так:

+ prev_prev_out_r = prev_src_r * unchecked(coeff[6]);

+ unchecked(line[line_index] = prev_out_r);

Да, есть кое-что ещё. В AssemblyScript типизированные массивы (Uint8Array, Float32Array и так далее) реализованы по образу и подобию ArrayBuffera. Однако из-за отсутствия высокоуровневой оптимизации для доступа к элементу массива с индексом i каждый раз приходится обращаться к памяти дважды: первый раз для загрузки указателя на первый элемент массива и второй раз для загрузки элемента по смещению i*sizeOfType. То есть приходится обращаться к массиву как к буферу (через указатель). В случае с JS чаще всего этого не происходит, так как V8 удаётся сделать высокоуровневую оптимизацию доступа к массиву, используя однократное обращение к памяти.

Для повышения производительности в AssemblyScript реализованы статические массивы (StaticArray). Они похожи на Array, только имеют фиксированную длину. А следовательно пропадает необходимость хранения указателя на первый элемент массива. Если есть возможность используйте эти массивы для ускорения работы ваших программ.

Итак, я взял связку AssemblyScript TurboFan (она работала быстрее) и назвал её naive. Затем я добавил к ней две оптимизации, о которых говорил в этом разделе, и получил вариант под названием optimized.



Намного лучше! Мы существенно продвинулись. Хотя AssemblyScript всё ещё работает медленнее, чем JavaScript. Неужели это всё, что мы можем сделать? [спойлер: нет]

Ох уж эти умолчания


Ребята из команды AssemblyScript также рассказали мне, что флаг --optimize эквивалентен -O3s. Он хорошо оптимизирует скорость работы, но не доводит её до максимума, так как одновременно препятствует разрастанию бинарного файла. Флаг -O3 оптимизирует только скорость и делает это до конца. Использовать -O3s по умолчанию вроде бы правильно, так как в web-разработке принято сокращать размеры бинарников, но стоит ли оно того? По крайней мере, в этом конкретном примере ответ отрицательный: -O3s экономит ничтожные ~ 30 байт, но закрывает глаза на существенное падение производительности:



Один единственный флаг оптимизатора просто переворачивает игру: наконец-то, AssemblyScript обогнал JavaScript (в этом конкретном тестовом примере!).

Я больше не буду указывать в таблице флаг O3, но будьте уверены: отныне и до конца статьи он будет с нами незримо.

Сортировка методом пузырька


Чтобы убедиться, что пример с размытием изображения не просто случайность, я решил взять ещё один. Я взял реализацию сортировки на StackOverflow и проделал тот же процесс:

  • портировал код, добавив типы;
  • запустил тест;
  • оптимизировал;
  • снова запустил тест.

(Создание и заполнение массива для пузырьковой сортировки я не тестировал).



Мы сделали это снова! На этот раз с ещё большим выигрышем по скорости: оптимизированный AssemblyScript почти в два раза быстрее, чем JavaScript. Но и это ещё не всё. Далее меня опять ждут взлёты и падения. Пожалуйста, не переключайтесь!

Управление памятью


Некоторые из вас, возможно, заметили, что в обоих этих примерах толком не показана работа с памятью. В JavaScript V8 берёт на себя всё управление памятью (и сборку мусора) за вас. В WebAssembly, с другой стороны, вы получаете кусок линейной памяти, и вам нужно решить, как его использовать (или, скорее, Wasm должен решить). Насколько сильно изменится наша таблица, если мы будем интенсивно использовать динамическую память?

Я решил взять новый пример с реализацией двоичной кучи. В процессе тестирования я заполняю кучу 1 миллионом случайных чисел (любезно предоставленных Math.random()) и pop() возвращает их обратно, проверяя, находятся ли числа в порядке возрастания. Общая схема работы осталась той же: портировать JS-код в ASC, скомпилировать с конфигурацией naive, запустить тесты, оптимизировать и снова запустить тесты:



В 80 раз медленнее, чем JavaScript с TurboFan?! И в 6 раз медленнее, чем с Ignition! Что же пошло не так?

Настройка среды исполнения


Все данные, которые мы генерируем в AssemblyScript, должны храниться в памяти. Но нам нужно убедиться, что мы не перезаписываем что-либо ещё, что уже там хранится. Поскольку AssemblyScript стремится подражать поведению JavaScript, у него тоже есть сборщик мусора и при компиляции он добавляет этот сборщик в модуль WebAssembly. ASC не хочет, чтобы вы беспокоились о том, когда выделять, а когда освобождать память.

В таком режиме (он называется incremental) он работает по умолчанию. При этом в Wasm-модуль добавляется всего около 2 КБ в архиве gzip. AssemblyScript также предлагает альтернативные режимы minimal и stub. Режимы можно выбирать с помощью флага --runtime. Minimal использует тот же аллокатор памяти, но более лёгковесный сборщик мусора, который не запускается автоматически, а должен быть вызван вручную. Это может понадобиться при разработке высокопроизводительных приложений (например, игр), где вы хотите контролировать момент, когда сборщик мусора приостановит вашу программу. В режиме stub в Wasm-модуль добавляется очень мало кода (~ 400 Б в формате gzip). Он работает быстро, так как используется резервный аллокатор (подробнее про аллокаторы написано здесь).

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

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



И minimal, и stub резко приблизили нас к уровню производительности JavaScript. Интересно, почему? Как упоминалось выше, minimal и incremental используют один и тот же аллокатор. У обоих также есть сборщик мусора, но minimal не запускает его, если он не вызван явно (а я его как раз не вызываю). Значит, всё дело в том, что incremental запускает сборку мусора автоматически, и зачастую делает это без необходимости. Ну и зачем это нужно, если он должен отслеживать всего лишь один массив?

Проблема с распределением памяти


Несколько раз запустив Wasm-модуль в режиме отладки (--debug), я обнаружил, что скорость работы замедляется из-за библиотеки libsystem_platform.dylib. Она содержит примитивы уровня ОС для работы с потоками и управления памятью. Вызовы в эту библиотеку выполняются из __new() и __renew(), которые, в свою очередь, вызываются из Array#push:

[Bottom up (heavy) profile]:

ticks parent name

18670 96.1% /usr/lib/system/libsystem_platform.dylib

13530 72.5% Function: *~lib/rt/itcms/__renew

13530 100.0% Function: *~lib/array/ensureSize

13530 100.0% Function: *~lib/array/Array#push

13530 100.0% Function: *binaryheap_optimized/BinaryHeap#push

13530 100.0% Function: *binaryheap_optimized/push

5119 27.4% Function: *~lib/rt/itcms/__new

5119 100.0% Function: *~lib/rt/itcms/__renew

5119 100.0% Function: *~lib/array/ensureSize

5119 100.0% Function: *~lib/array/Array#push

5119 100.0% Function: *binaryheap_optimized/BinaryHeap#push


Ясно: здесь проблема с управлением памятью. Но JavaScript каким-то образом умудряется быстро обрабатывать постоянно растущий массив. Так почему же этого не может AssemblyScript? К счастью, исходники стандартной библиотеки AssemblyScript есть в открытом доступе, так что давайте взглянем на эту зловещую функцию push () класса Array:

export class Array<T> {// ...push(value: T): i32 {var length = this.length_;var newLength = length + 1;ensureSize(changetype<usize>(this), newLength, alignof<T>());// ...return newLength;}// ...}


Пока всё верно: новая длина массива равна текущей длине, увеличенной на 1. Далее следует вызов функции ensureSize (), чтобы убедиться, что в буфере достаточно места (Capacity) для нового элемента.

function ensureSize(array: usize, minSize: usize, alignLog2: u32): void {// ...if (minSize > <usize>oldCapacity >>> alignLog2) {// ...let newCapacity = minSize << alignLog2;let newData = __renew(oldData, newCapacity);// ...}}


Функция ensureSize (), в свою очередь, проверяет: Capacity меньше, чем новый minSize? Если да, выделяет новый буфер размером minSize с помощью функции__renew(). Это влечёт за собой копирование всех данных из старого буфера в новый буфер. По этой причине наш тест с заполнением массива 1 миллионом значений (один элемент за другим), приводит к перераспределению большого количества памяти и создаёт много мусора.

В других библиотеках (как std::vec в Rust или slices в Go), новый буфер имеет вдвое большую ёмкость (Capacity), чем старый, что помогает сделать процесс работы с памятью не таким затратным и медленным. Я работаю над этой проблемой в ASC, и пока единственное решение это создать собственный CustomArray , с собственной оптимизацией работы с памятью.



Теперь incremental работает так же быстро, как minimal и stub. Но JavaScript в этом тестовом примере всё равно остаётся лидером. Вероятно, я мог бы сделать больше оптимизаций на уровне языка, но это не статья о том, как оптимизировать сам AssemblyScript. Я и так уже погрузился достаточно глубоко.

Есть много простых оптимизаций, которые мог бы сделать за меня компилятор AssemblyScript. С этой целью команда ASC работает над высокоуровневым оптимизатором IR (Intermediate Representation) под названием AIR. Сможет ли это сделать работу быстрее и избавить меня от необходимости каждый раз вручную оптимизировать доступ к массиву? Скорее всего. Будет ли он быстрее, чем JavaScript? Трудно сказать. Но в любом случае мне было интересно побороться за ASC, оценить возможности JS и увидеть, чего может достичь более зрелый язык с очень умными инструментами компиляции.

Rust & C++


Я переписал код на Rust, максимально идиоматично (как мог), и скомпилировал его в WebAssembly. Он оказался быстрее, чем AssemblyScript (naive), но медленнее, чем наш оптимизированный AssemblyScript с CustomArray . Далее я оптимизировал модуль, скомпилированный из Rust примерно по той же схеме, что и AssemblyScript. С такой оптимизацией Wasm-модуль на базе Rust работает быстрее, чем наш оптимизированный AssemblyScript, но всё же медленнее, чем JavaScript.

Я применил тот же подход к C ++, для компиляции в WebAssembly использовал Emscripten. К моему удивлению, даже первый вариант без оптимизации оказался не хуже JavaScript.


Здесь URL картинки нет. Я сам делал скриншот

Версии, помеченные как idiomatic (идиоматические), в любом случае создавались под влиянием исходного кода JS. Я пытался использовать свои знания идиом Rust, C++, но в моей голове прочно сидела установка, что я занимаюсь портированием. Я уверен, что тот, у кого больше опыта в этих языках мог бы реализовать задачу с нуля, и код выглядел бы иначе.

Я почти уверен, что модули с Rust и C ++ могли бы работать ещё быстрее. Но у меня не хватило глубоких знаний этих языков, чтобы выжать из них больше.

Опять про размеры бинарников


Посмотрим на размеры бинарных файлов после сжатия с помощью gzip. По сравнению с Rust и C++, бинарники AssemblyScript действительно намного легче по весу.



И всё-таки рекомендации


Я писал об этом в начале статьи, повторю и сейчас: результаты в лучшем случае являются приблизительными ориентирами. Поэтому было бы опрометчиво на их основе давать обобщённые количественные оценки производительности. Например, нельзя сказать, что Rust во всех случаях в 1,2 раза медленнее JavaScript. Эти числа очень сильно зависят от кода, который я написал, оптимизаций, которые я применил, и машины, которую я использовал.

И всё-таки, я думаю, есть несколько общих рекомендаций, которые мы можем извлечь, чтобы помочь вам лучше разбираться в теме и принимать более обоснованные решения:

  • Компилятор Liftoff в связке с AssemblyScript будет генерировать Wasm-код, который выполняется значительно быстрее, чем код, который выдаёт Ignition или SparkPlug в связке с JavaScript. Если вам нужна производительность и нет времени на разогрев JS-движка, WebAssembly лучший вариант.
  • V8 действительно хорошо компилирует и оптимизирует JavaScript-код. Хотя WebAssembly может работать быстрее, чем JavaScript, вполне вероятно, что для этого вам придётся заниматься оптимизацией вручную.
  • Более медленный язык и компилятор, с годами обросший разнообразными способами оптимизации, даст фору шустрому языку и молодому компилятору.
  • Модули AssemblyScript, как правило, весят намного меньше, чем Wasm-модули, скомпилированные из других языков. В этой статье бинарник с AssemblyScript не был меньше бинарника с JavaScript, но для более крупных модулей ситуация обратная, как утверждает команда разработки ASC.

Если вы мне не верите (а вы и не обязаны) и хотите самостоятельно разобраться в коде тестовых примеров, вот они.



Наши серверы можно использовать для разработки на WebAssembly.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Наиболее полное руководство по практическому использованию Web Speech API

27.05.2021 10:19:54 | Автор: admin

В этой статье я хочу поделиться с вами результатами изучения основных возможностей Web Speech API (далее WSA).


Введение


WSA это экспериментальная технология, состоящая из двух интерфейсов: SpeechSynthesis (интерфейс для перевода текста в речь) и SpeechRecognition (интерфейс для распознавания речи).


О том, что из себя представляют названные интерфейсы и что в себя включают можно почитать на MDN или в рабочем черновике (данный черновик, в отличие от большинства спецификаций, написан более-менее человеческим языком).


Что касается поддержки, то вот что об этом говорит Can I use:




Как мы видим, SpeechSynthesis поддерживается большинством современных браузеров. К сожалению, поддержка SpeechRecognition оставляет желать лучшего. Я говорю "к сожалению", поскольку SpeechRecognition предоставляет более интересные возможности с точки зрения практического применения в веб-приложениях, в чем вы сами убедитесь после прочтения статьи. Полагаю, такая ситуация обусловлена сложностью технической реализации распознавания речи.


Мы с вами реализуем 4 простых приложения, по 2 на каждый интерфейс:


  1. Плеер для озвучивания текста со всеми возможностями, которые предоставляет SpeechSynthesis, включая выбор языка (голоса)
  2. Страницу с возможностью озвучивания ее текстового содержимого
  3. "Диктофон" для распознавания речи и ее перевода в текст с автоматическим и кастомным форматированием
  4. Одностраничное приложение с голосовым управлением, в том числе, переключение страниц, прокрутка и изменение цветовой темы (или схемы), используемой на сайте

Если вам это интересно, то прошу следовать за мной.


Плеер для озвучивания текста


Наша разметка будет выглядеть следующим образом:


<div id="wrapper">  <h1>Speech Synthesis - Player</h1>  <label    >Text:    <textarea id="textarea">Привет! Как дела?</textarea>  </label>  <label    >Voice:    <select id="select"></select>  </label>  <label    >Volume:    <input id="volume" type="range" min="0" max="1" step="0.1" value="1" />    <span>1</span>  </label>  <label    >Rate:    <input id="rate" type="range" min="0" max="3" step="0.5" value="1" />    <span>1</span>  </label>  <label    >Pitch:    <input id="pitch" type="range" min="0" max="2" step="0.5" value="1" />    <span>1</span>  </label>  <div id="buttons">    <button class="speak">Speak</button>    <button class="cancel">Cancel</button>    <button class="pause">Pause</button>    <button class="resume">Resume</button>  </div></div>

У нас имеется поле для ввода текста (textarea), который мы будем озвучивать; выпадающий список (select) для выбора голоса, которым будет озвучиваться текст; инпуты-диапазоны для установки громкости (volume), скорости (rate) воспроизведения и высоты голоса (pitch), а также кнопки для запуска (speak), отмены (cancel), приостановления (pause) и продолжения (resume) воспроизведения. В общем, ничего особенного.


Обратите внимание на атрибуты инпутов min, max, step и value. Значения данных атрибутов взяты из черновика спецификации, но некоторые из них отданы на откуп производителям браузеров, т.е. зависят от конкретной реализации. Также обратите внимание на наличие почти у всех элементов идентификаторов. Мы будем использовать эти id для прямого доступа к элементам в скрипте в целях сокращения кода, однако в реальных приложениях так лучше не делать во избежание загрязнения глобального пространства имен.


Переходим к JavaScript. Создаем экземпляр SpeechSynthesisUtterance ("utterance" можно перевести как "выражение текста словами"):


const U = new SpeechSynthesisUtterance()

Пытаемся получить доступные голоса (именно "пытаемся", поскольку в первый раз, по крайней мере, в Chrome возвращается пустой массив):


let voices = speechSynthesis.getVoices()

При вызове метода getVoices() возникает событие voiceschanged. Обрабатываем это событие для "настоящего" получения голосов и формирования выпадающего списка:


speechSynthesis.onvoiceschanged = () => {  voices = speechSynthesis.getVoices()  populateVoices(voices)}

Объект голоса (назовем его так) выглядит следующим образом:


0: SpeechSynthesisVoice  default: true  lang: "de-DE"  localService: false  name: "Google Deutsch"  voiceURI: "Google Deutsch"

Реализуем функцию формирования выпадающего списка:


function populateVoices(voices) {  // Перебираем голоса и создаем элемент `option` для каждого  // Текстовым содержимым `option` является название голоса, а значением - индекс голоса в массиве голосов  voices.forEach((voice, index) => {    select.options[index] = new Option(voice.name, index)  })  // Делаем голосом по умолчанию `Google русский`  // Он нравится мне больше, чем голос от `Microsoft`  const defaultVoiceIndex = voices.findIndex(    (voice) => voice.name === 'Google русский'  )  select.selectedIndex = defaultVoiceIndex  // После этого инициализируем обработчики событий  initializeHandlers()}

Функция инициализации обработчиков событий выглядит так:


function initializeHandlers() {  // Ниже перечислены почти все события, которые возникают при работе с рассматриваемым интерфейсом  U.onstart = () => console.log('Started')  U.onend = () => console.log('Finished')  U.onerror = (err) => console.error(err)  // Мне не удалось добиться возникновения этих событий  U.onpause = () => console.log('Paused')  U.onresume = () => console.log('Resumed')  // Обработка изменения настроек  wrapper.onchange = ({ target }) => {    if (target.type !== 'range') return    handleChange(target)  }  // Обработка нажатия кнопок  buttons.addEventListener('click', ({ target: { className } }) => {    // SpeechSynthesis предоставляет таким методы как `speak()`, `cancel()`, `pause()` и `resume()`    // Вызов метода `speak()` требует предварительной подготовки    // Кроме того, мы проверяем наличие текста в очереди на озвучивание с помощью атрибута `speaking`    // Есть еще два атрибута: `pending` и `paused`, но они не всегда возвращают корректные значения    switch (className) {      case 'speak':        if (!speechSynthesis.speaking) {          convertTextToSpeech()        }        break      case 'cancel':        return speechSynthesis.cancel()      case 'pause':        return speechSynthesis.pause()      case 'resume':        return speechSynthesis.resume()      default:        return    }  })}

Обработка изменения настроек:


function handleChange(el) {  el.nextElementSibling.textContent = el.value}

Функция преобразования текста в речь:


function convertTextToSpeech() {  // Получаем текст  const trimmed = textarea.value.trim()  if (!trimmed) return  // Передаем его экземпляру `SpeechSynthesisUtterance`  U.text = trimmed  // Получаем выбранный голос  const voice = voices[select.value]  // Передаем голос и другие настройки экземпляру  U.voice = voice  // язык  U.lang = voice.lang  // громкость  U.volume = volume.value  // скорость  U.rate = rate.value  // высота голоса  U.pitch = pitch.value  // Запускаем озвучивание!  speechSynthesis.speak(U)}

После установки всех настроек наш экземпляр SpeechSynthesisUtterance выглядит следующим образом:


SpeechSynthesisUtterance  lang: "ru-RU"  onboundary: null  onend: () => console.log('Finished')  onerror: (err) => console.error(err)  onmark: null  onpause: () => console.log('Paused')  onresume: () => console.log('Resumed')  onstart: () => console.log('Started')  pitch: 1  rate: 1  text: "Привет! Как дела?"  voice: SpeechSynthesisVoice { voiceURI: "Google русский", name: "Google русский", lang: "ru-RU", localService: false, default: false }  volume: 1

Уже в процессе написания статьи я решил добавить управление с помощью клавиатуры:


window.onkeydown = ({ key }) => {  switch (key.toLowerCase()) {    case 's':      if (!speechSynthesis.speaking) {        convertTextToSpeech()      }      break    case 'c':      return speechSynthesis.cancel()    case 'p':      return speechSynthesis.pause()    case 'r':      return speechSynthesis.resume()    default:      return  }}

Поиграть с кодом можно здесь:



Страница с возможность озвучивания текстового содержимого


Что касается SpeechSynthesis, то с точки зрения практического использования данного интерфейса, сложно что-то придумать, кроме создания страниц с возможностью озвучивания размещенного на них текста. Этим мы и займемся.


Преимущество использования SpeechSynthesis заключается в реальном улучшении доступности контента приложения для людей с ограниченными возможностями здоровью. Основная проблема, на мой взгляд, состоит в том, что пользователи не привыкли к подобным приложениям, следовательно, им надо каким-то образом намекнуть о существовании такой возможности. Причем, сделать это нужно в очень ясной, но при этом ненавязчивой форме.


Наша разметка будет выглядеть так:


<div id="wrapper">  <h1>Speech Synthesis - Page Reader</h1>  <div>    <button class="play" tabindex="1"></button>    <p>      JavaScript  мультипарадигменный язык программирования. Поддерживает      объектно-ориентированный, императивный и функциональный стили.      Является реализацией спецификации ECMAScript (стандарт ECMA-262).    </p>  </div>  <div>    <button class="play" tabindex="2"></button>    <p>      JavaScript обычно используется как встраиваемый язык для программного      доступа к объектам приложений. Наиболее широкое применение находит в      браузерах как язык сценариев для придания интерактивности      веб-страницам.    </p>  </div>  <div>    <button class="play" tabindex="3"></button>    <p>      Основные архитектурные черты: динамическая типизация, слабая      типизация, автоматическое управление памятью, прототипное      программирование, функции как объекты первого класса.    </p>  </div></div>

У нас имеется три блока (div) с кнопками для озвучивания текста (play) и, собственно, текстом, который будет озвучиваться (первые три абзаца статьи по JavaScript из Википедии). Обратите внимание, что я добавил кнопкам атрибут tabindex, чтобы переключаться между ними с помощью tab и нажимать с помощью space. Однако, имейте ввиду, что использовать атрибут tabindex не рекомендуется по причине того, что браузер использует переключение фокуса с помощью tab для повышения доступности.


С вашего позволения, я приведу код скрипта целиком:


// Это часть должна быть вам знакома по предыдущему примеруlet voices = speechSynthesis.getVoices()let defaultVoicespeechSynthesis.onvoiceschanged = () => {  voices = speechSynthesis.getVoices()  defaultVoice = voices.find((voice) => voice.name === 'Google русский')  wrapper.addEventListener('click', handleClick)  window.addEventListener('keydown', handleKeydown)}const PLAY = 'play'const PAUSE = 'pause'const RESUME = 'resume'function handleClick({ target }) {  switch (target.className) {    case PLAY:      // При нажатии кнопки `play` в момент озвучивания другого текста,      // нам нужно прекратить текущее озвучивание перед началом нового      speechSynthesis.cancel()      const { textContent } = target.nextElementSibling      // Об этой части см. ниже      textContent.split('.').forEach((text) => {        const trimmed = text.trim()        if (trimmed) {          const U = getUtterance(target, text)          speechSynthesis.speak(U)        }      })      break    case PAUSE:      // CSS-класс кнопки отвечает за отображаемую рядом с ней иконку      // ``- ожидание/стоп, `` - озвучивание/воспроизведение, `` - пауза      // иконка `` используется в качестве индикатора кнопки, находящейся в фокусе      target.className = RESUME      speechSynthesis.pause()      break    case RESUME:      target.className = PAUSE      speechSynthesis.resume()      break    default:      break  }}// При нажатии `escape` прекращаем озвучивание текста// Можете добавить свои контролыfunction handleKeydown({ code }) {  switch (code) {    case 'Escape':      return speechSynthesis.cancel()    default:      break  }}function getUtterance(target, text) {  const U = new SpeechSynthesisUtterance(text)  U.voice = defaultVoice  U.lang = defaultVoice.lang  U.volume = 1  U.rate = 1  U.pitch = 1  // Я специально не стал скрывать смену иконок при начале/окончании воспроизведения  U.onstart = () => {    console.log('Started')    target.className = PAUSE  }  U.onend = () => {    console.log('Finished')    target.className = PLAY  }  U.onerror = (err) => console.error(err)  return U}

Тонкий момент в приведенном коде это преобразование озвучиваемого текста в массив предложений (разделителем является точка (.)), перебор массива, и воспроизведение каждого предложения по отдельности (точнее, помещение всех предложений одного за другим в очередь на озвучивание) textContent.split('.').forEach(...). Причина такого решения заключается в том, что озвучивание длинного текста тихо обрывается примерно на 220 символе (в Chrome). Черновик спецификации для такого случае предусматривает специальную ошибку text-to-long (текст слишком длинный), но данной ошибки не возникает, озвучивание просто резко прекращается, причем, для восстановления работы SpeechSynthesis зачастую приходится перезагружать вкладку (при запущенном сервере для разработки даже это не всегда срабатывает). Возможно, вам удастся найти другое решение.


Поиграть с кодом можно здесь:



"Диктофон" для распознавания речи


После того, как мы обстоятельно рассмотрели SpeechSynthesis, можно переходить ко второму, немного более сложному, но, вместе с тем, и более интересному интерфейсу, входящему в состав WSA SpeechRecoginition.


Вот наша начальная разметка:


<div id="wrapper">  <h1>Speech Recognition - Dictaphone</h1>  <textarea id="final_text" cols="30" rows="10"></textarea>  <input type="text" id="interim_text" />  <div id="buttons">    <button class="start">Старт</button>    <button class="stop">Стоп</button>    <button class="abort">Сброс</button>    <button class="copy">Копия</button>    <button class="clear">Очистить</button>  </div></div>

У нас имеется поле для вставки финального (распознанного) текста (final_text) и поле для вставки промежуточного (находящегося в процессе распознавания) текста (interim_text), а также панель управления (buttons). Выбор элементов textarea и input для хранения текста обусловлен как вариативностью промежуточных результатов, которые меняются на лету, так и необходимостью внесения небольших правок в распознанный текст, что связано с естественным несовершенством перевода устной речи в текст. Стоит отметить, что, в целом, Chrome очень неплохо справляется с задачей распознавания речи и автоматическим форматированием распознанного текста.


Кроме кнопок для управления распознаванием речи (start, stop и abort), мы реализуем возможность копирования распознанного текста в буфер обмена с помощью Clipboard API и очистки соответствующего поля.


Начнем с определения переменных для финального текста и индикатора распознавания:


let final_transcript = ''let recognizing = false

Далее, создаем и настраиваем экземпляр SpeechRecognition:


// Напоминаю, что `WSA`, в целом, и, особенно, `SpeechRecognition` являются экпериментальнымиconst speechRecognition =  window.SpeechRecognition || window.webkitSpeechRecognition// Создаем экземпляр `SpeechRecognition`const recognition = new speechRecognition()// Свойство `continuous` определяет, продолжается ли распознавание речи после получения первого финального результатаrecognition.continuous = true// обработка промежуточных результатовrecognition.interimResults = true// максимальное количество альтернатив распознанного словаrecognition.maxAlternatives = 3// языкrecognition.lang = 'ru-RU'

Добавляем обработчики событий запуска, ошибки и окончания распознавания речи:


recognition.onstart = () => {  console.log('Распознавание голоса запущено')}recognition.onerror = ({ error }) => {  console.error(error)}recognition.onend = () => {  console.log('Распознавание голоса закончено')  // Снова запускаем распознавание, если индикатор имеет значение `true`  if (!recognizing) return  recognition.start()}

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


const DICTIONARY = {  точка: '.',  запятая: ',',  вопрос: '?',  восклицание: '!',  двоеточие: ':',  тире: '-',  абзац: '\n',  отступ: '\t'}

Предполагается, что для решение задач, связанных с форматированием, будут использоваться специальные объекты SpeechGrammar и SpeechGrammarList, а также специальный синтаксис JSpeech Grammar Format, однако, в данной статье мы ограничимся обычным объектом.


Вы можете добавить в словарь любые пары ключ/значение, которые посчитаете нужными. Обратите внимание на то, что все ключи в нашем словаре представлены одним словом. Дело в том, что рассматриваемый интерфейс плохо справляется с ключами, которые состоят более чем из одного слова, например, "вопросительный знак", "восклицательный знак" и т.п. Я понимаю, что пара вопрос: '?' не лучшее решение, но для примера сойдет.


Также нам потребуются две функции для редактирования промежуточного и финального результатов:


function editInterim(s) {  return s    .split(' ')    .map((word) => {      word = word.trim()      return DICTIONARY[word.toLowerCase()] ? DICTIONARY[word.toLowerCase()] : word    })    .join(' ')}function editFinal(s) {  return s.replace(/\s{1,}([\.+,?!:-])/g, '$1')}

Функция editInterim() разбивает фразу на массив слов, перебирает слова, удаляет пробелы в начале и конце слова и заменяет символом из словаря при совпадении. Обратите внимание, что мы приводим слово в нижний регистр только при поиске совпадения. Если мы сделаем это в строке word = word.trim(), то нивелируем автоматическую "капитализацию" строки, выполняемую браузером.


Функция editFinal() удаляет пробел перед каждым из указанных символов мы разделяем слова пробелами при объединении в функции editInterim(). Следует отметить, что по умолчанию каждое распознанное слово с двух сторон обрамляется пробелами.


Итак, событие, которое интересует нас больше всего это result. Реализуем его обработку:


recognition.onresult = (e) => {  // Промежуточные результаты обновляются на каждом цикле распознавания  let interim_transcript = ''  // Перебираем результаты с того места, на котором остановились в прошлый раз  for (let i = e.resultIndex; i < e.results.length; i++) {    // Атрибут `isFinal` является индикатором того, что речь закончилась    if (e.results[i].isFinal) {      // Редактируем промежуточные результаты      const result = editInterim(e.results[i][0].transcript)      // и добавляем их к финальному результату      final_transcript += result    } else {      // В противном случае, записываем распознанные слова в промежуточный результат      interim_transcript += e.results[i][0].transcript    }  }   // Записываем промежуточные результаты в `input`  interim_text.value = interim_transcript  // Редактируем финальный результат  final_transcript = editFinal(final_transcript)  // и записываем его в `textarea`  final_text.value = final_transcript}

Событие распознавания выглядит следующим образом:


SpeechRecognitionEvent  bubbles: false  cancelBubble: false  cancelable: false  composed: false  currentTarget: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, }  defaultPrevented: false  emma: null  eventPhase: 0  interpretation: null  isTrusted: true  path: []  resultIndex: 1  // здесь нас интересуют только результаты  results: SpeechRecognitionResultList {0: SpeechRecognitionResult, 1: SpeechRecognitionResult, length: 2}  returnValue: true  srcElement: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, }  target: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, }  timeStamp: 59862.61999979615  type: "result"

Результаты (SpeechRecognitionResultList) выглядят так:


results: SpeechRecognitionResultList  0: SpeechRecognitionResult    0: SpeechRecognitionAlternative      confidence: 0.7990190982818604      transcript: "привет"    isFinal: true    length: 1  length: 1

Вот почему для получения результата мы обращаемся к e.results[i][0].transcript. На самом деле, поскольку мы указали maxAlternatives = 3, во всех результатах будет представлено по три SpeechRecognitionAlternative. Первым (с индексом 0) всегда будет наиболее подходящий результат с точки зрения браузера.


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


buttons.onclick = ({ target }) => {  switch (target.className) {    case 'start':      // Обнуляем переменную для финального результата      final_transcript = ''      // Запускаем распознавание      recognition.start()      // Устанавливаем индикатор распознавания в значение `true`      recognizing = true      // Очищаем `textarea`      final_text.value = ''      // Очищаем `input`      interim_text.value = ''      break    case 'stop':      // Останавливаем распознавание      recognition.stop()      // Устанавливаем значение индикатора распознавания в значение `false`      recognizing = false      break    case 'abort':      // Прекращаем распознавание      recognition.abort()      recognizing = false      break    case 'copy':      // Копируем текст из `textarea` в буфер обмена      navigator.clipboard.writeText(final_text.value)      // Сообщаем об этом пользователю      target.textContent = 'Готово'      const timerId = setTimeout(() => {        target.textContent = 'Копия'        clearTimeout(timerId)      }, 3000)      break    case 'clear':      // Обнуляем переменную для финального результата      final_transcript = ''      // Очищаем `textarea`      final_text.value = ''      break    default:      break  }}

Тестируем. Нажимаем на кнопку "Старт", дожидаемся, когда красная точка рядом с фавиконкой перестанет мигать (о готовности к распознаванию можно сообщать пользователю через обработку события speechstart), произносим фразу (произношение должно быть максимально четким и ясным), например, "привет точка как дела вопрос". В input на какое-то время появляется "Привет точка Как дела вопрос", затем этот промежуточный результат редактируется и переносится в textarea в виде "Привет. Как дела?". Отлично, наша машина нас понимает.


Поиграть с кодом можно здесь:



Подумал о том, что было бы неплохо реализовать функцию для удаления последнего распознанного слова на тот случай, если результат распознавания получился некорректным. Для этого потребуется пара удалить: () => removeLastWord() (если добавить эту пару в словарь, то потребуется дополнительная проверка typeof DICTIONARY[word] === 'function') и примерно такая операция:


function removeLastWord() {  const oldStr = final_text.value  const newStr = oldStr.substring(0, oldStr.lastIndexOf(' '))  final_text.value = newStr}

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


Одностраничное приложение с голосовым управлением


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


Мы будем генерировать страницы программным способом, поэтому разметка нам не потребуется.


Создаем директорию pages с тремя файлами:


// home.jsexport default /*html*/ `<div id="wrapper">  <div>Section 1</div>  <div>Section 2</div>  <div>Section 3</div>  <div>Section 4</div>  <div>Section 5</div>  <div>Section 6</div>  <div>Section 7</div>  <div>Section 8</div>  <div>Section 9</div></div>`// product.jsexport default /*html*/ `<h1>This is the Product Page</h1>`// about.jsexport default /*html*/ `<h1>This is the About Page</h1>`

Основной скрипт начинается с импорта страниц и генерации домашней страницы:


import HomePage from './pages/home.js'import ProductPage from './pages/product.js'import AboutPage from './pages/about.js'const { body } = documentbody.innerHTML = HomePage

Нам потребуются константы для прокрутки страницы и объект с операциями:


// Константы для прокруткиconst DOWN = 'down'const UP = 'up'const RIGHT = 'right'const LEFT = 'left'// Операцииconst ACTIONS = {  // операции переключения страниц  home: () => (body.innerHTML = HomePage),  product: () => (body.innerHTML = ProductPage),  about: () => (body.innerHTML = AboutPage),  // операции прокрутки  down: () => scroll(DOWN),  up: () => scroll(UP),  left: () => scroll(LEFT),  right: () => scroll(RIGHT),  // операции переключения цветовой темы  light: () => body.removeAttribute('class'),  dark: () => (body.className = 'dark')}

Далее следуют настройки SpeechRecognition и обработчики событий начала, ошибки и окончания распознавания, аналогичные рассмотренным в предыдущем разделе (кроме настройки языка для управления страницей мы будем использовать американский английский: recognition.lang = 'en-US').


Обработка события result:


recognition.onresult = (e) => {  // Перебираем результаты  for (let i = e.resultIndex; i < e.results.length; i++) {    if (e.results[i].isFinal) {      const result = e.results[i][0].transcript.toLowerCase()      // Выводим слова в консоль, чтобы убедиться в корректности распознавания (как выяснилось, слово `product` очень плохо распознается, возможно, у меня проблемы с его правильным произношением)      console.log(result)      // Преобразуем фразу в массив, перебираем слова и выполняем соответствующие операции      result        .split(' ')        .forEach((word) => {          word = word.trim().toLowerCase()          // ACTION[word] - это функция          return ACTIONS[word] ? ACTIONS[word]() : ''        })    }  }}

Функция для выполнения прокрутки выглядит следующим образом:


function scroll(direction) {  let newPosition  switch (direction) {    case DOWN:      newPosition = scrollY + innerHeight      break    case UP:      newPosition = scrollY - innerHeight      break    case RIGHT:      newPosition = scrollX + innerWidth      break    case LEFT:      newPosition = scrollX - innerWidth      break    default:      break  }  if (direction === DOWN || direction === UP) {    scrollTo({      top: newPosition,      behavior: 'smooth'    })  } else {    scrollTo({      left: newPosition,      behavior: 'smooth'    })  }}

Наконец, добавляем обработку нажатия клавиш клавиатуры:


window.addEventListener('keydown', (e) => {  e.preventDefault()  switch (e.code) {    // Нажатие пробела запускает распознавание    case 'Space':      recognition.start()      recognizing = true      break    // Нажатие `escape` останавливает распознавание    case 'Escape':      recognition.stop()      recognizing = false    default:      break  }})

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


  • home, product, about переключение страниц
  • dark, light переключение цветовой темы
  • down, up, left, right выполнение прокрутки к соответствующему разделу страницы (имеет видимый эффект только на домашней странице, иногда приходится добавлять слово scroll, например, scroll down)

Поиграть с кодом можно здесь:



Заключение


Как мы видим, WSA существенно расширяет арсенал JavaScript, связанный с добавлением динамики, "оживлением" веб-страниц. Сложно сказать, насколько широкое распространение получит данная технология и насколько активно она будет использоваться. Несмотря на то, что WSA это технология общего назначения, очевидно, что она внесет серьезный вклад, в первую очередь, в развитие так называемого доступного Интернета.


Мы с вами рассмотрели все основные возможности, предоставляемые WSA. Единственное, что мы не сделали, это не реализовали совместное использование SpeechSynthesis и SpeechRecognition. Первое, что приходит на ум это голосовой помощник, который реагирует на ключевые слова, встречающиеся во фразе, произнесенной пользователем, отвечая определенными шаблонами и задавая уточняющие вопросы, при необходимости. Более простой вариант: волшебный шар задаешь любой вопрос, предполагающий однозначный ответ, программа какое-то время "думает" и отвечает да, нет или, например, не знаю, спросите позже. Пусть это будет вашим домашним заданием, все необходимые знания для этого у вас имеются. Обязательно поделитесь результатом в комментариях.




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Как использовать GraphQL Federation для инкрементальной миграции с монолита (Python) на микросервисы (Go)

26.05.2021 16:17:30 | Автор: admin
Или как поменять фундамент старого дома, чтобы он не обвалился



Лет 10 назад мы выбрали 2-ю версию Python для разработки нашей обучающей платформы с монолитной архитектурой. Но с тех пор индустрия существенно изменилась. Python 2 был официально похоронен 1 января 2020 года. В предыдущей статье мы объясняли, почему решили отказаться от миграции на Python 3.

Каждый месяц нашей платформой пользуются миллионы людей.

Мы пошли на определённый риск, когда решили переписать наш бэкенд на Go и изменить архитектуру.

Язык Go мы выбрали по нескольким причинам:

  1. Высокая скорость компиляции.
  2. Экономия оперативной памяти.
  3. Достаточно широкий выбор IDE с поддержкой Go.

Но мы применили подход, который позволил минимизировать риск.

GraphQL Federation

Мы решили построить нашу новую архитектуру вокруг GraphQL Apollo Federation. GraphQL был создан разработчиками Facebook как альтернатива REST API. Федерация это построение единого шлюза для нескольких сервисов. Каждый сервис может иметь свою GraphQL-схему. Общий шлюз объединяет их схемы, генерирует единое API и позволяет выполнять запросы для нескольких сервисов одновременно.

Прежде чем, пойдём дальше, хотелось бы особо отметить следующее:

  1. В отличие от REST API, у каждого GraphQL-сервера есть собственная типизированная схема данных. Она позволяет получить любые комбинации именно тех данных с произвольными полями, которые вам нужны.

  2. Шлюз REST API позволяет отправить запрос только одному бэкенд-сервису; шлюз GraphQL генерирует план запросов для произвольного количества бэкенд-сервисов и позволяет вернуть выборки из них в одном общем ответе.

Итак, включив шлюз GraphQL в нашу систему, получим примерно такую картину:


URL картинки: https://lh6.googleusercontent.com/6GBj9z5WVnQnhqI19oNTRncw0LYDJM4U7FpWeGxVMaZlP46IAIcKfYZKTtHcl-bDFomedAoxSa9pFo6pdhL2daxyWNX2ZKVQIgqIIBWHxnXEouzcQhO9_mdf1tODwtti5OEOOFeb

Шлюз (он же сервис graphql-gateway) отвечает за создание плана запросов и отправки GraphQL-запросов другим нашим сервисам не только монолиту. Наши сервисы, написанные на Go, имеют свои собственные GraphQL-схемы. Для формирования ответов на запросы мы используем gqlgen (это GraphQL-библиотека для Go).

Так как GraphQL Federation предоставляет общую GraphQL-схему, а шлюз объединяет все отдельные схемы сервисов в одну, наш монолит будет взаимодействовать с ним так же, как и любой другой сервис. Это принципиальный момент.

Далее пойдёт речь о том, как мы кастомизировали сервер Apollo GraphQL, чтобы безопасно перелезть с нашего монолита (Python) на микросервисную архитектуру (Go).

Side-by-side тестирование


GraphQL мыслит наборами объектов и полей определённых типов. Код, который знает, что делать с входящим запросом, как и какие данные извлечь из полей, называется распознавателем (resolver).

Рассмотрим процесс миграции на примере типа данных для assignments:

123 type Assignment {createdDate: Time.}

Понятно, что в реальности у нас гораздо больше полей, но для каждого поля всё будет выглядеть аналогично.

Допустим, мы хотим, чтобы это поле из монолита было представлено и в нашем новом сервисе, написанном на Go. Как мы можем быть уверены, что новый сервис по запросу вернёт те же данные, что и монолит? Для этого используем подход, аналогичный библиотеке Scientist: запрашиваем данные и у монолита, и у нового сервиса, но затем сравниваем результаты и возвращаем только один из них.

Шаг 1: Режим manual


Когда пользователь запрашивает значение поля createdDate, наш GraphQL-шлюз обращается сначала к монолиту (который, напоминаю, написан на Python).


На первом шаге нам нужно обеспечить возможность добавления поля в новый сервис assignments, уже написанный на Go. В файле с расширением .graphql должен лежать следующий код распознавателя (resolver):

12345 extend type Assignment key(fields: id) {id: ID! externalcreatedDate: Time @migrate(from: python, state: manual)}

Здесь мы используем Федерацию, чтобы сказать, что сервис добавляет поле createdDate к типу Assignment. Доступ к полю происходит по id. Мы также добавляем секретный ингредиент директиву migrate. Мы написали код, который понимает эти директивы и создаёт несколько схем, которые GraphQL-шлюз будет использовать при принятии решения о маршрутизации запроса.

В режиме manual запрос будет адресован только коду монолита. Мы должны предусмотреть эту возможность при разработке нового сервиса. Чтобы получить значение поля createdDate, мы по-прежнему можем обращаться к монолиту напрямую (в режиме primary), а можем запрашивать у GraphQL-шлюза схему в режиме manual. Оба варианта должны работать.

Шаг 2: Режим side-by-side


После того, как мы написали код распознавателя (resolver) для поля createdDate, мы переключаем его в режим side-by-side:

12345 extend type Assignment key(fields: id) {id: ID! externalcreatedDate: Time @migrate(from: python, state: side-by-side)}

И вот теперь шлюз будет обращаться и к монолиту (Python), и к новому сервису (Go). Он будет сравнивать результаты, регистрировать случаи, в которых есть различия, и возвращать пользователю результат, полученный от монолита.

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

В процессе тестирования мы получаем вот такие отчёты.


Эту картинку при вёрстке попытайся увеличить как-то без сильной потери качества.

В них акцент сделан на случаи, когда в работе монолита и нового сервиса обнаруживаются расхождения.

Поначалу мы часто сталкивались с такими случаями. Со временем мы научились выявлять такого рода проблемы, оценивать их на критичность и при необходимости устранять.

При работе с нашими dev-серверами мы используем инструменты, которые выделяют различия цветом. Так легче анализировать проблемы и тестировать их решения.

А что по мутациям?


Возможно, у вас возник вопрос: если мы запускаем одинаковую логику и в Python, и в Go, что произойдет с кодом, который изменяет данные, а не просто запрашивает их? В терминах GraphQL это называется мутациями (mutation).

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

Шаг 2.5: Режим сanary


Если у нас есть поле или мутация, которые успешно дожили до стадии продакшна, мы включаем режим canary (канареечный деплой).

12345 extend type Assignment key(fields: id) {id: ID! externalcreatedDate: Time @migrate(from: python, state: canary)}

Поля и мутации в режиме canary будут добавлены в сервис Go для небольшого процента наших пользователей. Кроме того, канареечную схему тестируют и внутренние пользователи платформы. Это достаточно безопасный способ тестирования сложных изменений. Мы можем быстро отключить канареечную схему, если что-то не работает должным образом.

Мы используем только одну канареечную схему за раз. На практике не так много полей и мутаций одновременно находятся в канареечном режиме. Так что, я думаю, проблем не будет и дальше. Это хороший компромисс, потому что схема довольно велика (более 5000 полей), а экземпляры шлюза должны хранить в памяти три схемы primary, manual и canary.

Шаг 3: Режим migrated


На этом шаге поле createdDate должно перейти в режим migrated:

12345 extend type Assignment key(fields: id) {id: ID! externalcreatedDate: Time @migrate(from: python, state: migrated)}

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

Шаг 4: Завершение миграции


После успешного деплоя нам больше не нужен код монолита для этого поля, и мы удаляем из кода распознавателя (resolver) директиву @migrate:

12345 extend type Assignment key(fields: id) {id: ID! externalcreatedDate: Time}

С этого момента выражение Assignment.createdDate шлюз будет воспринимать как получение значения поля из нового сервиса, написанного на Go.

Вот такая она инкрементальная миграция!

И как далеко шагнули мы?


Мы завершили работу над нашей инфраструктурой side-by-side тестирования только в этом году. Это позволило нам безопасно, медленно, но верно переписать кучу кода на Go. В течение года мы поддерживали высокую доступность платформы на фоне роста объёма трафика в нашей системе. На момент написания этой статьи ~ 40% наших полей GraphQL вынесены в сервисы Go. Так что, описанный нами подход хорошо зарекомендовал себя в процессе миграции.

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

P.S. Стив Коффман делал доклад на эту тему (на Google Open Source Live). Вы можете посмотреть запись этого выступления на YouTube (или просто глянуть презентацию).



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Ахиллесова пята коронавируса

18.05.2021 10:20:20 | Автор: admin
Вирус SARS-CoV-2 критически зависит от особого механизма, обеспечивающего синтез его белков. Коллаборация под руководством исследовательской группы из Высшей Технической Школы Цюриха (ETH Zurich) докопалась до молекулярного устройства этого процесса и показала, что его можно ингибировать специальными химическими соединениями, тем самым существенно подавляя репликацию вируса в инфицированных клетках.


Видео: Said Sannuga, Cellscape.co.uk / ETH Zurich, The Ban Lab

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

Когда клетка здорова, рибосома движется вдоль РНК в строго размеренном темпе, считывая три нуклеотида РНК за шаг. Такой трехбуквенный код определяет соответствующую аминокислоту, которая прикрепляется к наращиваемому белку. Почти не случается такого, чтобы рибосома сдвинула один или два нуклеотида РНК вперед или назад, отступив от этого правильного трехчастного паттерна. Если такой сдвиг в рибосоме (его называют сдвиг рамки считывания) все-таки происходит, то он приводит к ошибкам в считывании генетического кода.

В наших клетках сдвиг рамки считывания почти никогда не случается; из-за этого клеточные белки становились бы нефункциональными. Но жизненный цикл некоторых вирусов, например, коронавирусы и ВИЧ, зависит именно от сдвига рамки считывания в ходе таких событий регулируется уровень вирусных белков. Например, вирус SARS-CoV-2, тот самый, что вызывает COVID-19 критически зависит от сдвига рамки считывания, обеспечиваемого необычной и запутанной сверткой в вирусной РНК.

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

Получена подробная картина процесса, критически важного для репликации коронавируса


Команда исследователей из Высшей Технической Школы Цюриха и из университетов Берна, Лозанны и Корка (Ирландия) впервые смогла выявить взаимодействия между вирусным геномом и рибосомой, происходящие при сдвиге рамки считывания. Результаты их работы были опубликованы в журнале Science.

При помощи филигранных биохимических экспериментов исследователи смогли застать рибосому именно на том сайте РНК-генома SARS-CoV-2, где происходил сдвиг рамки считывания. Этот молекулярный комплекс им удалось изучить при помощи криоэлектронной микроскопии.

По результатам работы удалось с беспрецедентной детализацией дать описание этого процесса и открыть ряд новых свойств, о которых даже не подозревали. При сдвиге рамки считывания рибосомная машина, отличающаяся динамичностью, принимает неестественную конфигурацию, что помогло получить одно из самых резких и точных изображений рибосомы млекопитающих, визуализируемой в процессе сдвига рамки, когда как раз происходит считывание информации из вирусного генома. Далее ученые проследили свои структурные находки, поставив эксперименты in vitro и in vivo и, в частности, как на этот процесс можно нацелить нужные химические соединения. Ненад Бан, профессор молекулярной биологии в Высшей Технической Школе Цюриха и соавтор данного исследования, акцентирует, что представленные здесь результаты, касающиеся SARS-CoV-2, также будут полезны для понимания механизмов сдвига рамки считывания у других РНК-вирусов.

Потенциальная мишень для разработки противовирусных препаратов


Зависимость SARS-CoV-2 от такого события сдвига рамки считывания у рибосомы может пригодиться при разработке противовирусных препаратов. По данным более ранних исследований, известно несколько препаратов, способных ингибировать сдвиг рамки считывания у коронавирусов; но новое исследование теперь позволяет судить о том, как эти соединения влияют на уровни SARS-CoV-2 в инфицированных клетках.

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

Хотя, в настоящее время эти соединения не обладают достаточной мощностью, чтобы применять их в терапевтических целях, данное исследование демонстрирует, что ингибирование рибосомального сдвига рамки считывания коренным образом влияет на репликацию вируса, что открывает путь к разработке более качественных соединений. В силу того, что все коронавирусы зависят от сохранения такого механизма сдвига рамки считывания, препарат, нацеленный на борьбу с этим процессом, может иметь лечебный эффект и при борьбе с другими коронавирусами, отдаленно родственными SARS-CoV-2. Наша дальнейшая работа будет сосредоточена на понимании механизмов клеточной защиты, подавляющих процесс вирусного считывания рамки сдвига, поскольку это может быть полезно для разработки небольших соединений со схожим механизмом действия, говорит Бан.

РНК (желтая) вируса SARS-CoV-2 образует псевдоузловую структуру (разноцветная, справа внизу), которая вызывает сдвиг при движении рамки считывания по рибосоме (коричневая). Таким образом, вирусная РНК контролирует уровни синтеза вирусных белков. Подробнее об исследовании рассказано в видео, ссылка на которое дана выше (Графика: Said Sannuga, Cellscape.co.uk / ETH Zurich, The Ban Lab)





Облачные VDS серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Игра в Нострадамуса

23.05.2021 10:08:18 | Автор: admin

Давайте спрогнозируем, сколько еще, как минимум, осталось жить масочному режиму, Интернету или Хабру? Прикинем на пальцах, ничего не зная, кроме того, сколько времени уже с нами эти явления, а обоснуем свои предсказания нехитрыми трюками из статистики и принципом Коперника.


Принцип Коперника


Теорема о конце света, буквально утверждает, что с 95 % уверенностью мы можем считать, что человеческая раса исчезнет в течение 9120 лет. Конкретный срок не определен точно, но что исчезнет это практически наверняка. Открыл и популяризировал теорему профессор астрофизики Джон Готт.


В основу своих рассуждений Готт положил, что живущие сейчас люди находятся в случайном месте всей хронологии человеческой истории. Это чистая случайность, что мы сейчас живём в 2021 году, и этот год ничем не предпочтительнее любого другого 20 000 года до новой эры, 1315 или 1917. Как положение Земли в солнечной системе не центральное, так и наш 2021 год. Это утверждение Готт назвал принципом Коперника.


Догадка посетила будущего известного ученого в 1969 году после туристического визита в Берлин, где он увидел Берлинскую стену. На тот момент стена стояла уже 8 лет. После несложных выкладок в уме, он сообщил другу, что стена простоит не меньше 2 и не больше 24 лет. Вот почему.


Пусть $t$ время существования явления к настоящему моменту, а $T$ сколько остаётся ему до конца. Считая, что попадание во временную точку t отрезка времени полного существования $t+T$ случайно и равновероятно, имеем случайную величину


$x = \frac{t}{T+t}$


распределённую на отрезке [0, 1] равномерно. В этом случае доверительный интервал, с которым случайная величина $x$ с вероятностью $1-\alpha$ находится внутри отрезка есть


$\frac{\alpha}{2}\leq x \leq 1-\frac{\alpha}{2}$


Выразим $T$ через $t$ и $\alpha$ и получим интервал для времени дальнейшего существования $T$


$\frac{\alpha/2}{1-\alpha/2}t \leq T \leq \left( \frac{2}{\alpha} - 1 \right)t$


С шансами один к одному ($\alpha=0.5$) Готт оценил сколько осталось Берлинской стене:


$\frac{t}{3} \leq T \leq 3t$


Умножил случайное число 8 на 3 и получил, что не более 24 лет. Во всяком случае, располагая такой оценкой уже можно принимать ставки.


Предсказания


Вдохновленный своим открытием, Готт сделал множество прогнозов. Наиболее знаменитый из них та самая Теорема о конце света, опубликованная в журнале Nature в 1993 году. Принцип тот же самый, разве что $\alpha=0.05$, так сказать, чтобы наверняка, с вероятностью ошибки не более 1/20. В роли равномерно распределённой случайной переменной взято отношение $\frac{n}{N}$, где $n$ приблизительное число уже живших и живущих людей на этом свете, а $N$ окончательное число всех, кто поживет за все времена. Оно составит не более $20n$, то есть, если мы примем, что 60 млрд людей родились вплоть до настоящего момента (оценка Лесли), то тогда мы можем сказать, что с уверенностью 95 % общее число людей N будет менее, чем 2060 миллиардов = 1,2 триллиона. Предполагая, что население мира стабилизируется на уровне 10 млрд человек, и средняя продолжительность жизни составит 80 лет, нетрудно посчитать, сколько потребуется времени, чтобы оставшиеся 1140 миллиардов людей родились. А именно, данное рассуждение означает, что с 95 % уверенностью мы можем утверждать, что человеческая раса исчезнет в течение 9120 лет. Так написано в Википедии.


Следом за Готтом, давайте и я притворюсь Нострадамусом и предскажу, что


  • масочный режим (уже длится более 500 дней) вряд ли исчезнет в ближайшие 10 дней,
  • Хабр будет здравствовать никак не меньше еще пяти месяцев,
  • а Интернет не исчезнет минимум год.

Серьезно? Давайте поспорим!


В своей книге J. R. Gott III, Time Travel in Einsteins Universe (Houghton Mifflin, Boston, 2001), Глава. 5. Джон Готт сделал много предсказаний о судьбе государств, политиков, ток-шоу. Журнал The New Yorker посвятил ему статью How to Predict Everything. Казалось бы успех, но как это бывает в науке, критических статей и обзоров вышло еще больше.


Первое очевидное возражение, которое приходит на ум, иллюстрирует комикс xkcd


Не повезло же жителям Берлина, что видели разделяющую стену! Как только её выстроили, они могли бы надеяться, что всё проходит пройдёт и это. В скором времени. А к ближе к её краху считать, что раньше их жизнь закончится.


Серьёзный анализ, который я не воспроизведу здесь, дан в статье Carlton M. Caves // Predicting future duration from present age: Revisiting a critical assessment of Gotts rule, 2008. В сухом остатке: оценка Готта имеет право на жизнь, но лишь в том случае, когда априорная плотность вероятности имеет вид:


$\omega (T)=\frac{1}{T^2}$


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


У многих интересующих нас объектов есть характерный масштаб времени. В среднем собаки живут 10-13 лет, люди 60-80, бабочки день-другой и так далее. "Собачьи года" для собак, календарный год для людей. К ним формула Готта неприменима. Встречаются в жизни и масштабно инвариантные распределения вероятностей, такие как закон Ципфа и другие ранговые распределения.


Последовательное применение принципа Коперника (оценки Готта) означает следующее:


  • взять случайный объект,
  • посмотреть сколько он уже существует,
  • оценить к какому рангу он относится (по порядку величины)

Житейский опыт говорит нам, что если взятый наудачу субъект, скажем, научный сотрудник попавшийся не вовремя на глаза директору, на коварный вопрос "а сколько ты уже пишешь свой труд-то?" отвечает "месяц где-то", то да, вряд ли он сделает всё к завтрашнему дню, но через пару лет уж точно. Оценка по порядку величин, всё нормально.


Заключение


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




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Первое поколение компьютеров от Древнего Рима до Второй Мировой

05.05.2021 10:13:29 | Автор: admin


Лень двигатель прогресса. Стремление человечества хотя бы частично автоматизировать свою деятельность всегда выливалось в различные изобретения. Математические вычисления и подсчеты также не избежали научного прогресса. Ещё в Древнем Риме местные таксисты использовали аналог современного таксометра механическое устройство, которое определяло стоимость поездки в зависимости от длины маршрута. Время шло, и к середине прошлого века эволюция вычислительных систем привела к появлению нового типа устройств компьютеров. Тогда, конечно, их так никто не называл. Для этого использовался другой термин ЭВМ (электронно-вычислительная машина). Но время и прогресс стерли границы между этими определениями. Так как же прогресс дошел до первых ЭВМ и как они работали?

История развития


Арифмометр


Одно из главных событий в истории развития вычислительных систем является изобретение арифмометра. Арифмометр это механическая вычислительная машина, предназначенная для выполнения алгебраических операций. Первая схема такого устройства датируется 1500 годом за авторством Леонардо да Винчи. Вокруг его схемы в 60-х годах 20 века возникло много споров. Доктор Роберто Гуателли, работавший в IBM с 1951 года по проекту воссоздания машин Леонардо да Винчи, в 1968 году создал копию счетной машины по эскизам 16-го века.

Данная машина представляла собой 13-разрядную суммирующую машину.

В следующем году вокруг этой машины начали появляться различные возражения, а именно по поводу её механизма. Существовало мнение о том, что машина да Винчи представляет собой механизм пропорционирования, а не счетную машину. Также возникал вопрос и о её работе: по идее, 1 оборот первой оси вызывает 10 оборотов второй, 100 оборотов третьей и 10 в степени n оборотов n-ной оси. Работа такого механизма не могла осуществляться из-за огромной силы трения. По итогу голоса сторонников и противников счетной машины Леонардо да Винчи разделились, но, тем не менее, IBM решила убрать эту модель из коллекции

Но, оставим наработки Леонардо Да Винчи. Расцвет арифмометров пришелся на 17 век. Первой построенной моделью стал арифмометр Вильгельма Шиккарда в 1623 году. Его машина была 6-разрядной и состояла из 3 блоков множительного устройства, блока сложения-вычитания и блока записи промежуточных результатов.


Копия арифмометра Шиккарда

Также 17 век отметился ещё несколькими арифмометрами: паскалина за авторством Блеза Паскаля, арифмометр Лейбница и машина Сэмюэля Морленда. В промышленных масштабах арифмометры начали производиться в начале 19 века, а распространены были практически до конца 20-го.

Аналитическая и разностная машины Бэббиджа


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

Идея о создании разностной машины не принадлежит Чарльзу Бэббиджу. Она впервые была описана немецким инженером Иоганном Мюллером в книге с очень сложным названием. До конца не ясно, повлияли ли на Бэббиджа идеи Мюллера при создании разностной машины, поскольку Чарльз ознакомился с его работой в переводе, дата создания которого неизвестна.


Книга Иоганна Мюллера

Считается, что основные идеи для создания разностной машины Бэббидж взял из работ Гаспара де Прони и его идей о декомпозиции математических работ. Его идея заключалась в следующем: есть 3 уровня, на каждом из которых математики занимаются решением определенных проблем. На верхнем уровне находятся самые крутые математики и их задача вывод математических выражений, пригодных для расчетов. У математиков на втором уровне стояла задача вычислять значения функций, которые вывели на верхнем уровне, для аргументов, с определенным периодом. Эти значения становились опорными для третьего уровня, задачей которого являлись рутинные расчеты. От них требовалось делать только грамотные вычисления. Их так и называли вычислители. Эта идея навела Бэббиджа на мысль о создании машины, которая могла бы заменить вычислителей. Машина Бэббиджа основывалась на методе аппроксимации функций многочленами и вычисления конечных разностей. Собственно, поэтому машина и называется разностной.

В 1822 году Бэббидж построил модель разностной машины и заручился государственной поддержкой в размере 1500 фунтов стерлингов. Он планировал, что закончит машину в течение 3 лет, но по итогу работа была не завершена и через 9 лет. За это время он получил ещё 15500 фунтов стерлингов в виде субсидий от государства. Но всё же часть машины функционировала и производила довольно точные (>18 знаков после запятой) расчеты.


Созданная на основе работ Бэббиджа разностная машина

Во время работы над разностной машиной у Чарльза Бэббиджа возникла идея о создании аналитической машины универсальной вычислительной машины. Её называют прообразом современного цифрового компьютера, и не зря. Она состояла из арифметического устройства (мельницы), памяти (склада) и устройства ввода-вывода, реализованного с помощью перфокарт различного типа. К сожалению, данная идея осталась лишь на бумаге.


Схема аналитической машины Бэббиджа

Табулятор


История электромеханических машин начинается в 1888 году, когда американский инженер Герман Холлерит, основатель компании CTR (будущая IBM), изобрел электромеханическую счетную машину табулятор, который мог считывать и сортировать данные, закодированные на перфокартах. В аппарате использовались электромагнитные реле, известные еще с 1831 года и до Холлерита не применявшиеся в счетной технике. Управление механическими счетчиками и сортировкой осуществлялось электрическими импульсами, возникающими при замыкании электрической цепи при наличии отверстия в перфокарте. Импульсы использовались и для ввода чисел, и для управления работой машины. Поэтому табулятор Холлерита можно считать первой счетной электромеханической машиной с программным управлением. Машину полностью построили в 1890 году и использовали при переписи населения США в том же году. Впоследствии табуляторы использовались вплоть до 1960-х 1970-х годов в бухгалтерии, учете, обработке данных переписей и подобных работах. И даже если в учреждении имелась полноценная ЭВМ, табуляторы все равно использовали, чтобы не нагружать ЭВМ мелкими задачами.


Табулятор IBM

Электромеханические машины времен ВМВ


Следующий виток в развитии вычислительной техники пришелся на Вторую мировую войну. Расчетные машины использовали для атак на вражеские шифры, расчета баллистики и при разработке сложных видов вооружения (авиация, ядерное оружие).

В 1937 году Клод Шеннон в своей работе A Symbolic Analysis of Relay and Switching Circuits показал, что электронные связи и переключатели могут представлять выражения булевой алгебры. Машины тех лет можно условно на два типа: электромеханические (основанные на электромагнитных переключателях) и электронные (полностью на электровакуумных лампах). К первым относились американский Harvard Mark I и компьютеры немецкого инженера Конрада Цузе.

Mark I


Работа над Mark I началась в 1939 году в Endicott laboratories по субподрядному договору с IBM. В качестве основы использовались наработки Чарльза Бэббиджа. Компьютер последовательно считывал инструкции с перфоленты, условного перехода не было, циклы организовывались в виде склеенных в кольцо кусков перфоленты. Принцип разделения данных и инструкций в Mark I получил известность как Гарвардская архитектура. Машину закончили в 1944 году и передали в ВМФ США. Характеристики:

  • 765 тысяч деталей (электромеханических реле, переключателей и т. п.)
  • Длина 17 м, высота 2.5 м, вес 4.5 тонн
  • Потребляемая мощность 4 кВт
  • Объем памяти 72 числа, состоящих из 23 разрядов (память на десятичных цифровых колесах)
  • Вычислительная мощность 3 операции сложения и вычитания в секунду, 1 операция умножения в 6 секунд, 1 операция деления в 15.3 секунды, логарифм и тригонометрические операции требовали больше минуты.


Mark 1

Z3-Z4


В 1936 немецкий инженер Конрад Цузе начал работу над своим первым вычислителем Z1. Первые две модели из серии Z были демонстративными. Следующий же компьютер, Z3, который закончили в 1941, имел практическое применение: с его помощью делали аэродинамические расчеты (стреловидные крылья самолетов, управляемые ракеты). Машина была выполнена на основе телефонных реле. Инструкции считывались с перфорированной пленки. Так же, как в Mark I, отсутствовали инструкции условного перехода, а циклы реализовывались закольцованной перфолентой. Z3 имел некоторые преимущества перед своими будущими собратьями (ENIAC, Mark I): вычисления производились в двоичной системе, устройство позволяло оперировать числами с плавающей точкой. Так как Цузе изначально исходил из гражданских интересов, его компьютеры более близки к современным, чем тогдашние аналоги. В 1944 году практически был завершен Z4, в котором уже присутствовали инструкции условного перехода. Характеристики Z3:

  • Арифметическое устройство: с плавающей точкой, 22 бита, +, , , /, квадратный корень.
  • Тактовая частота: 5,3 Гц.
  • Средняя скорость вычисления: операция сложения 0,8 секунды; умножения 3 секунды.
  • Хранение программ: внешний считыватель перфоленты.
  • Память: 64 слова с длиной в 22 бита.
  • Ввод: десятичные числа с плавающей запятой.
  • Вывод: десятичные числа с плавающей запятой.
  • Элементов: 2600 реле 600 в арифметическом устройстве и 2000 в устройстве памяти. Мультиплексор для выбора адресов памяти.
  • Потребление энергии: 4 кВт.
  • Масса: 1000 кг.

При постройке Z4 Цузе просил финансирование на замену электромагнитных реле полностью электронными схемами (лампами), но ему отказали. У электромеханических машин имелось два существенных недостатка низкое быстродействие и ограниченная износостойкость контактов (не более 10 млн переключений или 120 суток непрерывной работе при 1 переключении в секунду). Дальнейшая история показала, что перспективный путь это использование электровакуумных ламп.


Z3

Первые ламповые компьютеры


Однозначно определить первый в мире компьютер сложно. Многими учеными определение первого поколения основывается на вычислительной базе из электронных ламп. При этом первое поколение компьютеров разрабатывалось во время Второй мировой войны. Возможно, созданные в то время компьютеры засекречены и по сей день. В целом выделяют два возможных первенца ENIAC и Colossus

ENIAC


Electronic Numerical Integrator and Computer (Электронный числовой интегратор и вычислитель) или ENIAC создавался по заказу от армии США для расчета баллистических таблиц. Изначально, подобные расчеты производились людьми и их скорость не могла соотноситься с масштабом военных действий. Построен компьютер был лишь к осени 1945 года.

Характеристики ENIAC:

  • Вес 30 тонн.
  • Объем памяти 20 число-слов.
  • Потребляемая мощность 174 кВт.
  • Количество электронных ламп 17 468
  • Вычислительная мощность 357 операций умножения или 5000 операций сложения в секунду.
  • Тактовая частота 100 кГц
  • Устройство ввода-вывода данных табулятор перфокарт компании IBM: 125 карт/минуту на ввод, 100 карт/минуту на вывод.


Colossus


Colossus в отличие от ENIAC был очень узконаправленной машиной. Он создавался исключительно с одной целью декодирование немецких сообщений, зашифрованных с помощью Lorenz SZ. Эта машина было схожа с немецкой Enigma, но состояла из большего числа роторов. Для декодирования этих сообщений было решено создать Colossus. Он включал в себя 1500 электронных ламп, потреблял 8,5 КВт и обладал тактовой частотой в 5.8 МГц. Такое значение частоты достигалось за счет того, что Colossus был создан для решения только одной задачи и применяться в других областях не мог. К концу войны на вооружении Британии стояло 10 таких машин. После войны все они были уничтожены, а данные о них засекречены. Только в 2000 году эта информация была рассекречена.


Реконструированная модель Colossus

Принцип работы


Вакуумные лампы


Радиолампа представляет собой стеклянную колбу с электродами, из которой откачан воздух. Простейшая разновидность ламп диод, состоящий из катода и анода, а также спирали, разогревающей катод до температур, при которых начинается термоэлектронная эмиссия. Электроны покидают катод и под действием разности потенциалов притягиваются к аноду. В обратном направлении заряд не переносится, так как заряженных ионов в колбе нет (вакуум). При изменении полярности электроны, покинувшие разогретый электрод, будут притягиваться обратно. До второго электрода они долетать не будут, отталкиваясь от него из-за отрицательного потенциала. Если добавить еще один электрод, то получится триод. В электровакуумном триоде устанавливается сетка между катодом и анодом. При подаче на сетку отрицательного потенциала она начинает отталкивать электроны, не позволяя им достичь анода. При подаче модулированного сигнала ток будет повторять изменения потенциала на сетке, поэтому изначально триоды использовали для усиления сигналов.


Радиолампа и схема триггера на двух триодах

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

Запоминающее устройство


На первых порах развития ЭВМ использовались разные подходы к созданию запоминающих устройств. Помимо памяти на триггерах из радиоламп и на электромагнитных реле (как в Z3) имелись следующие виды:

  • Память на магнитных барабанах
  • Память на электронно-лучевых трубках
  • Память на линиях задержки

Линии задержки


Основная идея линий задержки возникла в ходе разработки радаров во время Второй мировой войны. В первых ЭВМ в качестве линий использовались трубки с ртутью (у нее очень низкое затухание ультразвуковых волн), на концах которой располагались передающий и принимающий пьезокристаллы. Информация подавалась с помощью импульсов, модулированных высокочастотным сигналом. Импульсы распространялись в ртути. Информационная емкость трубки в битах равнялась максимальному количеству одновременно передаваемых импульсов. Единица кодировалось присутствием импульса на определенном месте, ноль отсутствием импульса. Приемный пьезокристалл передавал импульс на передающий информация циркулировала по кругу. Для записи вместо регенерации импульсов вводились записываемые. Такой вид памяти использовался в компьютерах EDVAC, EDSAC и UNIVAC I.


Запоминающее устройство на ртутных акустических линиях задержки в UNIVAC I

Запоминающие электронно-лучевые трубки (трубки Уильямса)


При попадании электронного луча на точку на люминофорном экране происходит вторичная эмиссия и участок люминофора приобретает положительный заряд. Благодаря сопротивлению люминофорного слоя, точка долю секунды держится на экране. Однако, если не отключать луч сразу, а сдвинуть его в сторону от точки, рисуя тире, то электроны, испущенные во время эмиссии, поглощаются точкой, и та приобретает нейтральный заряд. Таким образом, если выделить N точек, то можно записать N бит информации (1 нейтральный заряд, 0 положительный заряд). Для считывания информации используется доска с электродами, прикрепленная к внешней стороне экрана. Электронный луч снова направляется в точку, и та приобретает положительный заряд независимо от изначального. С помощью электрода можно определить величину изначального заряда (значение бита), однако информация уничтожается (после каждого считывания нужна перезапись). Так как люминофор быстро теряет заряд, необходимо постоянно считывать и записывать информацию. Такой вид памяти использовался в Манчестерском Марк I и Ferranti Mark1; американских IBM 701 и 702


Трубка Уильямса

Магнитные барабаны


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


Архитектура фон Неймана


Архитектура фон Неймана строилась на следующих принципах:

  • Принцип двоичности:

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

  • Принцип программного управления:

Программа представляет собой набор команд, которые процессор выполняет друг за другом в определенном порядке.

  • Принцип однородности памяти:

Программы и данные хранятся вместе в памяти компьютера.

  • Принцип адресуемости памяти:

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

  • Принцип условного перехода:

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

Основным недостатком этой архитектуры является ограничение пропускной способности между памятью и процессором. Из-за того, что программа и данные не могут считываться одновременно, пропускная способность между памятью и процессором существенно ограничивает скорость работы процессора. В дальнейшем, данную проблему решили с помощью введения кеша, что вызвало другие проблемы( например, уязвимость Meltdown).

Справедливости ради необходимо уточнить, что данные идеи не являются идеями Джона фон Неймана в полной степени. Также в их разработке участвовали ещё несколько ученых, пионеров компьютерной техники: Джон Преспер Экерт и Джон Уильям Мокли.

Гарвардская архитектура


Гарвардская архитектура была разработана в 30-е годы прошлого века Говардом Эйкеном в Гарвардском университете (Невероятно, но факт). В отличии от архитектуры фон Неймана, Гарвардская подразумевала разные хранилища для данных и инструкций, а также разные каналы их передачи. Такой подход позволял одновременно считывать команда из программы и данные из памяти, что вело к значительному увеличению общей производительности компьютера. Но, в тоже время, такая схема усложняет саму систему. В дальнейшем Гарвардская архитектура проиграла архитектуре фон Неймана, отчасти из-за этого фактора.

Гарвардская архитектура

Языки


В самых первых компьютерах программы считывались с перфоленты (как в Z3 и Mark I). Устройство чтения перфоленты предоставляло управляющему устройство код операции для каждой инструкции и адреса памяти. Затем управляющее устройство все это декодировало, посылало управляющие сигналы вычислительному блоку и памяти. Набор инструкций жестко задавался в схеме, каждая машинная инструкция (сложение, сдвиг, копирование) реализовывалась непосредственно в схеме. В ENIAC для изменения программы его нужно было перекоммутировать заново, на что уходило значительное время. Машинные коды считают первым поколением языков программирования.


Перфорированная лента с программой вычислений

Первые программисты всегда имели при себе блокнот, в который они записывали наиболее употребляемые подпрограммы независимые фрагменты программы, вызываемые из главной подпрограммы, например извлечение корня или вывод символа на дисплей. Проблема состояла в том, что адреса расположения переменных и команд менялись в зависимости от размещения в главной программе. Для решения этой проблемы кембриджские программисты разработали набор унифицированных подпрограмм (библиотеку), которая автоматически настраивали и размещали подпрограммы в памяти. Морис Уилкс, один из разработчиков EDSAC (первого практически реализованного компьютера с хранимой в памяти программой), назвал библиотеку подпрограмм собирающей системой (assembly system). Теперь не нужно было собирать программу вручную из машинных кодов, специальная программа (ассемблер) автоматически собирала программу. Первые ассемблеры спроектированы Кэтлин Бут в 1947 под ARC2 и Дэвидом Уилером в 1948 под EDSAC. При этом сам язык (мнемоники) называли просто множеством базовых команд или начальными командами. Использовать слово ассемблер для процесса объединения полей в командное слово начали в поздних отчетах по EDSAC. Ассемблер можно назвать вторым поколением языков.


Начальные команды для EDSAC

Компьютеры первого поколения в СССР


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

МЭСМ и БЭСМ


В 1948 году основоположник советской вычислительной техники С.А. Лебедев направил в Академию наук СССР докладную записку: в ней сообщалось о необходимости создания ЭВМ для практического использования и научного прогресса. Для разработки этой машины под Киевом, в Феофании институту отвели здание, ранее принадлежавшее монастырю. Через 2 года МЭСМ (малая электронная счетная машина) произвела первые вычисление нахождение корней дифференциального уравнения. В 1951 году инспекция из академии наук приняла работу Лебедева. МЭСМ имела сложную трехадресную систему команд и следующие характеристики:

  • Тактовая частота 5 КГц
  • Быстродействие 3000 операций в минуту
  • 6000 вакуумных ламп
  • Потребляемая мощность 25 КВт
  • Площадь 60 кв.м
  • Ввод данных: перфокарты или магнитная лента
  • Память на триггерных ячейках


МЭСМ

В 1950 году Лебедева перевели в Москву. Там он начал работать над БЭСМ-1 и к 1953 году построил опытный образец, отличавшийся отличной производительностью. Характеристики были следующими:

  • Быстродействие до 10000 операций в минуту
  • 5000 вакуумных ламп
  • Потребляемая мощность 35 КВт
  • Площадь 1000 кв.м

БЭСМ-1 получилась ЭВМ широкого профиля. Её планировали предоставлять ученым и инженерам для проведения различных работ.


Серия М и Стрела


В тоже время в Москве велась работа над М-1. М-1 была намного менее мощной, чем МЭСМ, но при этом занимала намного меньше места и тратила меньше энергии. Характеристики М-1:

  • 730 вакуумных ламп
  • Быстродействие 15-20 операций в секунду
  • Потребляемая мощность 8 КВт
  • Площадь 4 кв.м
  • Память электронно-лучевых трубках


М-1

В 1952 году на свет выпустили М-2. Её мощность увеличилась практически в 100 раз, при этом количество ламп увеличилось только вдвое. Подобный результат получился благодаря использованию управляющих полупроводниковых диодов. Характеристики М-2 были следующие:

  • 1676 вакуумных ламп
  • Быстродействие 2000-3000 операций в секунду
  • Потребляемая мощность 29 КВт
  • Площадь 22 кв.м
  • Память электронно-лучевых трубках


М-2

В массовое производство первой попала Стрела. Всего было произведено 7 штук. Характеристики Стрелы были следующие:

  • 6200 вакуумных ламп и 60 тыс. диодов.
  • Быстродействие 2000 операций в секунду
  • Потребляемая мощность 150 КВт
  • Площадь 300 кв.м
  • Память электронно-лучевых трубках


Стрела

Во многих смыслах Стрела была хуже М-2. Она выполняла всё те же 2 тысячи операций в секунду, но при этом занимала на порядок больше места и тратила в несколько раз больше электричества. М-2 не попала в массовое производство, поскольку её создатели не уложились в срок. М-1 не обладала хорошей производительностью и к моменту, когда М-2 была доведена до ума, Стрела была отдана в производство.

Следующий потомок серии М М-3 вышел в 1956 году и был в каком-то смысле урезанным вариантом. Она выполняла порядка 30 операций в секунду, но при этом занимала мало места, благодаря чему пошла в серийное производство. Характеристики М-3 были следующие:

  • 774 вакуумных ламп
  • Быстродействие 30 операций в секунду
  • Потребляемая мощность 10 КВт
  • Площадь 3 кв.м


Эпилог


Без технологического рывка, сделанного в 40-е годы, и четко сформированного вектора развития вычислительной техники, возможно, сегодня мы бы и не сидели в компьютерах и телефонах, читая статейки на хабре. Как показал опыт разных ученых, порой уникальные и революционные для своего времени образцы вычислительной техники не были востребованы как государством, так и обществом (например, машины серии Z Конрада Цузе). Переход ко второму поколению компьютеров во многом определился сменой вакуумных ламп на транзисторы и изобретением накопителей на ферритовых сердечниках. Но это уже другая история



Облачные серверы от Маклауд быстрые и надежные. Без древнего железа.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru