Alta Memória de Pico Durante OCR em Massa
Executar OCR em muitos segmentos de PDF de uma vez multiplica o uso de memória. Cada tarefa renderiza bitmaps de página inteira através de um OcrInput, e um motor IronTesseract novo por segmento recarrega os arquivos do modelo de linguagem toda vez. Na plena concorrência do processador isso eleva a memória de pico para a faixa de vários GB, com picos que falham em ambientes com memória limitada.
O OCR é naturalmente pesado em memória. Cada OcrInput renderiza bitmaps de página inteira, e cada motor IronTesseract carrega arquivos do modelo de linguagem na memória. Criar um novo motor por segmento recarrega esses modelos repetidamente, e executar uma tarefa de OCR por núcleo de CPU (Environment.ProcessorCount) permite que muitos trabalhos pesados em bitmap sejam executados lado a lado. Sem nada limitando quantas tarefas estão ativas, a memória de pico escala diretamente com a concorrência.
A solução é limitar o número de tarefas em andamento: limitar a concorrência, reutilizar motores de um pool e organizar o trabalho com um semáforo.
Solução
1. Limitar a concorrência do OCR
Limite o número de tarefas de OCR simultâneas a um teto pequeno. Menos tarefas simultâneas significam menos bitmaps de página inteira na memória de uma vez, o que reduz diretamente o pico. Ajuste o teto à capacidade da máquina.
// Clamp concurrency to avoid memory saturation and CPU over-subscription.
int concurrency = Math.Clamp(Environment.ProcessorCount / 2, 1, 4);
// Clamp concurrency to avoid memory saturation and CPU over-subscription.
int concurrency = Math.Clamp(Environment.ProcessorCount / 2, 1, 4);
Imports System
' Clamp concurrency to avoid memory saturation and CPU over-subscription.
Dim concurrency As Integer = Math.Clamp(Environment.ProcessorCount \ 2, 1, 4)
2. Pool dos motores
Crie exatamente um motor IronTesseract por slot concorrente na inicialização e reutilize-os em todos os segmentos, em vez de construir um novo motor e recarregar o modelo de linguagem cada vez.
// Pre-create one engine per concurrent slot and reuse them across segments.
var enginePool = new ConcurrentBag<IronTesseract>(
Enumerable.Range(0, concurrency).Select(_ => new IronTesseract())
);
// Pre-create one engine per concurrent slot and reuse them across segments.
var enginePool = new ConcurrentBag<IronTesseract>(
Enumerable.Range(0, concurrency).Select(_ => new IronTesseract())
);
' Pre-create one engine per concurrent slot and reuse them across segments.
Dim enginePool As New ConcurrentBag(Of IronTesseract)(
Enumerable.Range(0, concurrency).Select(Function(_) New IronTesseract())
)
Construir o pool uma vez amortiza o custo de carregar o modelo de linguagem em toda a execução em vez de pagar por isso por segmento.
3. Organizem o trabalho com um semáforo
Inicialize um SemaphoreSlim para o limite de concorrência e envolva-o em using. Cada tarefa chama WaitAsync() antes de começar e Release() em um finally, portanto, apenas o número permitido de segmentos está em andamento de uma vez.
using var semaphore = new SemaphoreSlim(concurrency);
await semaphore.WaitAsync();
try
{
// Rent a pre-loaded engine from the pool.
if (!enginePool.TryTake(out var ocr))
ocr = new IronTesseract(); // Defensive fallback; should never be reached.
try
{
using var input = new OcrInput();
input.LoadPdf(segmentStream); // page-range segment produced upstream
var ocrResult = await ocr.ReadAsync(input);
ocrResult.SaveAsSearchablePdf(outputPath);
}
finally
{
enginePool.Add(ocr); // Return engine to pool for the next waiting segment.
}
}
finally
{
semaphore.Release();
}
using var semaphore = new SemaphoreSlim(concurrency);
await semaphore.WaitAsync();
try
{
// Rent a pre-loaded engine from the pool.
if (!enginePool.TryTake(out var ocr))
ocr = new IronTesseract(); // Defensive fallback; should never be reached.
try
{
using var input = new OcrInput();
input.LoadPdf(segmentStream); // page-range segment produced upstream
var ocrResult = await ocr.ReadAsync(input);
ocrResult.SaveAsSearchablePdf(outputPath);
}
finally
{
enginePool.Add(ocr); // Return engine to pool for the next waiting segment.
}
}
finally
{
semaphore.Release();
}
Imports System.Threading
Imports IronOcr
Dim semaphore As New SemaphoreSlim(concurrency)
Await semaphore.WaitAsync()
Try
' Rent a pre-loaded engine from the pool.
Dim ocr As IronTesseract = Nothing
If Not enginePool.TryTake(ocr) Then
ocr = New IronTesseract() ' Defensive fallback; should never be reached.
End If
Try
Using input As New OcrInput()
input.LoadPdf(segmentStream) ' page-range segment produced upstream
Dim ocrResult = Await ocr.ReadAsync(input)
ocrResult.SaveAsSearchablePdf(outputPath)
End Using
Finally
enginePool.Add(ocr) ' Return engine to pool for the next waiting segment.
End Try
Finally
semaphore.Release()
End Try
A chamada WaitAsync() bloqueia até que um slot seja liberado, e o retorno do motor no finally interno entrega um motor pré-carregado diretamente para o próximo segmento aguardando.
4. Disponibilizar OcrInput por segmento
Envolva cada OcrInput em using para que seus bitmaps de página renderizados sejam liberados no momento em que o segmento é lido, antes que a próxima tarefa ocupe o slot.
using em OcrInput é o que impede a acumulação de memória de bitmap entre segmentos; sem isso, slots liberados ainda conteriam seus bitmaps de página.

