Оптимизация скорости работы скрипта при помощи Fast Undo

Основным преимуществом скриптов перед плагинами InDesign является простота и скорость разработки. Однако, выигрыш во времени разработки зачастую оборачивается значительной разницей в скорости выполнения — не в пользу скриптов, разумеется. И когда время работы полезного скрипта идет на минуты, а то и на десятки минут, нужно начать задумываться над тем, каким образом можно оптимизировать скорость работы. К счастью, Adobe всегда думает о нас, а потому с версии InDesign CS4 появилась возможность использования UndoMode. К версии CS6 эту штуку допилили до состояния, позволяющего пользоваться при повседневной работе. Разберем кратко, что это есть такое и как пользоваться в полевых условиях. Заодно подсчитаем, какой выигрыш во времени теоретически можно получить.

UdnoModes. Кто они и откуда

Сам по себе UndoMode есть enumerator, содержащий четыре элемента:

  1. UndoModes.AUTO_UNDO

  2. UndoModes.ENTIRE_SCRIPT

  3. UndoModes.FAST_ENTIRE_SCRIPT

  4. UndoModes.SCRIPT_REQUEST

Применяется UndoMode как параметр при вызове

app.doScript(script, language, withArguments, undoMode, undoName);

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

AUTO_UNDO

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

В случае использования AUTO_UNDO действия, выполненные в скрипте, будут «приклеены» к предыдущей точке Undo и, соответственно, не будут отображены в списке Undo как отдельный пункт. В том случае, если список Undo пуст, действия скрипта не попадут в список Undo чуть более, чем никак, из чего следует принципиальная невозможность отменить все, что наделал скрипт, при помощи стандартной процедуры вызова Undo.

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

ENTIRE_SCRIPT и FAST_ENTIRE_SCRIPT

Два брата-акробата. Вот с этими названиями все более-менее ясно: в список Undo все действия скрипта пишутся как одно единое действие. Название действия берется из параметра undoName.

Разница между просто ENTIRE_SCRIPT и FAST_ENTIRE_SCRIPT в целом трудноуловима, поэтому для ускоренного выполнения скриптов будем применять FAST_ENTIRE_SCRIPT, купившись на название.

Разница между ENTIRE_SCRIPT и FAST_ENTIRE_SCRIPT в подходе к обработке исключений в try…catch.

Если запускаемый с помощью ENTIRE_SCRIPT или FAST_ENTIRE_SCRIPT содержит ошибки, которые активируются вне блока try…catch, то результат работы одинаков — все изменения, проделанные скриптом до вызова ошибки, откатываются, объекты, с которыми работал скрипт, приводятся в состояние, которое у них было до запуска скрипта. Если же ошибка вызывается в блоке try…catch, то поведение отличается кардинально. При запуске скрипта с UndoModes.ENTIRE_SCRIPT исключения обрабатываются в обычном режиме, точно так же, как при обычном запуске скрипта. При использовании UndoModes.FAST_ENTIRE_SCRIPT в случае вызова ошибки внутри try…catch все изменения откатятся до состояния, предшествующего запуску скрипта, но выполнение оставшейся части кода при этом продолжится. Поэтому, если в скрипте, запускаемом c использованием ENTIRE_SCRIPT или FAST_ENTIRE_SCRIPT содержатся критичные обработки исключений, желательно использовать ENTIRE_SCRIPT, который, вероятно, будет работать чуть медленнее ввиду обработки ошибок в защищенном режиме.

SCRIPT_REQUEST

То, что используется по-умолчанию. Каждое действие скрипта пишется в список Undo отдельным действием.

UndoModes. Скептическое: «Надо ли оно?»

Надо. Даже в относительно простых скриптах. Во-первых, потому, что это красиво.

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

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

UndoModes. Энтузиазное: «Пусть меня научат!»

Как уже было сказано, UndoMode используется в виде параметра при вызове app.doScript, где первым параметром идет script. Означает ли это, что для использования UndoMode нужно писать два скрипта: один с кодом, а другой для вызова первого скрипта через doScript()? Совершенно не значит. Параметр script может быть как именем файла, так и именем вызываемой функции или даже просто кодом, что позволяет вызывать как весь скрипт целиком, так и отдельные функции с разными UndoModes по потребности.

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

Практика. Сеанс черной магии

Не один год бытует мнение, что запись всех действий скрипта в список Undo существенно снижает скорость его работы. Для проверки был написан совсем простой скрипт, который создает документ с одним текстовым фреймом и с нечеловеческим упорством пишет в story букву «а». Тридцать тысяч раз пишет — решил не мелочиться.

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

#target 'indesign'

function main() {
    var doc = app.documents.add(false);
    var frame = doc.pages[0].textFrames.add();
    var story = frame.parentStory
    $.hiresTimer;
    start = new Date();
    for (var j = 0; j < 30000; j++) {
         story.insertionPoints[-1].contents = 'a';
         if (j > 0 && j % 1000 == 0) {
            $.writeln($.hiresTimer)
        }
    }
    $.writeln($.hiresTimer);
    finish = new Date();
    doc.windows.add();
    $.writeln('Total ' + String(finish.getTime() - start.getTime()))
}

main();

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

#target 'indesign'

