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

Клиентские части наших систем разрабатываются как программное обеспечение автоматизированных рабочих мест (АРМ) специалистов заводов.
АРМ имеют следующие особенности:
должны работать 24 часа, 7 дней в неделю, круглый год, без остановок;
аппаратное обеспечение часто недостаточно мощное, ограниченной производительности;
должны работать полностью автономно - по требованиям безопасности доступа из Интернет к ним нет, для устранения проблем только физический доступ;
на UI должны выводиться данные с видеокамер и датчиков, поступающие в систему с высокой частотой;
вывод данных на UI с задержкой недопустим, потому что это исказит картину течения процесса для пользователя;
пользователи не будут самостоятельно устранять никакие проблемы с программным обеспечением - специалисты управляют техпроцессами в реальном времени и им некогда разбираться с состоянием системы, звонить в техподдержку или что-то самостоятельно обновлять.
Мы разрабатываем клиенты при помощи веб-технологий, с использованием фреймворка Angular. Веб-клиент открывается на странице в браузере и непрерывно работает с выводом данных в режиме реального времени и без взаимодействия с пользователем.
Особенности работы наших клиентов требуют особых подходов к обеспечению их стабильности и отказоустойчивости. При тестировании начальных версий наших систем были ситуации, когда из-за кратковременного сбоя сети или минимальных утечек памяти в браузере клиентская часть длительно работающей системы становилась неработоспособной. Решалась проблема простой перезагрузкой страницы. Только вот в боевых условиях для нажатия F5 в браузере сотруднику техподдержки пришлось бы вые��жать на завод, возможно, даже ночью.
Перечислим рекомендации, выполнение которых позволяет нашим сотрудникам техподдержки спать спокойно и может пригодиться при создании клиентов, подобных нашим.
1. Предусмотреть автоподключение клиента к серверу
В условиях боевой эксплуатации соединение между клиентом и сервером может быть потеряно по многим причинам: сбой сети, разрыв соединения по инициативе сервера, длительное отсутствие данных и, как следствие, закрытие сокета. Поэтому необходимо реализовать автоподключение клиента к серверу вне зависимости от причин потери соединения и без ограничения попыток переподключения. Пример простой реализации - автоподключение по таймауту:
const connectWebsocket = endpoint => {
const ws = new WebSocket(endpoint);
ws.onclose = () => {
console.warn(`Websocket ${currentWsId} disconnected. url: ${ws.url} .
Reconnect will be attempted in 1 second`);
// Установка таймаута в 1 секунду
setTimeout(() => {
// Переподключение
connectWebsocket(endpoint);
}, 1000);
};
};Пример реализации автоподключения по таймауту
Также не стоит забывать выводить для пользователя понятные информационные сообщения в случае потери соединения с сервером или отсутствия данных от сервера дольше ожидаемого времени.

2. Сократить утечки памяти в браузере, связанные с работой клиента
Утечки памяти происходят, когда браузер по какой-то причине не до конца освобождает выделенную память. При непрерывной работе клиента утечки накапливаются и могут привести к потере производительности, зависанию интерфейса, а в худшем случае – к out of memory браузера. Поэтому при разработке клиента расход памяти необходимо оптимизировать.

Для детализации распределения памяти между JavaScript-объектами можно использовать, например, инструменты Memory - Heap Snapshots DevTools Google Chrome. Для просмотра использования процессорного времени, частоты смены кадров, для анализа смещения разметки и зон прорисовки - инструменты Performance DevTools Google Chrome.

