[Перевод] PostMessage медленный?

внешний интерфейс JavaScript Программа перевода самородков

PostMessage медленный?

Нет, не обязательно (в зависимости от ситуации)

Что здесь означает «медленно»?Я упоминал это здесь раньше, еще раз: это не медленно, если вы его не измеряете, и даже если вы его измеряете, числа бессмысленны без контекста.

Сказав это, люди даже не подумают об усыновленииWeb Workers, потому что они беспокоятсяpostMessage()производительность, что означает, что это стоит исследовать.мой последний пост в блогетоже получил подобноеОтвечать. Подставим реальные цифрыpostMessage()производительность и увидеть, когда вы рискуете больше, чем вы можете себе позволить. Если даже обычныйpostMessage()Это слишком медленно для вашего варианта использования, так что еще вы можете сделать?

Вы готовы? Читать дальше.

Как работает postMessage?

Прежде чем мы начнем измерять, нам нужно понятьчто postMessage(), и какую его часть мы хотим измерить. иначе,Мы закончим сбор бессмысленных данныхи делать бессмысленные выводы.

postMessage()даСпецификация HTMLчасть (вместоECMA-262! ) как я вглубокая копия статьиупоминается в,postMessage()Полагается на структурированные данные клонирования для копирования сообщений из одного пространства JavaScript в другое. внимательно изучитьpostMessage()Технические характеристики, вы обнаружите, что структурированное клонирование представляет собой двухэтапный процесс:

Алгоритм структурированного клонирования

  1. выполнить в сообщенииStructuredSerialize()
  2. Добавьте задачу в очередь задач в приемнике, которая будет выполнять следующие шаги:
    1. Выполнить сериализованное сообщениеStructuredDeserialize()
    2. СоздаватьMessageEventи отправить сообщение с десериализованным сообщениемMessageEventсобытия в принимающий порт

Это упрощенная версия алгоритма, поэтому мы можем сосредоточиться на важных частях этого сообщения в блоге. Хотя этотехническиневерно, но суть отражает. Например,StructuredSerialize()а такжеStructuredDeserialize()На самом деле не работает в реальных сценариях, потому что они не передаются через JavaScript (Но есть предложение HTML, чтобы выставить их) незащищенный. Так что же на самом деле делают эти две функции? Сейчас,ты можешь поставитьStructuredSerialize()а такжеStructuredDeserialize()рассматривается какJSON.stringify()а такжеJSON.parse()умная версия. От работы с циклическими структурами данных встроенные типы данных, такие какMap,Setа такжеArrayBuffer) и т. д., они умнее. Но имеет ли вся эта хитрость свою цену? Мы обсудим это позже.

В приведенном выше алгоритме явно не указано, чтоСериализация блокирует отправителя, а десериализация блокирует получателя.Плюс: Chrome и Safari задерживаютсяStructuredDeserialize(), пока вы не посетитеMessageEventВверх.dataАтрибуты. С другой стороны, Firefox десериализует событие перед его отправкой.

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

Имея это в виду, мы должны выбрать правильныйкакиеДля сравнения: мы можем измерить end-to-end, поэтому мы можем измерить время, которое требуется рабочему процессу, чтобы отправить сообщение в основной поток. Однако это число будет отражать сумму времени сериализации и десериализации, но они происходят в разных пространствах. запомнить:Вся связь с рабочим активна, это необходимо для того, чтобы основной поток оставался свободным и отзывчивым.В качестве альтернативы мы могли бы ограничить бенчмарк Chrome и Safari и измерять отдельно отStructuredDeserialize()посетить.dataвремя атрибута, это нужно, чтобы исключить Firefox из теста. Я не нашел способа измерить индивидуальноStructuredSerialize(), если только вы не запустите код трассировки отладки. Ни один из вариантов не идеален, но в духе создания устойчивых веб-приложенийЯ решил провести сквозной бенчмарк дляpostMessage()Укажите верхний предел.

