O HotPDF carrega um PDF a partir de qualquer origem de acesso aleatório que implemente, e THPDFCoalescingRandomAccessSource envolve essa origem para que as leituras pequenas e dispersas do analisador se transformem num conjunto limitado de intervalos de blocos em cache com pré-obtenção assíncrona. Num documento servido através de pedidos HTTP por intervalos, isto é a diferença entre algumas centenas de ida-e-volta na rede e apenas algumas dezenas
Nada muda no analisador. Continua a chamar LoadFromRandomAccessSource, o mesmo objeto de documento é devolvido, e a mesma API de páginas funciona. O que muda é o tráfego por baixo
Porque carrega o mesmo PDF instantaneamente em local e arrasta-se pela rede?
Porque um analisador de PDF não lê um ficheiro, navega nele. Salta para o final à procura de startxref, recua até à tabela de referência cruzada, resolve o dicionário trailer, segue uma referência até ao Catalog, depois até à raiz da árvore de páginas, depois até a um nó de página, depois até ao seu dicionário de recursos. Cada um destes passos lê dezenas de bytes a partir de um deslocamento diferente
Num ficheiro local, esse padrão é quase gratuito: o sistema operativo já tem os 4 KiB circundantes em cache, pelo que a segunda leitura custa apenas um memcpy. Através de um transporte de rede não existe essa localidade. Cada leitura é um pedido com a sua própria latência, e 300 pedidos sequenciais a 40 ms cada representam doze segundos gastos quase inteiramente à espera. A solução não é ler menos; o analisador precisa exatamente do que pede. A solução é fazer com que cada leitura física cubra mais daquilo que a próxima leitura lógica vai querer
O que a coalescência muda
A origem de coalescência arredonda cada leitura para cima até um bloco e coloca esse bloco em cache. BlockSize assume por predefinição 262 144 bytes e MaxCacheBytes assume 2 097 152, pelo que oito blocos ficam residentes por predefinição e são despejados por ordem de utilização menos recente contra um orçamento rígido de bytes. A leitura de 40 bytes de uma chave de trailer feita pelo analisador traz consigo os 256 KiB à sua volta, e a dúzia de leituras seguintes nessa vizinhança, onde reside a informação de referência cruzada e do catálogo, são servidas a partir da memória
A sua própria origem mantém-se simples. Implemente GetSize e ReadAt, substitua ReadAtCancellable se o seu transporte conseguir abortar a meio, e deixe o invólucro tratar da cache, da coalescência e da pré-obtenção
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: o invólucro liberta Raw juntamente consigo mesmo
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;
Até onde deve ir a leitura antecipada?
A leitura antecipada adaptativa responde a essa pergunta por documento em vez de o obrigar a adivinhar. Com AdaptiveReadAheadEnabled ativo, a janela cresce ao longo de 1, 2, 4 e 8 blocos à medida que se acumulam leituras sequenciais sustentadas, e nunca excede MaxReadAheadBlocks nem a capacidade de cache configurada. No momento em que chega uma leitura que não está aproximadamente onde a anterior terminou, a janela colapsa e a pré-obtenção é suprimida
SequentialReadToleranceBytes, com predefinição de 4096, define "aproximadamente". Leituras que caiam dentro dessa distância do final da leitura anterior ainda contam como sequenciais, o que importa porque um analisador de PDF a percorrer um fluxo de conteúdo não produz deslocamentos perfeitamente contíguos; salta um campo de comprimento aqui, um dicionário inline ali. Definir a tolerância demasiado baixa faz com que uma leitura sequencial normal seja classificada como aleatória, pelo que a leitura antecipada nunca se ativa. Definir a tolerância demasiado alta faz com que um acesso genuinamente aleatório pareça sequencial, pelo que acaba por obter megabytes que ninguém quer. A predefinição está calibrada para a travessia de fluxos de conteúdo, e as estatísticas dir-lhe-ão se o seu transporte discorda
Esta assimetria é deliberada: o crescimento é gradual, o colapso é imediato. Sobre-obter dados num carregamento de trabalho de acesso aleatório custa largura de banda e dinheiro reais em transportes medidos, pelo que se prefere o erro barato ao erro dispendioso
Cancelamento que efetivamente interrompe a transferência
A classe base declara ReadAtCancellable, e a origem de coalescência honra-a de ponta a ponta. Quando chega uma leitura em primeiro plano para um intervalo que uma pré-obtenção em curso não está a servir, essa pré-obtenção é cancelada em vez de deixada a terminar, pelo que o pedido de página do utilizador não fica em fila atrás de tráfego especulativo. A implementação predefinida em THPDFRandomAccessSource recorre a um simples ReadAt, o que significa que a funcionalidade é opcional por transporte: clientes HTTP que suportam abortar pedidos obtêm um cancelamento genuíno, e origens mais simples continuam a funcionar sem alterações
Combine isso com um token de cancelamento propagado através da sua interface e, quando um utilizador fecha um documento, o tráfego de rede para efetivamente em vez de esperar para escoar. O mesmo modelo de token sustenta a fila de espera descrita em renderização em segundo plano com uma fila de pedidos, pelo que um único token pode cobrir todo o percurso desde a área de visualização até ao socket
Ler as estatísticas da cache de intervalos
GetStatistics preenche um registo THPDFRangeCacheStatistics que separa o que o seu transporte fez do que a cache fez. SourceReadCount e SourceBytesRead são tráfego físico. CacheHitCount e CacheMissCount são tráfego lógico. SequentialReadCount e RandomReadCount mostram como o padrão de acesso foi classificado, CurrentReadAheadBlocks e PeakReadAheadBlocks mostram até onde a janela se abriu, e PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount e SuppressedPrefetchCount mostram se a especulação compensou
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;
Três leituras dizem-lhe o que alterar. Muitas pré-obtenções canceladas com uma contagem de leituras aleatórias elevada significa que o documento está a ser acedido fora de ordem, pelo que deve reduzir MaxReadAheadBlocks e deixar de pagar por largura de banda que descarta. Muitas falhas com uma janela máxima ainda em 1 significa que a tolerância está a rejeitar um padrão que é efetivamente sequencial, pelo que deve aumentar SequentialReadToleranceBytes. E bytes lidos muito além do tamanho do ficheiro significa que a cache está a agitar-se, pelo que deve aumentar MaxCacheBytes antes de mexer em qualquer outra coisa
Os ficheiros linearizados mudam a aritmética
Se controla o produtor, linearizar o documento muda o problema em vez de o otimizar. Um PDF linearizado coloca os objetos da primeira página e uma tabela de sugestões no início do ficheiro, para que um visualizador consiga renderizar a página um a partir do primeiro megabyte sem ver o resto. O HotPDF expõe esse caminho diretamente através de GetProgressiveLinearizedLoadInfo e ReadProgressiveLinearizedFirstPageSection, e o lado da escrita é abordado em gerar PDFs linearizados com tabelas de sugestões
Ambas as técnicas combinam-se. A coalescência torna qualquer documento tolerável numa ligação lenta; a linearização faz a primeira página chegar rapidamente em documentos que produz internamente. Para ficheiros que residem em disco local mas são demasiado grandes para caberem em memória, os caminhos de ficheiro mapeado e de fluxo lento descritos em o fluxo de trabalho da API de ficheiro direto costumam ser a melhor ferramenta, uma vez que não há latência de ida-e-volta a amortizar em primeiro lugar
O HotPDF é um componente PDF VCL nativo para Delphi e C++Builder, sem DLL externa para o analisador e com código-fonte completo disponível. A API de origem de acesso aleatório, o invólucro de coalescência e os pontos de entrada de carregamento progressivo estão documentados na página do componente HotPDF para Delphi