HotPDF carga un PDF desde cualquier origen de acceso aleatorio que usted implemente, y THPDFCoalescingRandomAccessSource envuelve ese origen de modo que las lecturas pequeñas y dispersas del analizador se convierten en un conjunto acotado de rangos de bloque en caché con precarga asíncrona. En un documento servido mediante peticiones de rango HTTP, esa es la diferencia entre unos pocos cientos de idas y vueltas y unas pocas docenas
Nada del analizador cambia. Sigue llamando a LoadFromRandomAccessSource, obtiene el mismo objeto de documento y funciona la misma API de páginas. Lo que cambia es el tráfico por debajo
¿Por qué el mismo PDF carga al instante en local y se arrastra por la red?
Porque un analizador de PDF no lee un fichero, lo navega. Salta al final para buscar startxref, retrocede a la tabla de referencias cruzadas, resuelve el diccionario trailer, sigue una referencia hasta el Catalog, luego hasta la raíz del árbol de páginas, luego hasta un nodo de página, luego hasta su diccionario de recursos. Cada uno de esos pasos lee decenas de bytes de un desplazamiento distinto
En un fichero local ese patrón es casi gratis: el sistema operativo ya tiene en caché la página de 4 KiB circundante, así que la segunda lectura cuesta un memcpy. Sobre un transporte de red no existe esa localidad. Cada lectura es una petición con su propia latencia, y 300 peticiones secuenciales a 40 ms cada una son doce segundos gastados casi por completo en esperar. La solución no es leer menos; el analizador necesita exactamente lo que pide. La solución es hacer que cada lectura física cubra más de lo que la siguiente lectura lógica va a querer
Qué cambia la fusión
El origen de fusión redondea cada lectura hacia arriba hasta un bloque y almacena en caché ese bloque. BlockSize vale por defecto 262.144 bytes y MaxCacheBytes 2.097.152, así que por defecto hay ocho bloques residentes que se desalojan por orden de uso menos reciente contra un presupuesto de bytes estricto. La lectura de 40 bytes que hace el analizador sobre una clave del trailer arrastra los 256 KiB que la rodean, y la siguiente docena de lecturas en esa vecindad, que es donde vive la referencia cruzada y los datos del catálogo, se sirven desde memoria
Su propio origen se mantiene sencillo. Implemente GetSize y ReadAt, sobrescriba ReadAtCancellable si su transporte puede abortar en pleno vuelo, y deje que el envoltorio gestione el caché, la fusión y la precarga
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: el envoltorio libera Raw junto con él mismo
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;
¿Hasta dónde debe adelantarse la lectura?
La lectura anticipada adaptativa responde a esa pregunta por cada documento en lugar de obligarle a adivinar. Con AdaptiveReadAheadEnabled activado, la ventana crece por 1, 2, 4 y 8 bloques a medida que se acumulan lecturas secuenciales sostenidas hacia adelante, y nunca supera MaxReadAheadBlocks ni la capacidad de caché configurada. En el momento en que llega una lectura que no está aproximadamente donde terminó la anterior, la ventana se colapsa y la precarga se suprime
SequentialReadToleranceBytes, con valor 4.096 por defecto, define "aproximadamente". Las lecturas que caen dentro de esa distancia del final de la lectura anterior siguen contando como secuenciales, lo cual importa porque un analizador de PDF que recorre un flujo de contenido no produce desplazamientos perfectamente contiguos; se salta un campo de longitud aquí, un diccionario en línea allá. Fije la tolerancia demasiado baja y un barrido normal hacia adelante se clasifica como aleatorio, así que la lectura anticipada nunca se activa. Fíjela demasiado alta y el acceso genuinamente aleatorio parece secuencial, así que trae megabytes que nadie quiere. El valor por defecto está calibrado para el recorrido de flujos de contenido, y las estadísticas le dirán si su transporte discrepa
Esta asimetría es deliberada: el crecimiento es gradual, el colapso es inmediato. Sobreleer en una carga de trabajo de acceso aleatorio cuesta ancho de banda real y dinero real en transportes medidos, así que se prefiere el error barato sobre el caro
Cancelación que realmente detiene la transferencia
La clase base declara ReadAtCancellable, y el origen de fusión lo respeta de principio a fin. Cuando llega una lectura en primer plano para un rango que una precarga en curso no está sirviendo, la precarga se cancela en lugar de dejarla terminar, así que la petición de página del usuario no queda en cola detrás de tráfico especulativo. La implementación por defecto en THPDFRandomAccessSource recae en un simple ReadAt, lo que significa que la función es opcional por transporte: los clientes HTTP que admiten abortar peticiones obtienen cancelación genuina, y los orígenes más simples siguen funcionando sin cambios
Combine eso con un token de cancelación que atraviese su interfaz y un usuario que cierra un documento realmente detiene el tráfico de red en lugar de esperar a que se drene. El mismo modelo de token sustenta la cola descrita en el renderizado en segundo plano con cola de peticiones, así que un único token puede cubrir toda la ruta desde el visor hasta el socket
Cómo leer las estadísticas de la caché de rangos
GetStatistics rellena un registro THPDFRangeCacheStatistics que separa lo que hizo su transporte de lo que hizo la caché. SourceReadCount y SourceBytesRead son tráfico físico. CacheHitCount y CacheMissCount son tráfico lógico. SequentialReadCount y RandomReadCount muestran cómo se clasificó el patrón de acceso, CurrentReadAheadBlocks y PeakReadAheadBlocks muestran hasta dónde se abrió la ventana, y PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount y SuppressedPrefetchCount muestran si la especulación salió rentable
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;
Tres lecturas le dicen qué cambiar. Muchas precargas canceladas con un recuento de lecturas aleatorias alto significa que el documento se está accediendo fuera de orden, así que baje MaxReadAheadBlocks y deje de pagar por ancho de banda que descarta. Muchos fallos con una ventana pico que sigue en 1 significa que la tolerancia está rechazando un patrón que en la práctica es secuencial, así que suba SequentialReadToleranceBytes. Y bytes leídos muy por encima del tamaño del fichero significa que la caché está sufriendo thrashing, así que suba MaxCacheBytes antes de tocar cualquier otra cosa
Los ficheros linealizados cambian la aritmética
Si usted controla el productor, linealizar el documento cambia el problema en vez de solo optimizarlo. Un PDF linealizado coloca los objetos de la primera página y una tabla de sugerencias al frente del fichero, así que un visor puede renderizar la página uno desde el primer megabyte sin ver el resto. HotPDF expone esa ruta directamente a través de GetProgressiveLinearizedLoadInfo y ReadProgressiveLinearizedFirstPageSection, y el lado de escritura se cubre en la generación de PDFs linealizados con tablas de sugerencias
Ambas técnicas se combinan bien. La fusión hace tolerable cualquier documento sobre un enlace lento; la linealización hace que la primera página llegue rápido en documentos que usted mismo produce. Para ficheros que viven en un disco local pero son demasiado grandes para caber en memoria, las rutas de fichero mapeado y flujo perezoso descritas en la API directa de ficheros suelen ser la mejor herramienta, ya que ahí no hay latencia de ida y vuelta que amortizar de entrada
HotPDF es un componente VCL nativo para Delphi y C++Builder, sin DLL externa para el analizador y con código fuente completo disponible. La API de origen de acceso aleatorio, el envoltorio de fusión y los puntos de entrada de carga progresiva están documentados en la página de HotPDF para Delphi