иметь правоpostMessage()Для концептуального понимания и определения оценки я буду использовать ☠️ Microbenchmarks ☠️. Обратите внимание на разрыв между этими цифрами и реальностью.

Контрольный показатель 1: Сколько времени занимает отправка сообщения?

Two JSON objects showing depth and breadth

Глубина и ширина варьируются от 1 до 6. Для каждого смещения будет сгенерировано 1000 объектов.

Datums будут генерировать объекты с определенной «шириной» и «глубиной». Значения ширины и глубины находятся в диапазоне от 1 до 6.За каждую комбинацию ширины и глубины из одного воркера будет удалено 1000 уникальных объектов.postMessage()к основной ветке. Все имена свойств этих объектов представляют собой случайные 16-значные шестнадцатеричные строки, а значения — либо случайное логическое значение, либо случайное число с плавающей запятой, либо случайная строка из 16-значного шестнадцатеричного числа.Эталонный тест будет измерять время прохождения и вычислять 95-й процентиль.

Результаты измерений

Тест выполнялся в Firefox, Safari и Chrome на MacBook Pro 2018 года, Chrome на Pixel 3XL и Chrome на Nokia 2.

Уведомление:ты сможешьgistНайдите эталонные данные, код для создания эталонных данных и код визуализации в формате . Кроме того, это первый раз в моей жизни, когда я написал Python. Не будь слишком строг со мной.

Данные тестов Pixel 3, особенно Safari, могут показаться вам немного подозрительными. когдаSpectre & MeltdownПри обнаружении все браузеры будут отключеныSharedArrayBufferи поместите тот, который я хочу измерить, используяperformance.now()Функция реализует таймер с пониженной точностью. Только Chrome может отменить эти изменения, потому что ониИзоляция сайтаОпубликовать на рабочем столе Chrome. В частности, это означает, что браузерperformance.now()Точность ограничена следующими значениями:

  • Chrome (настольный компьютер): 5 мкс
  • Chrome (Android): 100 мкс
  • Firefox (настольная версия): 1 мс (это ограничение можно отключить, я его просто отключил)
  • Safari (рабочий стол): 1 мс

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

Тест 2: Что делает postMessage медленным?

Чтобы убедиться в этом, я модифицировал тест: я сгенерировал все перестановки ширины и глубины от 1 до 6, но в остальном все листовые атрибуты имеют символ длиной от 16 байт до 2 КиБ в строковом значении.

Результаты теста

A graph showing the correlation between payload size and transfer time for postMessage

время передачи иJSON.stringify()Длина возвращаемой строки имеет сильную корреляцию.

Я думаю, что корреляция достаточно сильна, чтобы дать эмпирическое правило:Строковый размер объекта в формате JSON примерно пропорционален времени его передачи.Однако более важным является тот факт, чтоЭта корреляция актуальна только для крупных объектов., по большому счету я имею в виду любой объект размером более 100 КиБ. Хотя эта корреляция сохраняется математически, разница становится более заметной при меньших полезных нагрузках.

Оценка: Отправить сообщение

У нас есть данные, но они не имеют смысла, если мы не будем их контекстуализировать. Если мы сможем выяснитьосмысленныйВ заключение нам нужно дать определение «медленному». Бюджет - полезный инструмент здесь, и я вернусь сноваRAILруководство для определения наших ожиданий.

По моему опыту, основной обязанностью веб-работника является как минимум управление объектами состояния приложения. Состояние обычно изменяется только тогда, когда пользователь взаимодействует с вашим приложением. Согласно RAIL, у нас есть 100 мс, чтобы отреагировать на взаимодействие с пользователем, что означаетДаже на самом медленном устройстве вы можетеpostMessage()Объекты размером до 100 КиБ и оправдайте ваши ожидания.