3. Применять принудительную перезагрузку страницы
Не все утечки памяти могут быть связаны с работой клиента, они могут быть вызваны и ошибками работы самого браузера. Например, собственные утечки памяти того же Google Chrome - известная проблема. На этот случай рекомендуется применять принудительную перезагрузку страницы приложения по расписанию или по мере расходования выделенного странице объема памяти. Пример самого простого варианта – принудительная перезагрузка страницы раз в сутки.
// Перезагрузка страницы при изменении дня (clean tab memory)
setInterval(() => {
const currentDate = new Date();
// уведомляем о будущей перезагрузке
if (
!this._showReloadNotification &&
currentDate.getHours() === 23 &&
currentDate.getMinutes() == 59
) {
this._showReloadNotification = true;
this.notificationService.send('arm_status.arm_is_about_to_reload', 'info', {
options: {
timeout: 60000,
},
});
}
// день сменился
if (this._initiationMoment.getDate() !== currentDate.getDate()) {
location.reload();
}
}, 1000);Пример реализации принудительной перезагрузки страницы. P.S. Как заметил @i86com в комментариях, надежнее проверять переход через день, чем точное время, например 00:00:00, т.к. точное время не всегда может сработать.
4. Изолировать обработку потоков данных на клиенте
На наши клиенты с высокой частотой идут потоки разнообразных данных, которые необходимо обработать и отобразить. Из-за этого основной поток исполнения терминала АРМ может быть перегружен, и, как следствие, в интерфейсе будут наблюдаться замирания картинки или отображения неактуальных данных. Для трудоемких вычислений и обработки больших объёмов поступающих данных рекомендуем применять веб-воркеры (Web Workers). Веб-воркеры - это средство для запуска скриптов в фоновом режиме. Поток веб-воркера может выполнять задачи без вмешательства в пользовательский интерфейс. Подробнее о работе с веб-воркерами можно почитать в официальной документации.
Применение этой технологии позволяет эффективно использовать аппаратные ресурсы за счет распараллеливания вычислений и, как следствие, разгрузит�� основной поток исполнения. Например, до выделения веб-воркеров из-за загрузки основного потока страница одного из наших клиентов справлялась с отрисовкой максимум 12 кадров в секунду (FPS), при этом наблюдались ощутимые подвисания интерфейса. После того, как обработка видеоданных была вынесена в отдельный веб-воркер, FPS стабильно поднялось до 20.

