понедельник, 27 декабря 2021 г.

Грабли 'setTimeout'

Исходная проблема: T1051228 - Form - In some circumstances, browser hangs up when resizing if there is a DxForm with column adaptability

Проблема возникла из-за особенностей кода в dxEventEngine: иногда код зацикливается. Вкратце "если при выполнении обработчика события на это же событие добавляется новый обработчик" + еще несколько условий. Код dxEventEngine решили не менять: слишком большая вероятность что-то сломать в совершенно неожиданных местах.

Решили использовать setTimeout в коде компонента dxForm: https://github.com/DevExpress/DevExtreme/pull/20688/files

Я начал изучать эту проблему, я передавал ее ответственным за dxEventEngine, я получил ее обратно, изучил расклад и согласен, что изменение кода dxForm будет иметь меньше эффектов, чем изменение кода eventEngine.

Ребята предложили такое элементарное изменение:
instance.on('autoColCountChanged', function() {
    setTimeout(() => {
        that._refresh();
    }, 0);
});
Это изменение и правда элементарно, но эффекты от него посложнее:
  1. Самый частый эффект "забыть вызвать clearTimeout перед setTimeout" и получить зацикливание, когда метод вызывают, он добавляет свой вызов в очередь "setTimeout", отрабатывает, получает управление из очереди "setTimeout" и снова начинает работать, goto 1.
    Для обработки этой ситуации нужен такой код:
             instance.on(
                'autoColCountChanged',
                () => {
                    if(this.autoColCountChangedTimeoutId) {
                        clearTimeout(this.autoColCountChangedTimeoutId);
                        this.autoColCountChangedTimeoutId = undefined;
                    }
                    this.autoColCountChangedTimeoutId = setTimeout(
                        () => this._refresh(),
                        0
                    );
                }
            );
  2. Второй эффект "забыть вызвать clearTimeout на dispose" и получить nullref, когда метод начнет работать с "дохлыми" свойствами "убитого" объекта, если после setTimeout но до начала выполнения элемента из очереди "setTimeout" был вызов dispose
    Для этой ситуации нужны еще несколько строчек кода:
        _dispose: function() {
            if(this.autoColCountChangedTimeoutId) {
                clearTimeout(this.autoColCountChangedTimeoutId);
                this.autoColCountChangedTimeoutId = undefined;
            }
            this.callBase();
        },
  3. Третий эффект "Breaking Change: изменение синхронного выполнения кода на выполнение когда-то потом через постановку в очередь", когда вызывающий код хочет обрабатывать результаты работы метода сразу после вызова метода: с вызовом setTimeout этих результатов не будет, ведь работа над ними еще не началась.
    В моем случае такого ожидания вроде бы нет, потому что есть только один вызов без обращений к результатам работы кода:
        _dimensionChanged: function() {
            if(this.option('colCount') === 'auto' && this.isCachedColCountObsolete()) {
                this._eventsStrategy.fireEvent('autoColCountChanged');
            }
        },
    Но про остальной код вокруг этого метода я конечно же ничего не гарантирую.
    Например, у меня сразу же упали тесты, которые как раз хотят получить результаты работы этого метода сразу же после вызова, и для них мне пришлось вписать emulatedTimer.tick(), что бы получить эти результаты.
    Аналогичная ситуация может возникнуть и в клиентских приложениях, если будет несколько обработчиков события "dimensionChanged" после dxForm._dimensionChanged и они ожидают готовые результаты работы метода "_refresh()"

  4. Четвертый эффект "БЧ: для управления выполнением кода можно применить setTimeout только один раз", когда кто-то уже использовал setTimeout в своем коде, что бы выполнить свой код после "dxForm._refresh" и получить результаты его работы. После такого изменения вызов dxForm._refresh произойдет позже и клиентский код не получит эти результаты.
    Тут я конечно же тоже ничего не гарантирую.

  5. Пятый эффект "невозможно написать тесты": я не понял как заставить наш eventEngine зациклиться при выполнении обработчиков, поэтому PR без тестов. Пишите, если у кого-то получилось написать такой тест.
Из-за этих эффектов я стараюсь не применять setTimeout для управления последовательностью выполнения кода: слишком много комбинаций.

Хотя иногда приходится делать решение именно на этом методе.