Это меняется при запуске анимации на основе JS. Бюджет RAIL для анимации составляет 16 мс, потому что визуальные эффекты необходимо обновлять каждый кадр. Если мы отправим сообщение от работника, которое блокирует основной поток дольше, чем это, то у нас проблемы. Согласно нашим контрольным данным, любая анимация размером более 10 КиБ не представляет риска для вашего бюджета на анимацию. То есть,Это основная причина, по которой мы предпочитаем использовать анимацию и переходы CSS, а не JS для управления анимацией основного потока.CSS-анимации и переходы выполняются в отдельном потоке — потоке компоновки — на него не влияет заблокированный основной поток.

необходимо отправить больше данных

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

Пластырь

В случае с объектами состояния сами объекты могут быть очень большими, но обычно изменяются только несколько глубоко вложенных свойств. мы вPROXXМы столкнулись с этой проблемой в нашей версии Minesweeper для PWA: Game State состоит из двумерного массива игровых сеток. В каждой ячейке хранятся эти поля: есть ли мина, была ли она найдена или отмечена.

interface Cell {
  hasMine: boolean;
  flagged: boolean;
  revealed: boolean;
  touchingMines: number;
  touchingFlags: number;
}

Это означает, что самая большая сетка (40 × 40 ячеек) в сумме дает размер JSON примерно 134 КиБ. Невозможно отправить весь объект состояния.Вместо отправки всего нового объекта состояния при изменении мы решили зарегистрировать изменение и отправить набор исправлений.Хотя мы не использовалиImmerJS, которая представляет собой библиотеку для работы с неизменяемыми объектами, но которая обеспечивает быстрый способ создания и применения наборов исправлений:

// worker.js
immer.produce(stateObject, draftState => {
  // 在这里操作 `draftState`
}, patches => {
  postMessage(patches);
});