function add(story) {
    for (var i = 0; i < 1000; i++) {
         story.insertionPoints[-1].contents = 'a';
     }     if (i > 0 && i % 1000 == 0) {
        $.writeln($.hiresTimer)
    }
}

function main() {
    var doc = app.documents.add(false);
    var frame = doc.pages[0].textFrames.add();
    var story = frame.parentStory
    $.hiresTimer;
    start = new Date();
    for (var j = 0; j < 30; j++) {
        add(story);
    }
    finish = new Date();
    doc.windows.add();
    $.writeln('Total ' + String(finish.getTime() - start.getTime()))
}

main();

И для сравнения то же самое, но с использованием FAST_ENTIRE_SCRIPT для всей функции main():

#target 'indesign'

function main() {
    var doc = app.documents.add(false);
    var frame = doc.pages[0].textFrames.add();
    var story = frame.parentStory
    $.hiresTimer;
    start = new Date();
    for (var j = 0; j < 30000; j++) {
         story.insertionPoints[-1].contents = 'a';
         if (j > 0 && j % 1000 == 0) {
            $.writeln($.hiresTimer)
        }
    }
    $.writeln($.hiresTimer);
    finish = new Date();
    doc.windows.add();
    $.writeln('Total ' + String(finish.getTime() - start.getTime()))
}

app.doScript(main, ScriptLanguage.JAVASCRIPT, [], UndoModes.FAST_ENTIRE_SCRIPT, 'Add chars');

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


#target 'indesign'

function add(story) {
    for (var i = 0; i < 1000; i++) {
         story.insertionPoints[-1].contents = 'a';
     }
     if (i > 0 && i % 1000 == 0) {
        $.writeln($.hiresTimer)
    }
}

function main() {
    var doc = app.documents.add(false);
    var frame = doc.pages[0].textFrames.add();
    var story = frame.parentStory
    $.hiresTimer;
    start = new Date();
    for (var j = 0; j < 30; j++) {
        app.doScript('add(story)', ScriptLanguage.JAVASCRIPT, [], UndoModes.FAST_ENTIRE_SCRIPT, 'Add chars');
    }
    finish = new Date();
    doc.windows.add();
    $.writeln('Total ' + String(finish.getTime() - start.getTime()));
}

main();

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

Для пользы дела отмечу еще один момент, раз уж речь идет про оптимизацию и ускорение скриптов. В совсем ранней версии добавление очередной буквы «а» делалось не как, собственно, добавление, а через изменение story.contents = story.contents + ‘a’. Каждый, кто заметил, что под конец работы цикла скрипту придется много раз оперировать переменными размером в десятки тысяч знаков, и предположил, что такое поведение никак не может привести к быстрому выполнению ввиду совершенно ненужного и бесполезного захламления памяти, может взять с виртуальной полки виртуальный пирожок.

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

«Программист, оптимизируй данные правильным образом!»

Практика. Разоблачение

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

https://docs.google.com/spreadsheet/pub?key=0AoBa-ESLvy8ydGlmVGg0eTR4RGMteV9wclBEQVZTenc&single=true&gid=0&output=html

Данные из таблицы сведены в диаграмму для наглядного сравнения:

Какие выводы можно сделать из проведенного исследования?

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

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

Третье, интересное теоретически: разбиение циклов на части не оказывает сколько-нибудь заметного влияния на скорость выполнения скрипта. При использовании FAST_ENTIRE_SCRIPT наблюдается некоторое замедление скорости, но это вполне объяснимо, поскольку в список Undo попадает не одна запись, а тридцать.

Итого

Желающие могут продолжить эксперименты с UndoModes. Например, как будет работать (и будет ли работать вообще) вызов вложенной функции с  UndoMode, отличной от UndoMode вызывающей функции. И на какой глубине вложенности эта конструкция поломает InDesign. Если нет желания мучить рабочий инструмент, можно пересмотреть свои старые скрипты на предмет существенного ускорения или улучшения (один шаг Undo вместо десятков).

В дополнение

После публикации статьи и обсуждения на форуме rudtp.ru были проведены тесты с использованием UndoModes.ENTIRE_SCRIPT и UndoModes.AUTO_UNDO. Средние значения из четырех тестов для каждого из них помещены в таблицу:

https://docs.google.com/spreadsheet/pub?key=0AoBa-ESLvy8ydGlmVGg0eTR4RGMteV9wclBEQVZTenc&single=true&gid=0&output=html

Общее время выполнения при всех опробованных UndoModes сведены в таблицу:

https://docs.google.com/spreadsheet/pub?key=0AoBa-ESLvy8ydGlmVGg0eTR4RGMteV9wclBEQVZTenc&single=true&gid=5&output=html

Результат сравнения на диаграмме

По итогу всех тестов можно определить, что самым эффективным по скорости является, как и предполагалось, FAST_ENTIRE_SCRIPT. Более защищенный от ошибок UndoModes.ENTIRE_SCRIPT выполняется не на много быстрее, чем стандартный запуск скрипта, проигрывая в скорости даже UndoModes.AUTO_UNDO.

Как реализовать запуск скрипта в режиме FAST_ENTIRE_SCRIPT без риска потери части действий при использовании try…catch расскажу в следующей статье.

Оставьте комментарий