Основным преимуществом скриптов перед плагинами InDesign является простота и скорость разработки. Однако, выигрыш во времени разработки зачастую оборачивается значительной разницей в скорости выполнения — не в пользу скриптов, разумеется. И когда время работы полезного скрипта идет на минуты, а то и на десятки минут, нужно начать задумываться над тем, каким образом можно оптимизировать скорость работы. К счастью, Adobe всегда думает о нас, а потому с версии InDesign CS4 появилась возможность использования UndoMode. К версии CS6 эту штуку допилили до состояния, позволяющего пользоваться при повседневной работе. Разберем кратко, что это есть такое и как пользоваться в полевых условиях. Заодно подсчитаем, какой выигрыш во времени теоретически можно получить.
UdnoModes. Кто они и откуда
Сам по себе UndoMode есть enumerator, содержащий четыре элемента:
-
UndoModes.AUTO_UNDO
-
UndoModes.ENTIRE_SCRIPT
-
UndoModes.FAST_ENTIRE_SCRIPT
-
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’. Каждый, кто заметил, что под конец работы цикла скрипту придется много раз оперировать переменными размером в десятки тысяч знаков, и предположил, что такое поведение никак не может привести к быстрому выполнению ввиду совершенно ненужного и бесполезного захламления памяти, может взять с виртуальной полки виртуальный пирожок.
Отсюда промежуточный вывод, который невредно распечатать большими буквами и повесить перед собой и перед знакомыми скриптописателями:
«Программист, оптимизируй данные правильным образом!»
Практика. Разоблачение
В результате тестирования скриптов была составлена следующая таблица:
Данные из таблицы сведены в диаграмму для наглядного сравнения:
Какие выводы можно сделать из проведенного исследования?
Первое, оно же главное: использование FAST_ENTIRE_SCRIPT дает почти двукратный рост скорости выполнения скрипта. Это очень, очень много, особенно для сложных скриптов и больших документов.
Второе, не очень очевидное: при использовании FAST_ENTIRE_SCRIPT время выполнения каждой последующей итерации все-таки растет, причем примерно с такой же скоростью, с какой растет при стандартной записи Undo. Но при этом рост постоянный и без значительных скачков.
Третье, интересное теоретически: разбиение циклов на части не оказывает сколько-нибудь заметного влияния на скорость выполнения скрипта. При использовании FAST_ENTIRE_SCRIPT наблюдается некоторое замедление скорости, но это вполне объяснимо, поскольку в список Undo попадает не одна запись, а тридцать.
Итого
Желающие могут продолжить эксперименты с UndoModes. Например, как будет работать (и будет ли работать вообще) вызов вложенной функции с UndoMode, отличной от UndoMode вызывающей функции. И на какой глубине вложенности эта конструкция поломает InDesign. Если нет желания мучить рабочий инструмент, можно пересмотреть свои старые скрипты на предмет существенного ускорения или улучшения (один шаг Undo вместо десятков).
В дополнение
После публикации статьи и обсуждения на форуме rudtp.ru были проведены тесты с использованием UndoModes.ENTIRE_SCRIPT и UndoModes.AUTO_UNDO. Средние значения из четырех тестов для каждого из них помещены в таблицу:
Общее время выполнения при всех опробованных UndoModes сведены в таблицу:
Результат сравнения на диаграмме
По итогу всех тестов можно определить, что самым эффективным по скорости является, как и предполагалось, FAST_ENTIRE_SCRIPT. Более защищенный от ошибок UndoModes.ENTIRE_SCRIPT выполняется не на много быстрее, чем стандартный запуск скрипта, проигрывая в скорости даже UndoModes.AUTO_UNDO.
Как реализовать запуск скрипта в режиме FAST_ENTIRE_SCRIPT без риска потери части действий при использовании try…catch расскажу в следующей статье.