Два счёта, собранные по одному шаблону с одной и той же гарнитурой TrueType и различающиеся только фамилией клиента, оставляют в кэше подмножеств шрифтов два отдельных файла. Фамилия добавляет заглавную Q и строчную z, которых в первом счёте не было, значит, меняется набор сохраняемых глифов, а с ним — подмножество и ключ кэша. Постоянный кэш подмножеств окупается, когда глифовый репертуар ваших документов повторяется, а его счётчики уже за один пакет скажут вам, повторяется ли он у вас
Включение дискового кэша в HotPDF
HotPDF держит кэш подмножеств выключенным, пока вы не дадите ему папку. Два свойства THotPDF управляют им: FontSubsetCacheFolder задаёт каталог, а FontSubsetCacheMaxBytes ограничивает его объём — по умолчанию 268435456 байт (256 МиБ). Кэш активен, только пока папка непуста и бюджет больше нуля. Присвоение папки создаёт каталог, если его нет, и сразу же подрезает всё уже лежащее там до бюджета
Субсетирование заслуживает кэширования, потому что это настоящая работа. Построение TrueType-подмножества — это обход замыкания составных глифов, перезапись loca и glyf, пересборка таблиц, индексирующих глифы, и выпуск CID-отображения, которое PDF может адресовать. Для одного документа цена тонет в шуме. А на сервере отчётов, рендерящем в цикле тысячи выписок одними и теми же тремя гарнитурами, одни и те же подмножества строятся снова и снова — именно это процессорное время кэш и возвращает
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// путь только ASCII: свойство имеет тип AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 МиБ вместо 256 МиБ по умолчанию
// ... BeginDoc / рисование / EndDoc для каждого документа пакета ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt — ваша собственная процедура журналирования
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;
Кэш стоит за обоими путями субсетирования: гарнитуры обрабатываются через EnableFontSubsetting, что покрывает шрифты TrueType и OpenType-CFF, и через гарнитуру, зарегистрированную RegisterUnicodeTTF. Больше ничего в коде рисования не меняется. Путь к папке объявлен как AnsiString, так что имя каталога с символами вне системной кодовой страницы — напрашиваться на неприятности; простой ASCII-путь под ProgramData или в каталоге данных службы снимает вопрос целиком
Из чего складывается ключ кэша
Четыре входа решают, можно ли переиспользовать сохранённое подмножество: точные байты исходного шрифта, набор глифов, которые сохраняет документ, режим субсетирования и версия формата кэша. Каждый ключ строится из SHA-256-хэша байтов шрифта и SHA-256-хэша битовой карты использованных ID глифов, с префиксом v1- и суффиксом -s для разреженного подмножества или -c для компактного. Записи попадают на диск как <key>.hpdfsubset, так что листинг каталога с одного взгляда показывает, какой режим производит ваш пакет
Хэширование содержимого шрифта вместо имени файла даёт практическое следствие. Обновление Arial или подмена корпоративной гарнитуры более свежей сборкой автоматически порождают новые ключи, а старые записи просто перестают попадаться, пока вытеснение их не уберёт. Чистить кэш после обновления шрифта ради корректного вывода не нужно — очистка лишь раньше возвращает место
Режим субсетирования — вход, о котором забывают. CompactFontSubsetting перенумеровывает сохранённые глифы в плотный диапазон и пишет явный /CIDToGIDMap (ISO 32000-1, §9.7.4.2), а зарегистрированная гарнитура всегда получает компактное подмножество, когда включён PDFACompliance. CompressDocument тоже включает компактные подмножества на время жизни документа. Поэтому служба, пишущая одну и ту же выписку один раз как обычный PDF, а другой как PDF/A, заполняет два набора записей для одних и тех же глифов — один с суффиксом -s, другой -c. Гарнитуры OpenType-CFF всегда идут разреженным путём
Именно на наборе глифов решается частота попаданий. Выписки из фиксированного шаблона, формы с переменными данными из одних цифр, каталоги из одной продуктовой базы — они повторяют одни и те же глифы из документа в документ. Нагрузка, где каждый документ тянет свой срез CJK-гарнитуры с десятками тысяч глифов, почти сплошь промахивается: битовая карта почти каждого документа нова. Прогоните один показательный пакет и прочтите счётчики, прежде чем решать, какого рода нагрузка у вас
Откуда вы знаете, что кэш вообще работает?
GetFontSubsetCacheInfo возвращает запись THPDFFontSubsetCacheInfo с девятью полями, и HitCount против MissCount отвечает на вопрос напрямую. WriteCount и EvictionCount показывают, переживают ли записи достаточно долго, чтобы переиспользоваться, или их выталкивает слишком тесный бюджет. CurrentBytes и FileCount описывают папку такой, какая она прямо сейчас
Две детали важны, когда вы подключаете их к мониторингу. Семь событийных счётчиков принадлежат экземпляру THotPDF и стартуют с нуля при создании компонента, поэтому служба, создающая по компоненту на документ, видит в каждом показании активность ровно одного документа. CurrentBytes и FileCount, напротив, берутся из перечисления папки в момент вызова: они отражают все процессы, пользующиеся этой папкой, и каждый вызов стоит обхода каталога. Читайте запись один раз на пакет, в конце, и держите документный цикл свободным от неё
Оставшиеся три счётчика — те, что стоят оповещения. CorruptCount считает записи, провалившие валидацию при загрузке и удалённые; каждая из них идёт ещё и в промахи. Горстка после сбоя питания — норма, а устойчивый ручей указывает на хранилище. RejectedCount считает подмножества, которые кэш отказался сохранять, обычно потому, что одно подмножество крупнее всего бюджета. WriteFailureCount считает незавершённые записи, что на практике почти всегда означает папку, недоступную для записи учётной записи службы. Ни один из трёх не прерывает генерацию документов, поэтому кэш с проблемой прав на запись выдаёт ровно те же PDF, что и полное отсутствие кэша. Просто ему уже никогда не стать быстрее
// SubsetCacheNeedsAttention — вспомогательная функция, которую вы добавляете сами;
// пороги — примеры, подгоните их под размер вашего пакета
function SubsetCacheNeedsAttention(const Info: THPDFFontSubsetCacheInfo;
out Reason: string): Boolean;
begin
Reason := '';
if Info.WriteFailureCount > 0 then
Reason := 'cache folder is not writable for this account'
else if Info.RejectedCount > Info.WriteCount then
Reason := 'subsets exceed FontSubsetCacheMaxBytes, raise the budget'
else if (Info.EvictionCount > Info.WriteCount div 2) and (Info.HitCount < Info.MissCount) then
Reason := 'entries are evicted before they are reused'
else if Info.CorruptCount > 10 then
Reason := 'repeated damaged entries, check the storage';
Result := Reason <> '';
end;
Бюджеты, вытеснение и переключатель, опустошающий папку
Вытеснение — LRU по времени модификации файла. Каждая успешная загрузка трогает отметку времени записи, за каждым сохранением следует подрезка, и первыми уходят старейшие файлы, пока папка снова не уложится в бюджет. Понижение FontSubsetCacheMaxBytes во время выполнения подрезает на месте, не дожидаясь следующей записи, поэтому служба, реагирующая на сигнал о нехватке диска, может ужать кэш в момент своего решения
У этой немедленности есть острый край. Обнуление бюджета при настроенной папке подрезает до нуля, то есть удаляет каждый файл .hpdfsubset в ней. А пустая строка в FontSubsetCacheFolder, наоборот, выключает кэш и оставляет файлы ровно там, где они лежат. Оба переключателя отключают кэширование для следующего документа; но лишь один из них сохраняет неделю прогретых записей. Для поиска неисправностей освобождайте свойство папки, а бюджет не трогайте
// Приостановите кэширование для диагностического прогона, сохранив все записи
Pdf.FontSubsetCacheFolder := '';
// Опасно: при заданной папке нулевой бюджет подрезает папку до пустоты
// Pdf.FontSubsetCacheMaxBytes := 0;
// Намеренный сброс: удаляет *.hpdfsubset и оставшиеся временные файлы,
// событийные счётчики сохраняют свои значения
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Диагностический прогон с приостановленным кэшем — самый чистый способ исключить его. Шестибуквенный тег подмножества в /BaseFont выводится из набора сохранённых глифов, поэтому одни и те же глифы дают одинаковый тег с кэшем и без, а встроенные шрифтовые программы должны совпадать. Хэши целых файлов всё равно разойдутся из-за дат создания и ID документа, так что сравнивайте шрифтовые потоки, а не файлы
Что происходит с повреждённой или недописанной записью?
Повреждённая запись удаляется, а подмножество строится как обычно — до того, как что-либо из этой записи доберётся до PDF-потока. При загрузке HotPDF проверяет сигнатуру файла, версию формата, флаг режима субсетирования, число глифов против исходного шрифта, объявленные длины против фактического размера файла и sfnt-заголовок сохранённой шрифтовой программы. Любое несовпадение выбрасывает запись, наращивает CorruptCount и откатывается к субсетированию в памяти. Сломанная шрифтовая программа в PDF заказчика всплыла бы спустя недели в просмотрщике на чужой машине, вдали от причины, — именно от этого сбоя данная валидация и существует
Недописанные записи и не появляются вовсе. Сохранение пишет во временный файл, чьё имя несёт ID процесса и экземпляр компонента, затем перемещает его поверх финального имени одной заменой со сбросом на диск. Падение посреди записи оставляет бродячий файл .tmp-, который подметает ClearFontSubsetCache, а настоящей записью остаётся либо полная старая версия, либо полная новая. Компактные записи хранят рядом со шрифтовой программой ещё и карту «старый-в-новый» для глифов, поэтому кэшированное компактное подмножество восстанавливает тот же /CIDToGIDMap, и вывод PDF/A может пользоваться кэшем, не обходя его
Где должна жить папка?
Решают три условия: учётная запись службы должна уметь туда писать, папка должна лежать на локальном хранилище, и никакой шаг развёртывания не должен её затирать. Сетевой ресурс превращает каждый поиск в полный обход сети и отдаёт семантику переименований на усмотрение файлового сервера. Папка внутри каталога приложения, которую пересоздаёт инсталлятор, означает холодный старт кэша после каждого обновления — и это тихо стирает выгоду ровно в тот день, когда вы пристальнее всего смотрите на производительность
Несколько компонентов и несколько процессов могут делить одну локальную папку. Временные имена никогда не сталкиваются, финальное переименование атомарно, а проигранная гонка стоит не дороже промаха или записи, которую придётся повторить на следующем документе. Совместное использование — лучший вариант по умолчанию на одной машине, потому что каждый экземпляр прогревает кэш для остальных. Единственный видимый побочный эффект: счётчики каждого экземпляра описывают только его трафик, тогда как CurrentBytes и FileCount описывают всех сразу, а отдельный тик WriteFailureCount может прийти от двух процессов, заменяющих одну запись одновременно. Папка только для чтения всё ещё отдаёт существующие записи; она лишь не может освежать их отметки времени или добавлять новые
Когда кэш — не тот рычаг
Кэш убирает повторяемую работу. Для первого документа пакета он не делает ничего, а для нагрузок, чьи наборы глифов не повторяются, — почти ничего. Если вывод доминирует одной большой CJK-гарнитурой по непредсказуемому тексту, посмотрите, какие глифы втягиваются в каждое подмножество и почему, — об этом заметки о замыкании подмножества шрифта и формировании глифов. Если цель — размер файла, а не процессор, статья о CompressDocument и компактных подмножествах шрифтов объясняет, что компактный режим меняет во встроенном шрифте. Когда пакет оказывается медленным по причинам, не связанным со шрифтами, разбор вывода отчётов со шрифтами и изображениями показывает, куда обычно уходит остальное время, а материал о порядке шрифтовых подмножеств на EndDoc рассказывает, когда подмножества строятся при финализации документа
Поскольку кэш подмножеств живёт внутри компонента, сервер отчётов на Delphi или C++Builder получает его, задав путь к папке и бюджет, — без дополнительной службы для развёртывания и мониторинга. Встраивание шрифтов, субсетирование и остальные возможности генерации PDF перечислены на странице компонента HotPDF для Delphi