HotPDF wczytuje plik PDF z dowolnego źródła o dostępie swobodnym, które sam zaimplementujesz, a THPDFCoalescingRandomAccessSource opakowuje to źródło tak, że rozproszone drobne odczyty analizatora zamieniają się w ograniczony zestaw buforowanych zakresów blokowych z asynchronicznym odczytem wyprzedzającym. Przy dokumencie serwowanym przez żądania zakresów HTTP to różnica między kilkuset podróżami w obie strony a kilkudziesięcioma
Nic w analizatorze się nie zmienia. Nadal wywołujesz LoadFromRandomAccessSource, wraca ten sam obiekt dokumentu, a to samo API stron działa. Zmienia się ruch pod spodem
Dlaczego ten sam plik PDF wczytuje się natychmiast lokalnie, a pełznie przez sieć?
Ponieważ analizator PDF nie czyta pliku, tylko po nim nawiguje. Przeskakuje na koniec w poszukiwaniu startxref, skacze z powrotem do tablicy odsyłaczy krzyżowych, rozwiązuje słownik trailer, podąża za odwołaniem do katalogu, potem do korzenia drzewa stron, potem do węzła strony, potem do jego słownika zasobów. Każdy z tych kroków czyta dziesiątki bajtów z innego przesunięcia
Na lokalnym pliku ten wzorzec jest niemal darmowy: system operacyjny już ma zbuforowaną otaczającą stronę 4 KiB, więc drugi odczyt kosztuje tyle co memcpy. Przez transport sieciowy nie ma takiej lokalności. Każdy odczyt to żądanie z własnym opóźnieniem, a 300 sekwencyjnych żądań po 40 ms każde to dwanaście sekund spędzonych niemal wyłącznie na czekaniu. Rozwiązaniem nie jest czytanie mniej; analizator potrzebuje dokładnie tego, o co prosi. Rozwiązaniem jest sprawienie, by każdy fizyczny odczyt obejmował więcej z tego, czego zażąda kolejny odczyt logiczny
Co zmienia scalanie
Źródło scalające zaokrągla każdy odczyt w górę do bloku i buforuje ten blok. BlockSize domyślnie wynosi 262 144 bajty, a MaxCacheBytes 2 097 152, więc domyślnie rezyduje osiem bloków i są one usuwane w kolejności najdawniej używanych względem twardego budżetu bajtowego. 40-bajtowy odczyt klucza trailer wykonywany przez analizator ściąga 256 KiB wokół niego, a kolejny tuzin odczytów w tym sąsiedztwie, gdzie mieszkają dane odsyłaczy krzyżowych i katalogu, jest obsługiwany z pamięci
Twoje własne źródło pozostaje proste. Zaimplementuj GetSize i ReadAt, nadpisz ReadAtCancellable, jeśli twój transport potrafi przerwać w locie, a resztę, czyli buforowanie, scalanie i odczyt wyprzedzający, zostaw opakowaniu
type
THttpRangeSource = class(THPDFRandomAccessSource)
private
FClient: TMyHttpClient;
FUrl: string;
FSize: Int64;
public
function GetSize: Int64; override;
function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
CancellationToken: THPDFCancellationToken): Longint; override;
end;
var
Raw: THttpRangeSource;
Cached: THPDFCoalescingRandomAccessSource;
Pdf: THotPDF;
begin
Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
// OwnsSource=True: opakowanie zwalnia Raw razem z sobą
Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
Pdf := THotPDF.Create(nil);
try
Cached.AsyncPrefetchEnabled := True;
Cached.AdaptiveReadAheadEnabled := True;
Cached.MaxReadAheadBlocks := 8;
if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Jak daleko powinien sięgać odczyt wyprzedzający?
Adaptacyjny odczyt wyprzedzający odpowiada na to pytanie osobno dla każdego dokumentu, zamiast zmuszać cię do zgadywania. Przy włączonym AdaptiveReadAheadEnabled okno rośnie przez 1, 2, 4 i 8 bloków w miarę kumulowania się utrzymujących się odczytów w przód i nigdy nie przekracza MaxReadAheadBlocks ani skonfigurowanej pojemności pamięci podręcznej. W chwili, gdy nadchodzi odczyt, który nie kończy się mniej więcej tam, gdzie skończył się poprzedni, okno zapada się, a prefetch zostaje wstrzymany
SequentialReadToleranceBytes, domyślnie 4096, definiuje to „mniej więcej”. Odczyty mieszczące się w tej odległości od końca poprzedniego odczytu wciąż liczą się jako sekwencyjne, co ma znaczenie, ponieważ analizator PDF przechodzący przez strumień treści nie generuje idealnie ciągłych przesunięć; pomija tu pole długości, tam wewnętrzny słownik. Ustaw tolerancję zbyt nisko, a normalne skanowanie w przód zostanie zaklasyfikowane jako losowe, więc odczyt wyprzedzający nigdy się nie włączy. Ustaw ją zbyt wysoko, a prawdziwy dostęp losowy wygląda na sekwencyjny, więc ściągasz megabajty, których nikt nie chce. Wartość domyślna jest skalibrowana pod przechodzenie przez strumień treści, a statystyki powiedzą ci, czy twój transport się z tym nie zgadza
Ta asymetria jest celowa: wzrost jest stopniowy, zapaść jest natychmiastowa. Nadmierne pobieranie przy obciążeniu dostępu losowego kosztuje realne pasmo i realne pieniądze na taryfowanych transportach, więc preferowana jest tania pomyłka nad drogą
Anulowanie, które faktycznie zatrzymuje transfer
Klasa bazowa deklaruje ReadAtCancellable, a źródło scalające honoruje to od początku do końca. Gdy nadchodzi odczyt pierwszoplanowy dla zakresu, którego nie obsługuje trwający prefetch, prefetch zostaje anulowany zamiast dokończony, więc żądanie strony użytkownika nie stoi w kolejce za spekulacyjnym ruchem. Domyślna implementacja w THPDFRandomAccessSource przechodzi na zwykłe ReadAt, co oznacza, że funkcja jest opcjonalna per transport: klienci HTTP obsługujący przerywanie żądań dostają prawdziwe anulowanie, a prostsze źródła nadal działają bez zmian
Połącz to z tokenem anulowania przeprowadzonym przez twój interfejs użytkownika, a użytkownik zamykający dokument faktycznie zatrzymuje ruch sieciowy, zamiast czekać, aż się dokończy. Ten sam model tokenów leży u podstaw kolejkowania opisanego w renderowaniu w tle z kolejką żądań, więc jeden token może pokryć całą ścieżkę od widoku po gniazdo
Odczytywanie statystyk pamięci podręcznej zakresów
GetStatistics wypełnia rekord THPDFRangeCacheStatistics, który oddziela to, co zrobił twój transport, od tego, co zrobiła pamięć podręczna. SourceReadCount i SourceBytesRead to ruch fizyczny. CacheHitCount i CacheMissCount to ruch logiczny. SequentialReadCount i RandomReadCount pokazują, jak sklasyfikowano wzorzec dostępu, CurrentReadAheadBlocks i PeakReadAheadBlocks pokazują, jak szeroko otworzyło się okno, a PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount i SuppressedPrefetchCount pokazują, czy spekulacja się opłaciła
var
S: THPDFRangeCacheStatistics;
begin
Cached.GetStatistics(S);
Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
[S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
[S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
[S.PrefetchRequestCount, S.PrefetchCompletedCount,
S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;
Trzy odczyty mówią ci, co zmienić. Wiele anulowanych prefetchy przy wysokiej liczbie odczytów losowych oznacza, że dokument jest odczytywany nie po kolei, więc obniż MaxReadAheadBlocks i przestań płacić za pasmo, które i tak odrzucasz. Wiele chybień przy oknie wciąż utrzymującym się na poziomie 1 oznacza, że tolerancja odrzuca wzorzec, który w praktyce jest sekwencyjny, więc podnieś SequentialReadToleranceBytes. A bajty odczytane znacznie przekraczające rozmiar pliku oznaczają, że pamięć podręczna się miota, więc podnieś MaxCacheBytes, zanim ruszysz cokolwiek innego
Zlinearyzowane pliki zmieniają arytmetykę
Jeśli kontrolujesz producenta, linearyzacja dokumentu zmienia problem, zamiast go optymalizować. Zlinearyzowany plik PDF umieszcza obiekty pierwszej strony i tablicę wskazówek na początku pliku, więc przeglądarka może wyrenderować pierwszą stronę z otwierającego megabajta, nie widząc reszty. HotPDF udostępnia tę ścieżkę bezpośrednio przez GetProgressiveLinearizedLoadInfo i ReadProgressiveLinearizedFirstPageSection, a strona zapisu jest omówiona w generowaniu zlinearyzowanych plików PDF z tablicami wskazówek
Obie techniki się łączą. Scalanie sprawia, że dowolny dokument staje się znośny przez wolne łącze; linearyzacja sprawia, że pierwsza strona pojawia się szybko w dokumentach, które sam produkujesz. W przypadku plików, które żyją na lokalnym dysku, ale są zbyt duże, by zmieścić się w pamięci, ścieżki mapowanego pliku i leniwego strumienia opisane w przepływie pracy bezpośredniego API plikowego są zwykle lepszym narzędziem, ponieważ nie ma tam w ogóle opóźnienia podróży w obie strony do zamortyzowania
HotPDF to natywny komponent VCL PDF dla Delphi i C++Builder, bez zewnętrznej biblioteki DLL dla analizatora i z pełnym dostępnym źródłem. API źródła o dostępie swobodnym, opakowanie scalające i punkty wejścia wczytywania progresywnego są udokumentowane na stronie komponentu HotPDF dla Delphi