Техническая статья

Кэширование подмножеств шрифтов на диске с HotPDF в Delphi

HotPDF умеет хранить подмножества шрифтов TrueType и OpenType на диске и повторно использовать их между документами и запусками процесса, поэтому пакет, генерирующий десять тысяч выписок на одних и тех же трёх шрифтах, строит подмножества этих шрифтов один раз, а не десять тысяч. Кэш настраивается двумя свойствами, инспектируется одной записью и его безопасно оставить включённым: сбой кэша откатывается к обычному построению подмножеств в памяти и никогда не останавливает выпуск документа

Подмножество шрифта дорого по причине. Построение подмножества означает обход замыкания глифов, перезапись loca и glyf, пересборку cmap и hmtx и выпуск CID-отображения, которое PDF может адресовать. Для одного документа эта цена тонет в шуме. Для сервера отчётов, выпускающего документы в цикле, она часто — крупнейший единый блок процессорного времени в прогоне

Что делает попадание в кэш возможным

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

Именно множество глифов удивляет людей. Два счёта, различающиеся одним именем клиента, используют разные множества глифов, а потому производят разные подмножества и разные записи кэша. Кэш окупается, когда документы делят глифовый репертуар — выписки по фиксированному шаблону, формы с числовой переменной частью, каталоги, собираемые из одной продуктовой базы, — и не даёт ничего, когда каждый документ рисует свою часть большой CJK-гарнитуры. Измерьте прежде, чем предполагать, какой у вас случай

var
  Pdf: THotPDF;
  Info: THPDFFontSubsetCacheInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.EnableFontSubsetting := True;
    Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
    Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024;   // 64 MiB, default is 256
    // ... generate the batch ...
    Info := Pdf.GetFontSubsetCacheInfo;
    LogFmt('subset cache: %d hits, %d misses, %d bytes in %d files',
      [Info.HitCount, Info.MissCount, Info.CurrentBytes, Info.FileCount]);
  finally
    Pdf.Free;
  end;
end;

Откуда вы знаете, что кэш вообще работает?

GetFontSubsetCacheInfo возвращает девять счётчиков, и отношение первых двух отвечает на вопрос напрямую. HitCount и MissCount дают частоту попаданий. WriteCount и EvictionCount показывают, переживают ли записи достаточно долго, чтобы переиспользоваться, или вытесняются слишком малым бюджетом. CurrentBytes и FileCount сообщают, что сейчас на диске

Оставшиеся три — те, на которые стоит настроить оповещения. CorruptCount считает записи, провалившие валидацию и удалённые: несколько после нештатного завершения нормальны, устойчивый поток означает, что хранилище ненадёжно. RejectedCount считает записи, отклонённые до использования. WriteFailureCount считает записи, которые вовсе не удалось записать, что обычно означает проблему с правами на папку, а не что-то про шрифты. Ни одна из этих трёх не останавливает генерацию документов, и именно поэтому смотреть на них необходимо: кэш, который молча никогда не пишет, снаружи выглядит так же, как работающий, кроме счёта за процессорное время

Вытеснение, бюджеты и момент, когда вы уменьшаете один из них

FontSubsetCacheMaxBytes по умолчанию равно 268435456 байт, то есть 256 MiB, и может быть понижено во время выполнения. Понижение немедленно запускает вытеснение по принципу наиболее давно использованного (LRU), а не ожидает следующей записи, поэтому служба, реагирующая на дисковое давление, может освободить место в момент своего решения, а не в какой-то более поздний, ею не контролируемый

Установка FontSubsetCacheFolder в пустую строку отключает дисковый уровень, не очищая уже сохранённое и не меняя ни одного байта шрифтового вывода. Именно к этому свойству стоит обратиться, когда нужно изолировать кэш при поиске неисправностей: выключите, прогоните тот же пакет и сравните полученные PDF. Они должны быть идентичны, потому что кэш хранит результат, а не политику

Что делает кэш с повреждённой записью

Удаляет её и строит подмножество как обычно. Повреждённые или усечённые записи отбрасываются до того, как подмножество сможет достичь потока PDF, и именно эта часть дизайна важнее всего: повреждённая запись кэша, попавшая в документ, породила бы PDF со сломанной шрифтовой программой, и этот сбой всплыл бы вдали от своей причины — в просмотрщике, на машине заказчика, спустя недели

Записи атомарны, поэтому читатель никогда не наблюдает наполовину записанную запись, а сбой в середине записи оставляет кэш согласованным, а не отравленным. Компактные записи подмножеств сохраняют данные CID-перенумерации, требуемые шрифтовыми словарями PDF/A, поэтому кэшированное подмножество остаётся соответствующим подмножеством: архивный вывод не обязан обходить кэш ради валидности

// Reset the disk tier after a font upgrade or a schema change
Pdf.ClearFontSubsetCache;

// Or move it somewhere writable and let the budget apply immediately
Pdf.SetFontSubsetCacheFolder('D:\cache\fonts');

Куда поместить папку в реальном развёртывании

Этим управляют три свойства: папка должна быть доступна для записи учётной записью, под которой работает служба, она должна лежать на локальном диске, а не на сетевом ресурсе, и не должна быть внутри каталога, который шаг развёртывания затирает. Кэш на ресурсе превращает каждый промах в обход сети, а каждое попадание — в два; кэш в папке приложения, которую инсталлятор пересоздаёт, — это кэш, стартующий холодным после каждого обновления

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

Когда обратиться к другому средству

Кэш снижает повторяемую работу. Он не снижает работу первого документа и не помогает нагрузке, чьи множества глифов никогда не повторяются. Если ваш вывод доминирует одной огромной CJK-гарнитурой по непредсказуемому тексту, то более действенный рычаг — само замыкание подмножества: какие глифы втягиваются и почему, — описанное в заметках о замыкании подмножества шрифта и формировании глифов. Если ваш пакет медленный по причинам, оказавшимся вообще не шрифтовыми, разбор вывода отчётов со шрифтами и изображениями показывает, куда обычно уходит остальное время, а разбор кейса про баг порядка подмножества шрифта на EndDoc напоминает, что корректность подмножества и скорость подмножества — отдельные задачи

HotPDF — нативный VCL PDF-компонент для Delphi и C++Builder, а кэш подмножеств — часть библиотеки, а не надстройка-служба, поэтому сервер отчётов получает его, задав один путь к папке — см. страницу компонента HotPDF для полного списка шрифтовых и производительных возможностей