O lock de renderização do PDFiumPas é uma seção crítica por documento — EnterRenderLock e LeaveRenderLock, apoiados por um campo TRTLCriticalSection em TPdf — destinado a envolver toda chamada ao rasterizador do PDFium, de modo que uma página não possa ser descarregada ou recarregada por baixo de uma renderização em andamento. Seis métodos, divididos igualmente entre TPdf e TPdfView, chamavam as APIs de bitmap e extração de miniatura do PDFium diretamente e ignoravam esse lock por completo, uma lacuna que o PDFiumPas v2.26.0 fechou envolvendo todos os seis no mesmo par de lock que todo outro ponto de entrada de renderização já usava
A lacuna coberta aqui não é a passada de reforço de ABI coberta em outro lugar neste blog, que percorreu uma incompatibilidade de convenção de chamada cdecl e um truncamento de largura de ponteiro do FPC Win64 na mesma vinculação PDFium. O que se segue é mais restrito e mais mecânico: uma lista de verificação de cobertura de lock para seis pontos de chamada que todos alcançam o caminho de renderização do PDFium, por que cada um foi fácil de perder, e por que a condição de corrida que se segue à falta do lock é um dos defeitos mais difíceis deste código-base para reproduzir sob demanda
O que o lock de renderização de fato protege
O PDFiumPas serializa a renderização porque a página carregada do PDFium não é segura para ler a partir de uma thread enquanto outra thread está livre para liberá-la. TPdf possui um TRTLCriticalSection em FRenderLock, inicializado no construtor e protegido por uma flag FRenderLockReady, de modo que uma chamada que chega depois do desmonte se torna um no-op silencioso, em vez de entrar em uma seção crítica excluída. EnterRenderLock e LeaveRenderLock são a única forma sancionada de entrar e sair dessa seção
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile e RenderPageProgressive já seguiam essa disciplina antes de esta auditoria em particular sequer começar, cada um tomando o lock antes de chamar o PDFium e liberando-o em um bloco finally, de modo que uma pré-renderização em segundo plano e um UnloadPage em primeiro plano na mesma instância de TPdf não pudessem se sobrepor. A lacuna que o PDFiumPas v2.26.0 encontrou não estava nesses pontos de entrada óbvios — apareceu em seis métodos que se leem como acessores, e não como renderizações, mesmo que cada um deles peça ao PDFium para rasterizar pixels antes de poder retornar qualquer coisa
Quais seis chamadas ignoraram o lock de renderização?
TPdf.GetObjectBitmap, TPdf.GetBitmap e TPdf.GetThumbnail compunham metade da lista, e TPdfView.GetObjectBitmap, TPdfView.GetBitmap e TPdfView.GetThumbnail compunham a outra metade — as mesmas três operações, duplicadas entre as duas classes de componente que expõem a mesma página subjacente. Todas as seis eventualmente chamam ou FPDFImageObj_GetBitmap ou FPDFPage_GetThumbnailAsBitmap, e ambos esses pontos de entrada do PDFium rasterizam na hora, em vez de devolver uma referência a algo já renderizado. Nada em nenhum dos seis nomes de método diz render, o que é uma explicação razoável de por que não foram escritos contra a mesma lista de verificação que RenderPage e RenderTile na primeira vez
function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
Bitmap: FPDF_BITMAP;
begin
Result:= nil;
EnterRenderLock;
try
Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
finally
LeaveRenderLock;
end;
if Bitmap<> nil then
try
Result:= ToBitmap(Bitmap);
finally
FPDFBitmap_Destroy(Bitmap);
end;
end;
Por que o TPdfView protege sua chamada de lock com uma verificação de nil
TPdfView não possui sua própria seção crítica — cada uma de suas seis chamadas de lock encaminha para FPdf.EnterRenderLock e FPdf.LeaveRenderLock, envolvida em uma verificação de que a referência TPdf associada não é nil primeiro. Essa proteção existe porque um TPdfView pode sentar em um formulário em tempo de design, ou brevemente entre um documento fechando e o próximo abrindo, sem nenhum TPdf atribuído a FPdf ainda. Pular a proteção trocaria uma quebra por outra, já que uma chamada de bloqueio contra uma referência nil falha não menos graciosamente que a condição de corrida que o lock existe para prevenir
function TPdfView.GetThumbnail: TBitmap;
var
PdfBitmap: FPDF_BITMAP;
begin
CheckActive;
Result:= nil;
if FPdf<> nil then
FPdf.EnterRenderLock;
try
PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
finally
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
if PdfBitmap<> nil then
try
Result:= ToBitmap(PdfBitmap);
finally
FPDFBitmap_Destroy(PdfBitmap);
end;
end;
Por que RenderPage(HDC) pertence à mesma auditoria?
TPdfView.RenderPage contra um contexto de dispositivo não é um dos seis — apareceu em um lançamento anterior, no PDFiumPas v2.25.0, e ganha um lugar nesta lista de verificação porque é o mesmo defeito vestindo uma assinatura diferente. Essa sobrecarga chamava FPDF_RenderPage diretamente sem EnterRenderLock nem a chamada SetArithmeticMask que protege contra exceções de FPU em compiladores Delphi mais antigos, enquanto a sobrecarga TBitmap sentada algumas linhas abaixo na mesma classe já carregava ambos. Duas passadas de auditoria capturando o mesmo modo de falha um lançamento depois diz menos sobre qualquer método individual e mais sobre a forma do bug: ele se esconde em qualquer sobrecarga que ninguém relê depois que sua irmã parece correta
procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
ArithmeticMask: TArithmeticMask;
begin
CheckActive;
if FPdf<> nil then
FPdf.EnterRenderLock;
ArithmeticMask:= SetArithmeticMask;
try
FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
Ord(Rotation), EncodeRenderOptions(Options));
finally
RestoreArithmeticMask(ArithmeticMask);
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
end;
Por que essa condição de corrida é quase impossível de reproduzir?
A lacuna de lock de renderização do PDFiumPas não falha em toda execução, nem mesmo na maioria delas, porque precisa de duas coisas específicas pousando na mesma instância de TPdf ao mesmo tempo: uma chamada de rasterização já em andamento, e um UnloadPage ou ReloadPage concorrente chegando dentro dessa mesma janela. Testes single-thread nunca exercitam o caminho de forma alguma, e mesmo cargas de trabalho genuinamente multi-thread só disparam isso quando uma renderização em segundo plano e um evento de ciclo de vida de documento por acaso se sobrepõem dentro do tempo de vida de uma página. O gatilho mais realista é pré-renderização de PDF em segundo plano construída sobre futures canceláveis, onde uma worker thread rasteriza a próxima página enquanto a thread de UI recarrega ou descarrega a atual sob entrada do usuário
FPDFImageObj_GetBitmap e FPDFPage_GetThumbnailAsBitmap percorrem estruturas de objeto de página que UnloadPage é livre para liberar no meio da travessia, de modo que uma corrida que de fato dispara nem sempre produz uma violação de acesso imediata também. Uma estrutura lida um momento tarde demais pode facilmente devolver pixels de lixo, ou corromper metadados de heap que só quebram várias alocações não relacionadas mais tarde, em uma função que nunca tocou uma página de PDF. Esse é o motivo honesto pelo qual essa classe de bug consegue sobreviver em um código-base ao longo de vários ciclos de lançamento: o stack trace no ponto de falha raramente aponta para perto das seis linhas que de fato estavam sem um lock
O que muda para quem chama
GetBitmap, GetObjectBitmap, GetThumbnail, e a sobrecarga HDC de RenderPage mantêm suas assinaturas públicas exatamente como eram, já que a correção é bloqueio interno adicionado ao redor de chamadas existentes, em vez de uma migração. Vale lembrar que o lock de renderização tem escopo por instância de TPdf, não global ao processo, de modo que duas threads renderizando dois documentos carregados separadamente ainda rodam totalmente em paralelo — o lock só serializa operações contra o único documento que as duas threads por acaso compartilham. Se seu bloqueio já está correto e as renderizações ainda parecem lentas sob zoom ou rolagem, essa é uma questão diferente, respondida em o artigo sobre táticas de cache de renderização e desempenho de zoom do PDFium — correção e velocidade são eixos separados aqui, e essa correção só toca o primeiro
Seis métodos e uma sobrecarga irmã são uma pequena fração da superfície do PDFium que o PDFiumPas expõe, mas foram a fração que só se comportava mal sob carga que por acaso ninguém estava rodando em um depurador. O próprio lock de renderização, e o conjunto completo de pontos de entrada de renderização que agora cobre, vêm como parte do Componente PDFium para Delphi, C++Builder e Lazarus/FPC