Dwie faktury wyrenderowane z tego samego szablonu tą samą czcionką TrueType, różniące się tylko nazwiskiem klienta, zostawiają dwa osobne pliki w pamięci podręcznej podzbiorów czcionek. Nazwisko dodaje wielkie Q i małe z, których pierwsza faktura nigdy nie użyła, więc zestaw zachowanych glifów się zmienia, a razem z nim podzbiór i klucz w cache. Trwała pamięć podręczna podzbiorów zwraca się wtedy, gdy repertuar glifów twoich dokumentów się powtarza, a liczniki, które wystawia, powiedzą ci w ciągu jednej partii, czy u ciebie się powtarza
Włączanie cache na dysku w HotPDF
HotPDF trzyma cache podzbiorów wyłączony, dopóki nie podasz mu folderu. Sterują nim dwie właściwości THotPDF: FontSubsetCacheFolder nazywa katalog, a FontSubsetCacheMaxBytes ogranicza, ile może pomieścić, domyślnie 268435456 bajtów (256 MiB). Cache jest aktywny tylko dopóki folder nie jest pusty, a budżet większy od zera. Przypisanie folderu tworzy katalog, jeśli go brakuje, i od razu przycina to, co już tam jest, do budżetu
Subsetowanie zasługuje na cache, bo to prawdziwa robota. Zbudowanie podzbioru TrueType oznacza przejście domknięcia glifów złożonych, przepisanie loca i glyf, przebudowę tabel indeksujących glify i wyprodukowanie mapowania CID, które PDF potrafi zaadresować. Dla jednego dokumentu ten koszt ginie w szumie. W serwerze raportów, który renderuje tysiące wyciągów w pętli tymi samymi trzema krojami, te same podzbiory są budowane w kółko — i dokładnie ten CPU oddaje cache
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
// tylko ASCII: właściwość jest typu AnsiString
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB zamiast domyślnych 256 MiB
// ... BeginDoc / rysowanie / EndDoc dla każdego dokumentu partii ...
Info := Pdf.GetFontSubsetCacheInfo;
// LogFmt to twoja własna procedura logowania
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;
Cache siedzi za obiema ścieżkami subsetowania: kroje są subsetowane przez EnableFontSubsetting, które obejmuje czcionki TrueType i OpenType-CFF, oraz krój zarejestrowany przez RegisterUnicodeTTF. W kodzie rysowania nie zmienia się nic innego. Ścieżka folderu jest zadeklarowana jako AnsiString, więc nazwa katalogu ze znakami poza systemową stroną kodową to proszenie się o kłopoty; zwykła ścieżka ASCII pod ProgramData albo w katalogu danych usługi zamyka temat
Co wchodzi do klucza cache
O czterech danych wejściowych zależy, czy zapisany podzbiór da się użyć ponownie: dokładne bajty czcionki źródłowej, zestaw glifów, które dokument zachowuje, tryb podzbioru i wersja formatu cache. Każdy klucz jest budowany z haszu SHA-256 bajtów czcionki i haszu SHA-256 bitmapy użytych identyfikatorów glifów, z prefiksem v1- i sufiksem -s dla podzbioru sparse albo -c dla compact. Wpisy lądują na dysku jako <key>.hpdfsubset, więc listing katalogu pokazuje ci od razu, który tryb produkuje twoja partia
Haszowanie zawartości czcionki zamiast jej nazwy pliku ma praktyczny skutek. Podniesienie Arialu albo podmiana na nowszą wersję korporacyjnego kroju automatycznie produkuje nowe klucze, a stare wpisy po prostu przestają być trafiane, aż wykluczenie je usunie. Po aktualizacji czcionki nie musisz czyścić cache, żeby dostać poprawne wyjście. Czyszczenie odzyskuje miejsce tylko trochę wcześniej
Tryb podzbioru to dane wejściowe, o których ludzie zapominają. CompactFontSubsetting renumeruje zachowane glify do gęstego zakresu i zapisuje jawne /CIDToGIDMap (ISO 32000-1 §9.7.4.2), a zarejestrowany krój zawsze dostaje podzbiór compact, gdy ustawione jest PDFACompliance. CompressDocument też włącza podzbiory compact na czas trwania dokumentu. Usługa, która pisze ten sam wyciąg raz jako zwykły PDF, a raz jako PDF/A, zapełni więc dwa zestawy wpisów dla identycznych glifów — jeden kończący się na -s, drugi na -c. Kroje OpenType-CFF zawsze idą ścieżką sparse
O trafności decyduje zestaw glifów. Wyciągi z jednego szablonu, formularze, których dane zmienne to głównie cyfry, katalogi rysowane z jednej bazy produktów: te używają w kółko tych samych glifów, dokument za dokumentem. Obciążenie, w którym każdy dokument ciągnie inny kawał kroju CJK z dziesiątkami tysięcy glifów, głównie pudłuje, bo bitmapa prawie każdego dokumentu jest nowa. Przerób jedną reprezentatywną partię i przeczytaj liczniki, zanim zdecydujesz, z jakim obciążeniem masz do czynienia
Skąd wiesz, że pamięć podręczna cokolwiek robi?
GetFontSubsetCacheInfo zwraca rekord THPDFFontSubsetCacheInfo z dziewięcioma polami, a HitCount zestawiony z MissCount odpowiada na pytanie wprost. WriteCount i EvictionCount pokazują, czy wpisy żyją dostatecznie długo, by zostać użyte ponownie, czy też są wypychane przez zbyt ciasny budżet. CurrentBytes i FileCount opisują folder taki, jaki jest w danej chwili
Podczas podłączania ich do monitoringu mają znaczenie dwa szczegóły. Siedem liczników zdarzeń należy do instancji THotPDF i startuje od zera w chwili utworzenia komponentu, więc usługa, która tworzy jeden komponent na dokument, widzi w każdym odczycie aktywność jednego dokumentu. CurrentBytes i FileCount przeciwnie — pochodzą z wylistowania folderu w momencie wywołania, więc odzwierciedlają każdy proces korzystający z tego folderu, a przy okazji każde wywołanie kosztuje skan katalogu. Czytaj rekord raz na partię, na jej końcu, i trzymaj pętlę po dokumencie z dala od niego
Pozostałe trzy liczniki to te, na które warto założyć alert. CorruptCount liczy wpisy, które nie przeszły walidacji przy wczytaniu i zostały usunięte; każdy liczy się przy tym jako pudło. Kilka po utracie zasilania jest w normie, a stały strumyk wskazuje na nośnik. RejectedCount liczy podzbiory, których cache odmówił zapisania, zwykle dlatego, że pojedynczy podzbiór jest większy niż cały budżet. WriteFailureCount liczy zapisy, które się nie dokończyły — w praktyce prawie zawsze to folder, do którego konto usługi nie może pisać. Żaden z trzech nie przerywa generowania dokumentu, więc cache z problemem uprawnień do zapisu produkuje dokładnie te same PDF-y co brak cache w ogóle. Tylko nigdy nie robi się szybszy
// SubsetCacheNeedsAttention to pomocnik, który dopisujesz sam;
// progi są przykładowe, dostosuj je do wielkości swojej partii
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;
Budżety, wykluczanie i przełącznik, który opróżnia folder
Wykluczanie działa jak LRU według czasu modyfikacji pliku. Każde udane wczytanie dotyka znacznika czasu wpisu, każdy zapis kończy się przycinaniem, a najstarsze pliki idą pierwsze, dopóki folder nie zmieści się znowu w budżecie. Obniżenie FontSubsetCacheMaxBytes w czasie działania przycina natychmiast, zamiast czekać na kolejny zapis, więc usługa reagująca na alert o miejscu na dysku może zmniejszyć cache w chwili, kiedy sama o tym zdecyduje
Ta natychmiastowość ma ostrą krawędź. Ustawienie budżetu na zero przy skonfigurowanym folderze przycina do zera, czyli kasuje każdy plik .hpdfsubset w środku. Ustawienie FontSubsetCacheFolder na pusty string z kolei wyłącza cache i zostawia pliki dokładnie tam, gdzie są. Oba przełączniki wyłączają cache dla następnego dokumentu; tylko jeden z nich zachowuje tydzień rozgrzanych wpisów. Do diagnostyki opróżnij właściwość folderu i nie ruszaj budżetu
// Wstrzymaj cache na uruchomienie diagnostyczne, zachowaj każdy zapisany wpis
Pdf.FontSubsetCacheFolder := '';
// Uwaga: przy ustawionym folderze zerowy budżet przycina folder do niczego
// Pdf.FontSubsetCacheMaxBytes := 0;
// Celowy reset: usuwa pliki *.hpdfsubset i porzucone pliki tymczasowe,
// liczniki zdarzeń zachowują swoje wartości
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.ClearFontSubsetCache;
Uruchomienie diagnostyczne z wstrzymanym cache to najczystszy sposób, by go wykluczyć. Sześcioliterowy tag podzbioru w /BaseFont jest wyprowadzany z zachowanego zestawu glifów, więc te same glify dają ten sam tag z cache i bez niego, a osadzone programy czcionek powinny się zgadzać. Hasze całych plików i tak będą różne przez daty utworzenia i identyfikator dokumentu, więc porównuj strumienie czcionek, nie pliki
Co się dzieje z uszkodzonym albo na wpół zapisanym wpisem?
Uszkodzony wpis jest kasowany, a podzbiór budowany normalnie, zanim cokolwiek z tego wpisu trafi do strumienia PDF. Przy wczytaniu HotPDF sprawdza sygnaturę pliku, wersję formatu, flagę trybu podzbioru, liczbę glifów względem czcionki źródłowej, zadeklarowane długości względem faktycznego rozmiaru pliku i nagłówek sfnt zapisanego programu czcionki. Każda niezgodność wyrzuca wpis, podbija CorruptCount i spada do subsetowania w pamięci. Zepsuty program czcionki w PDF-ie klienta wychodziłby na jaw tygodnie później, w przeglądarce na czyjejś maszynie, daleko od swojej przyczyny — i właśnie przed taką porażką istnieje ta walidacja
Wpisy na wpół zapisane w ogóle nie mają prawa się pojawić. Zapis idzie najpierw do pliku tymczasowego, którego nazwa niesie identyfikator procesu i instancję komponentu, po czym jest przenoszony na nazwę końcową jednym rename'em z podmianą i opróżnieniem buforów. Awaria w połowie zapisu zostawia zabłąkany plik .tmp-, który sprząta ClearFontSubsetCache, a prawdziwy wpis to albo kompletna stara wersja, albo kompletna nowa. Wpisy compact przechowują obok programu czcionki też mapę starych glifów na nowe, więc zcache'owany podzbiór compact odbuduje tę samą /CIDToGIDMap, a wyjście PDF/A może używać cache bez omijania go
Gdzie powinien mieszkać folder?
Rozstrzygają trzy warunki: konto usługi musi móc tam pisać, folder powinien leżeć na lokalnym magazynie, a żaden krok wdrożenia nie powinien go wymazywać. Zasób sieciowy zamienia każde wyszukanie w rundę do serwera i oddaje semantykę rename'a w ręce serwera plików. Folder w katalogu aplikacji, który instalator odtwarza, oznacza, że cache po każdej aktualizacji startuje zimny — co po cichu zaciera korzyść dokładnie tego dnia, kiedy najbardziej przypatrujesz się wydajności
Kilka komponentów i kilka procesów może dzielić jeden lokalny folder. Nazwy tymczasowe nigdy się nie zderzą, końcowy rename jest atomowy, a przegrany wyścig kosztuje nic gorszego niż pudło albo zapis do ponowienia przy następnym dokumencie. Dzielenie to lepszy domyślny wybór na jednej maszynie, bo każda instancja dogrzewa cache pozostałym. Jedyny widoczny efekt uboczny: liczniki każdej instancji opisują tylko jej własny ruch, podczas gdy CurrentBytes i FileCount opisują ruch wszystkich, a sporadyczne tyknięcie WriteFailureCount może pochodzić z dwóch procesów podmieniających ten sam wpis naraz. Folder tylko do odczytu nadal serwuje istniejące wpisy; nie odświeży jedynie ich znaczników czasu ani nie doda nowych
Kiedy cache to niewłaściwa dźwignia
Cache usuwa powtarzalną robotę. Dla pierwszego dokumentu partii nie daje nic, a dla obciążeń, których zestawy glifów nigdy się nie powtarzają — niewiele. Jeśli wyjście zdominował jeden duży krój CJK używany w nieprzewidywalnym tekście, spójrz, które glify są ciągnięte do każdego podzbioru i dlaczego — omawiają to notki o domknięciu podzbioru czcionki i kształtowaniu glifów. Jeśli celem jest rozmiar pliku, a nie CPU, artykuł o CompressDocument i kompaktowych podzbiorach czcionek wyjaśnia, co tryb compact zmienia w osadzonej czcionce. Gdy partia okaże się wolna z powodów niezwiązanych z czcionkami, przejście przez wyjście raportowe z czcionkami i obrazami pokazuje, gdzie zwykle ucieka reszta czasu, a opis kolejności podzbiorów czcionek w EndDoc obejmuje, kiedy podzbiory są budowane podczas finalizacji dokumentu
Ponieważ cache podzbiorów mieszka wewnątrz komponentu, serwer raportów w Delphi lub C++Builder podbiera go przez ustawienie ścieżki folderu i budżetu, bez dodatkowej usługi do wdrożenia i monitorowania. Osadzanie czcionek, subsetowanie i reszta funkcji generowania PDF jest wypisana na stronie komponentu HotPDF Delphi