Дополнительные преимущества от применения веб-воркеров можно получить при использовании OffscreenCanvas. OffscreenCanvas позволяет работать с html-элементами canvas напрямую из веб-воркера, а это значит, что построение сложных графиков, может происходить без участия основного потока, тем самым разгружая его. Подробнее о работе с OffscreenCanvas - здесь.
ngAfterViewInit(): void {
// Проверка поддержки веб-воркеров и OffscreenCanvas
if (this._worker && window['OffscreenCanvas']) {
const htmlCanvas = <HTMLCanvasElement>document.getElementById('canvas');
// Перевод canvas в offscreen
const offscreen = htmlCanvas.transferControlToOffscreen();
this._worker.postMessage({
type: 'canvas',
payload: {
canvas: offscreen
}
}, [offscreen]); // Обязательна передача в веб-воркер ссылки на canvas в
transfer массиве
// canvas передан, можно делать подключение
this._worker.postMessage({type: 'websocket', payload: this.frameWsApiUrl});
} else {
// Логика на случай, если OffscreenCanvas не поддерживается
this.connectToWsFrame();
}
}Пример применения веб-воркеров и OffscreenCanvas
5. Ограничить частоту обрабатываемых на клиенте данных
Еще одна мера обеспечения стабильности клиента, когда данные поступают с высокой частотой и при этом необходимо перерисовывать интерфейс, - ограничение частоты обрабатываемых на клиенте данных с помощью троттлинга (throttle). Троттлинг функции означает, что функция вызывается не более одного раза в указанный период времени, период троттлинга. Это особенно актуально, если терминал слабый и не может полноценно справляться с неограниченной троттлингом обработкой данных: интерфейс подвисает, копится задержка, актуальность отображаемых данных теряется.
Например: пусть при фактическом поступлении данных каждые 200 мс настраивается период троттлинга в 1 секунду. В этом случае данные за 1 секунду будут накапливаться, а обрабатываться будет только последнее значение, прочие значения будут игнорироваться. Интерфейс также будет перерисовываться не чаще одного раза в секунду.
Самый простой вариант реализации – жестко задать период троттлинга в виде константы. Однако предпочтительно определять период троттлинга в зависимости от производительности аппаратного обеспечения, на котором работает клиент. Например, один из вариантов – экспериментально определить несколько значений периода троттлинга и назначать их в зависимости от количества ядер терминала.
export const DEFAULT_ENV_PROD: Partial<IEnvironment> = {
wsDebug: false,
// Проверка количества логических ядер терминала для задания периода троттлинга
wsThrottle: navigator.hardwareConcurrency >= 8 ? 200 : 400,
noDataTimeout: 5000,
videoOverlayFrameNumber: false,
videoAspectRatio: '4/3',
};Пример определения значения периода троттлинга в зависимости от количества ядер терминала
Для «тонкой настройки» обработки данных мы рекомендуем библиотеку RxJS. Библиотека позволяет манипулировать данными в функциональном стиле, в том числе ограничивать обработку данных с помощью троттлинга. А с помощью функции distinctUntilChanged библиотеки можно определять, что новые данные не отличаются от предыдущих, и за счет этого оптимизировать частоту обновления интерфейса и исключать «паразитные» перерисовки, когда данные не изменились.
this.wsService
.on<FrontSystemState>('FrontSystemState')
.pipe(
// Если значение троттлинга задано в переменных окружения и является
числом (кроме нуля), то применяется троттлинг, иначе данные
пропускаются «как есть»
typeof this.env.wsThrottle === 'number'
? throttleTime(this.env.wsThrottle, undefined, {leading: true,
trailing: true})
: identity,
map(s => {
// Проверка null или undefined значений
s.integrationState = s.integrationState ?? {};
return s;
}),
// Мы не хотим лишний раз обновлять интерфейс, особенно когда в данных
// ничего не изменилось, поэтому проверяем, действительно ли значение
// объекта поменялось по сравнению с предыдущим
// distinctUntilChanged продолжит обработку пайпа, только если
значение изменилось
distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)),
takeUntil(this._unsubscribe$),
tap(data => {
timeoutTimer.bounce();
this._system$.next(data);
}),
)
.subscribe();Пример «тонкой» обработки данных
Например, после настройки троттлинга и исключения паразитных срабатываний отрисовки нам удалось поднять FPS страниц клиентов до 30 и избежать подвисаний интерфейса при высокой частоте данных, передаваемых с сервера.
6. Защитить пользователя от самого себя
Если есть доступ к терминалам пользователей и возможность настраивать их до начала эксплуатации, рекомендуем настроить терминалы на режим киоска средствами операционной системы или с использованием стороннего программного обеспечения. Цель - обеспечить, чтобы клиент был открыт на странице в выбранном браузере и у пользователя не было возможности свернуть страницу или перейти на другую. Кроме того, необходимо отключить браузерные расширения, блокировать модальные окна и предложения обновлений.
Один из вариантов реализации настройки терминала на режим киоска – использование специального дистрибутива Linux Porteus Kiosk на базе Gentoo. Это свободно распространяемый open source дистрибутив под лицензией GPL, который на текущий момент отвечает всем нашим требованиям к режиму киоска.
Заключение
Описанные подходы просты и очевидны, но если их заранее не предусмотреть, то пользователи и служба поддержки обречены на совершенно лишние мучения. Поэтому если перед вами также стоит задача разработки непрерывно функционирующих веб-клиентов или ваши веб-клиенты должны справляться с большими потоками данных, то будем рады, если на�� опыт вам пригодится. Также будем рады узнать, какие еще подходы вы бы рекомендовали использовать при создании подобных клиентов.
Свои производственные заметки мы, помимо Хабра, выкладываем в Telegram-канале. Присоединяйтесь!