// main.js
worker.addEventListener("message", ({data}) => {
  state = immer.applyPatches(state, data);
  // 对新状态的反应
}

Патч, сгенерированный ImmerJS, выглядит так:

[
  {
    "op": "remove",
    "path": [ "socials", "gplus" ]
  },
  {
    "op": "add",
    "path": [ "socials", "twitter" ],
    "value": "@DasSurma"
  },
  {
    "op": "replace",
    "path": [ "name" ],
    "value": "Surma"
  }
]

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

Блокировать

Как я уже сказал, для государственных объектов,как правилоИзменятся лишь некоторые свойства. Но не всегда. Фактически,PROXXСуществует сценарий, когда набор исправлений может стать очень большим: первая презентация может затронуть до 80% игрового поля, а это означает, что размер набора исправлений составляет около 70 КиБ. Это слишком много для обычных телефонов, особенно когда мы можем запускать анимацию WebGL на основе JS.

Спросите себя архитектурный вопрос: может ли наша приложение поддерживает частичные обновления? Petchsets - это коллекции патчей.Вместо одновременной отправки всех исправлений в наборе исправлений вы можете «разбить» набор исправлений на меньшие разделы и применять исправления последовательно.Отправляйте патчи 1–10 в первом сообщении, патчи 11–20 в следующем и так далее. Если довести это до крайности, то можно эффективно сделать свой патчПотоковое, что позволяет вам использовать шаблоны проектирования, которые вы, вероятно, знаете, и реактивное программирование, которое вы любите.

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

мы вPROXXДелай блокировку. Когда пользователь щелкает поле, рабочий процесс проходит по сетке, определяет, какие поля необходимо обновить, и собирает их в список. Если список превышает определенный порог, мы отправляем то, что у нас есть на данный момент, в основной поток, очищаем список и продолжаем перебирать игровые поля. Эти наборы исправлений настолько малы, что даже на обычных телефонахpostMessage()Стоимость также незначительна, и у нас все еще есть достаточно времени бюджета основного потока, чтобы обновить наш игровой интерфейс. Итеративный алгоритм работает от первого тайла наружу, а это значит, что наши патчи устроены одинаково. Если основной поток может уместить только одно сообщение в бюджет кадра (как в Nokia 8110), то частичное обновление маскируется под анимацию отображения. Если бы мы работали на мощной машине, основной поток продолжал бы обрабатывать события сообщений до тех пор, пока не был бы превышен бюджет, что является естественным следствием цикла событий JavaScript.

Ссылка на видео:Великий Заявитель Лошадь/вещи/боится…

Классика: в [PROXX] фрагментация набора патчей выглядит как анимация. Это особенно заметно на настольных компьютерах или недорогих телефонах, поддерживающих 6-кратное троттлинг процессора.

Может JSON?

JSON.parse()а такжеJSON.stringify()очень быстро. JSON — это небольшое подмножество JavaScript, поэтому синтаксическому анализатору приходится обрабатывать меньше случаев. Они также значительно оптимизированы из-за их частого использования.Матиас недавно заметил, иногда путем инкапсуляции больших объектов вJSON.parse()чтобы сократить время парсинга JavaScript.Может быть, мы также можем использовать JSON для ускоренияpostMessage()? К сожалению, похоже, что нет:

将发送对象的持续时间与序列化、发送和反序列化对象进行比较的图

Сравните производительность ручной сериализации JSON с обычнойpostMessage()Сравнения были сделаны, и не было получено четких результатов.

Хотя явного победителя нет, в среднемpostMessage()В лучшем случае лучше, в худшем так же плохо.

двоичный формат

Еще один способ справиться с влиянием структурированного клонирования на производительность — вообще не использовать его. В дополнение к структурированным объектам клонирования,postMessage()Достаточно хорошокоробка передачопределенные виды.ArrayBufferэтиКабриолетодин из видов. Как следует из названия, передачаArrayBufferНикакого копирования. Отправитель фактически потерял доступ к буферу, который теперь принадлежит получателю.передать одинArrayBufferочень быстро и независимо отArrayBufferразмер.слабость этоArrayBufferПросто непрерывный блок памяти. Мы больше не можем иметь дело с объектами и свойствами. чтобыArrayBufferЧтобы функционировать, мы должны решить для себя, как маршалировать данные. Это само по себе имеет свою цену, но, зная форму или структуру данных во время сборки, мы потенциально можем сделать много оптимизаций, которые невозможны с помощью обычных алгоритмов клонирования.

Формат, который позволяет вам использовать эти оптимизации,FlatBuffers. У Flatbuffers есть компиляторы для JavaScript (и других языков), которые могут преобразовывать описания схемы в код. Код содержит функции для сериализации и десериализации данных. Еще более интересно: Flatbuffers не нужно анализировать (или «распаковывать») весьArrayBufferчтобы вернуть содержащееся в нем значение.

WebAssembly

А как насчет использования всеми любимого WebAssembly?Один из способов — использовать WebAssembly для просмотра библиотек сериализации в других языковых экосистемах.CBOR— это формат двоичных объектов, вдохновленный json, который был реализован на многих языках.ProtoBuffersи вышеупомянутыйFlatBuffersСуществует также обширная языковая поддержка.

Однако здесь мы можем быть более наглыми: мы можем полагаться на структуру памяти языка как на формат сериализации. я используюRustнаписанонебольшой пример: он определяет a с некоторыми методами получения и установкиStateСтруктуры (которые являются символами независимо от состояния вашего приложения), поэтому я могу проверять состояние и управлять им с помощью JavaScript. Чтобы «сериализовать» объект состояния, достаточно скопировать блок памяти, занимаемый структурой. Для десериализации я назначаю новыйStateобъект и перезапишите его данными, переданными функции десериализации. Поскольку в обоих случаях я использую один и тот же модуль WebAssembly, структура памяти будет одинаковой.

Это просто доказательство концепции. Если ваша структура содержит указатели (например,Vecа такжеString), то вы можете легко попасть в ошибки неопределенного поведения. Есть и ненужное копирование. Поэтому, пожалуйста, возьмите на себя ответственность за код!

pub struct State {
    counters: [u8; NUM_COUNTERS]
}

#[wasm_bindgen]
impl State {
    // 构造器, getters and setter...

    pub fn serialize(&self) -> Vec<u8> {
        let size = size_of::<State>();
        let mut r = Vec::with_capacity(size);
        r.resize(size, 0);
        unsafe {
            std::ptr::copy_nonoverlapping(
                self as *const State as *const u8,
                r.as_mut_ptr(),
                size
            );
        };
        r
    }
}

#[wasm_bindgen]
pub fn deserialize(vec: Vec<u8>) -> Option<State> {
    let size = size_of::<State>();
    if vec.len() != size {
        return None;
    }

    let mut s = State::new();
    unsafe {
        std::ptr::copy_nonoverlapping(
            vec.as_ptr(),
            &mut s as *mut State as *mut u8,
            size
        );
    }
    Some(s)
}

Уведомление: Ingvarуказал мнеAbomonation, является библиотекой сериализации с серьезными ошибками, хотя можно использовать концепцию указателей. Его совет: «Не используйте эту библиотеку!».

Окончательный размер gzip модуля WebAssembly составляет около 3 КиБ, большая часть которого приходится на управление памятью и некоторые основные библиотечные функции. Когда что-то меняется, отправляется весь объект состояния, но так какArrayBuffersпортативность при очень низкой стоимости. другими словами:Метод должен иметь почти постоянное время прохождения независимо от размера штата.Однако стоимость доступа к данным о состоянии будет выше. Всегда есть компромиссы!

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

SAB и веб-сборка

намекать:Этот раздел относится кSharedArrayBuffer, который отключен во всех браузерах, кроме Chrome на рабочем столе. Это продолжается, но ETA не может быть сообщено.

В частности, от разработчиков игр я слышал многочисленные просьбы о том, чтобы JavaScript мог совместно использовать объекты в нескольких потоках. Я думаю, что это вряд ли будет добавлено в сам JavaScript, так как это нарушает фундаментальное предположение движка JavaScript. Однако есть исключение, называемоеSharedArrayBuffer("САБ"). SAB ведут себя точно так же, какArrayBuffers, но при передаче, в отличие отArrayBuffersэто приведет к тому, что одна из сторон потеряет доступ, SAB может их клонировать, иобе стороныВсе могут получить доступ к одному и тому же базовому блоку памяти.SAB позволяют пространству JavaScript принять модель общей памяти.Для синхронизации между несколькими пространствами естьAtomicsОбеспечивает взаимное исключение и атомарные операции.

С помощью SAB вы просто передаете часть памяти при запуске приложения. Однако в дополнение к проблеме бинарного представления вы должны использоватьAtomicsчтобы одна сторона не читала объект состояния, пока другая все еще пишет, и наоборот. Это может существенно повлиять на производительность.

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

В заключение

Мой вывод: даже на самых медленных устройствах можно пользоватьсяpostMessage()Объекты размером до 100 КиБ и оставайтесь в пределах бюджета ответа 100 мс. Полезные нагрузки до 10 КиБ безопасны, если у вас есть анимация на основе JS. Для большинства приложений этого должно быть достаточно.postMessage()Цена есть, но не настолько, чтобы сделать невозможными архитектуры без основных потоков.

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

я уже встарый пост в блогеподразумевая Модель Актера выше, я твердо верю, что мы можемсейчасРеализация высокопроизводительной неосновной многопоточной архитектуры в сети требует от нас выхода из зоны комфорта многопоточных языков и стандартного для сети режима работы со всеми основными потоками. Нам нужно изучить другую архитектуру и модель,ОбъятиеОграничения Web и JavaScript. Преимущества того стоят.

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


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.