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

Voximplant kit

Оптимизация графики в Voximplant Kit

24.03.2021 14:22:50 | Автор: admin

Что делать, если сценарий крутой и сложный, но из-за этого начинает тормозить? Данным вопросом задались наши разработчики Voximplant Kit и придумали функцию оптимизации. Продолжая серию обновлений Кита, расскажем, как оптимизация заставила большие сценарии летать и с какими проблемами мы столкнулись в процессе её создания.

Почему надо оптимизировать

Многим знакома проблема производительности, вызванная наличием слишком большого количества элементов на странице. Что это значит? В нашем случае чем больше элементов в сценарии Voximplant Kit, тем больше это влияет на скорость визуализации перемещения блоков по холсту (всех вместе и по отдельности), а также на скорость визуализации перемещения и масштабирования самого холста.

Мы рассмотрели разные варианты и пришли к выводу, что в качестве решения отлично подойдёт CSS свойство will-change, позволяющее заранее проинформировать браузер об изменениях, которые могут быть применены к элементу. Благодаря этому свойству можно настроить оптимизацию до начала изменений во избежание затрат на запуск операций, негативно влияющих на отзывчивость страницы.

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

<svg ... > <---- Холст <g transform="matrix(1,0,0,1,224,444)"> <---- Группа элементов внутри svg  <rect>  <rect>

Реализация

У наших разработчиков появилась идея обернуть SVG в div-элемент, чтобы применять все трансформации сначала к нему, а затем при необходимости к самому SVG-элементу с холстом. После того, как трансформации стали применяться к <div>, мы смогли использовать will-change: transform для их отслеживания:

<div> <---- div-обёртка, к которой применяется оптимизация  <svg ... > <---- Холст   <g> <---- Группа элементов внутри svg    <rect>    <rect>

Но появилась ещё одна проблема использование will-change инициирует создание нового слоя, и чем больше ширина и высота элемента, к которому это св-во применяется, тем больше расходуется оперативной памяти для хранения слоя. Справиться с этим помогло уменьшение масштаба SVG в 10 раз. Так, например, при масштабе холста =200% для слоя сwill-change требовалось300 мегабайтоперативки , а после уменьшения масштаба стало нужно всего около3 мегабайт.

Чтобы это осуществить, выставляем параметр zoom = 0.1 и подключаем к работе методtransformToCenterViewport, после чего применяем те же трансформации к div-элементу:

if (isPerfMode) {  this.el.classList.add('perf-mode');  // Меняем масштаб перед включением performance mode  const prevScale = this._viewportMatrix.a;  const point = this.getViewPortCenter();  const zoom = 0.1;       // Уменьшаем исходный svg, чтобы will-change тратил меньше оперативной памяти  this.transformToCenterViewport(point, zoom, true, false, true);  this.initScale = this._viewportMatrix.a;  this.createMatrix();     this.isPerfMode = true;       // Применяем трансформации к элементу-обертке  this.startPerformance();  this.transformToCenterViewport(point, prevScale, false, false, true);}

Т.к. при переходе в режим оптимизации мы уменьшаем SVG, холст становится очень маленьким и неудобным для работы. Чтобы это исправить, применим обратное масштабирование непосредственно к div-элементу:

public startPerformance(force = false) {  ...  this.isPerformance = true;    // Получаем размер области с блоками и отступ от левого угла вьюпорта  const { x, y, width, height } = this.layers.getBBox();  const initScale = this.initScale;    // Ширина и высота для обёртки и смещение по оси x и y для области с блоками  const wrapW = Math.floor(width * initScale) + 2;  const wrapH = Math.floor(height * initScale) + 2;  const layerX = -x * initScale;  const layerY = -y * initScale;    // this.wrapMatrix - матрица div-элемента с холстом   this.wrapMatrix.e = +(this._viewportMatrix.e + x * this._viewportMatrix.a);   this.wrapMatrix.f = +(this._viewportMatrix.f + y * this._viewportMatrix.d);   this.svgWrapper.style.width = wrapW + 'px';   this.svgWrapper.style.height = wrapH + 'px';   this.svgWrapper.style.transform = this.wrapMatrix.toString();   this.svgWrapper.style.willChange = 'transform'; this.layers.style.transform = `matrix(${initScale},0,0,${initScale},${layerX} ,${layerY} )`;}

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

После завершения масштабирования (событие о скролле), св-во will-change удаляется на 0.1 секунды и затем устанавливается заново. Это заставляет браузер повторно растрировать слой, возвращая пропавшие детали изображения:

// Добавляем 3d трансформацию, чтобы слой не был удаленthis.svgWrapper.style.transform = this.wrapMatrix.toString() + ' translateZ(0)';this.transformFrameId = requestAnimationFrame(() => {  // Устанавливаем св-во will-change для применения в следующем кадре  this.svgWrapper.style.willChange = '';  this.transformFrameId = requestAnimationFrame(() => {    this.svgWrapper.style.willChange = 'transform';    this.svgWrapper.style.transform = this.wrapMatrix.toString();  });});

Осталось внести последний фикс всегда отображать перемещаемый блок поверх других. В JointJS для перемещения блоков и линков по оси Z существуют методы toFront и toBack (аналог z-index в HTML). Принцип их работы заключается в сортировке элементов и перерисовке блоков и линков, это вызывает задержки.

Наши разработчики придумали следующее: блок, с которым мы взаимодействуем, временно ставится в конец дерева элементов внутри SVG (элемент с самым высоким z-index находится в конце списка) на событие mousedown, а затем возвращается на прежнее место на событие mouseup.

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

Режим оптимизации можно протестировать во всех браузерах на основе Chromium (Chrome, Opera, Edge, Yandex Browser и т.п.), а также в браузере Safari. Для сценариев, содержащих от 50 блоков, функция включается автоматически. Самостоятельно включить или отключить её можно, перейдя в меню настроек сценария в правом верхнем углу:

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

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

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

Подключаем оптимизацию и вуаля!

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

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

Подробнее..

Живые интерактивные логи визуализация логов в Voximplant Kit

30.06.2020 14:13:12 | Автор: admin

Мы продолжаем обновлять Voximplant Kit с помощью JointJS. И рады сообщить о появлении живых логов (live logs) звонков. Насколько они живые и опасны ли для простых юзеров, читайте под катом.

Ранее для анализа звонков в Voximplant Kit пользователям были доступны лишь записи разговоров. Нам же хотелось в дополнение к аудио сделать не просто текстовый лог, а более удобный инструмент для просмотра деталей звонка и анализа ошибок. И поскольку мы имеем дело с low-code/no-code продуктом, появилась идея визуализации логов.

В чем соль?/ Новый концепт


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


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


Управление


Контролы старт\стоп (1) останавливают/возобновляют воспроизведение, а назад\далее (2) точечно перемещают юзера к началу следующего/предыдущего блока. Можно также просто кликать по таймлайну, чтобы начать воспроизведение с определенного момента времени, как с проигрыванием песни.

Если сценарий включает в себя запись разговора, то она будет проигрываться параллельно с перемещением по блокам. Аудиозапись на таймлайне выделена отдельным цветом (3).


Для удобства пользователя также доступен список пройденных блоков с таймстампами (Лог):


Спойлер:
Во вкладке Лог мы планируем показывать детали блоков. Они помогут нам понять, почему из блока вышли по определенному порту и были ли ошибки. Например, для блока распознавания мы увидим результаты и ошибки распознавания.
Наибольший интерес здесь будут представлять сложные блоки, такие как DialogFlowConnector, IVR, ASR и т.д.


Переменные


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


Лайфхак


Логи звонков сохраняются в первоначальном виде, даже после изменения или удаления сценария. А это значит, что не возникнет проблем в восстановлении хода звонка: если он понадобится, всегда можно обратиться к логу.

Самостоятельно пощупать логи можно на Voximplant Kit.

Так, а что внутри?


Разберемся, как именно динамические логи реализованы в коде. Скажем сразу, от Joint JS мы взяли лишь анимацию и выделение блоков, как в деморежиме. Остальное (что можно на основе этого сделать) наша фантазия.

Кстати, чтобы узнать подробнее о внутренностях деморежима, читайте нашу предыдущую статью.

Получаем timepointы


При переходе в просмотр лога, сервер присылает данные, где содержится список всех пройденных блоков, время входа в них и список переменных, которые менялись во время звонка. Другими словами, на фронте мы получаем два массива объектов: log_path и log_variables.

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

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

Обновляем временную шкалу


После нажатия кнопки play временная шкала начинает обновляться каждые 10 мс. Во время каждого обновления проверяем, совпадает ли текущее время с одним из timepointов:

const found = this.timePoints.find((item) => item === this.playTime);

Если совпадение есть, будем искать все блоки у которых timepoint = текущее время + 600 мс (время, за которое происходит анимация перемещения между блоками).

Код метода updatePlayTime():

updatePlayTime(): void {    const interval = 10;    let expected = Date.now() + interval;    const tick = () => {        const drift = Date.now() - expected;        const found = this.timePoints.find((item) => item === this.playTime);        this.$emit('update', {            time: this.playTime,            found: found !== undefined        });        if (this.playTime >= this.duration) {            this.isPlay = false;            this.playTime = this.duration;            clearTimeout(this.playInterval);            this.$emit('end', this.playTime);            return;        }        expected += interval;        this.playTime += 0.01;        this.playTime = +this.playTime.toFixed(2);        this.updateProgress();        this.playInterval = window.setTimeout(tick, Math.max(0, interval - drift));    };    this.playInterval = window.setTimeout(tick, 10);}

Так же каждые 90 мс мы проверяем совпадения для текущего времени и timepoint'ов у измененных переменных + 4000 мс (время, в течение которого висит уведомление об изменении переменной).

Выделяем блоки


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

Если блоков с timepoint = текущее время + 600 мс несколько, то переход анимируется только к последнему:

if (i === blocks.length - 1) {    await this.selectBlock(blocks[i], 600, true, true);}

Это необходимо, поскольку есть блоки, которые обрабатываются очень быстро. Например, Проверка данных, Изменение данных и т.п. за 1 секунду может быть пройдено сразу несколько блоков. Если их анимировать последовательно, то возникнет отставание от времени таймлайна.

Код метода onUpdateTimeline:

async onUpdateTimeline({    time,    found}) {    this.checkHistoryNotify();    if (!found) return;    // Выделяем группу блоков от первой найденной точки + 600мс    const blocks = this.callHistory.log_path.filter((item) => {        return item.timepoint >= this.logTimer && item.timepoint < this.logTimer + 600;    });    if (blocks.length) {        this.editor.unselectAll();        for (let i = 0; i < blocks.length; i++) {            if (i === blocks.length - 1) {                await this.selectBlock(blocks[i], 600, true, true);                const cell = this.editor.getCellById(blocks[i].idTarget);                this.editor.select(cell);            } else {                await this.selectBlock(blocks[i], 0, false, true);            }        }    }}


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

В этом нам помогает метод selectBlock():

async selectBlock(voxHistory, timeout = 700, animate = true, animateLink = true) {    const inQueue = this.selectQueue.find((item) => item[0].targetId === voxHistory.idTarget);    if (!inQueue) this.selectQueue.push(arguments);    return this.exeQueue();}


Перематываем


При перемотке тот же принцип: когда таймлайн переместили, мы получаем время, на которое нужно перемотать и отмечаем блоки с timepoint'ами меньше текущего времени как пройденные:

const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });

Анимированный переход делаем к последнему из них.

Код метода onRewind():

async onRewind({    time,    accurate}, animation = true) {    this.editor.unselectAll();    this.stopLinksAnimation();    this.checkHistoryNotify(true);    const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });    for (let i = 0; i < forSelect.length; i++) {        if (i === forSelect.length - 1) {            await this.selectBlock(forSelect[i], 600, animation, false);            const cell = this.editor.getCellById(forSelect[i].idTarget);            this.editor.select(cell);        } else {            await this.selectBlock(forSelect[i], 0, false, false);        }    }    if (this.isPlay) this.restartAnimateLink();    this.onEndTimeline();}

Проигрываем аудио


С включение/выключением аудиозаписи дела обстоят еще проще. Если время таймлайна совпадает со стартом записи, она начинает проигрываться и далее время синхронизируется. За это отвечает метод updatePlayer():

updatePlayer() {    if (this.playTime >= this.recordStart && this.player.paused && !this.isEndAudio) {        this.player.play();        this.player.currentTime = this.playTime - this.recordStart;    } else if (this.playTime < this.recordStart && !this.player.paused) {        this.player.pause();    }}

На этом всё! Вот так на основе методов Joint JS и креатива наших разработчиков появились живые логи. Обязательно протестируйте их самостоятельно, если вы этого еще не сделали :)

Здорово, если вам нравится наша серия статей про обновления Кита. Будем и дальше делиться с вами самым свежим и интересным!
Подробнее..

Категории

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

  • Имя: Макс
    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