Artigo Técnico

Diagnósticos Estruturados em vez de Resultados Booleanos no HotXLS

Rode uma conversão em lote sobre dez mil planilhas durante a noite, e pela manhã três delas voltam False. Esse é todo o post-mortem que um resultado booleano de salvamento dá a você: uma contagem de falhas, sem nada sobre qual arquivo, qual planilha, ou qual entre uma dúzia de causas possíveis foi responsável. O HotXLS, o componente nativo da losLab para Delphi e C++Builder para arquivos Excel, substitui esse único bit por diagnósticos estruturados. A interface IXLSWorkbookProgress expõe uma lista Diagnostics e um evento OnDiagnostic que reportam um código numérico estável, um nível de severidade, a operação que falhou e a planilha onde aconteceu, para cada chamada Open, SaveAs e Recalculate

Por que um resultado booleano de salvamento falha em escala?

Um único arquivo que falha não é o problema que um resultado booleano cria; mil deles são. Quando SaveAs retorna algo diferente de sucesso para três arquivos entre dez mil, a próxima pergunta é sempre a mesma: esses três podem ser tentados novamente, ou precisam de um humano? Um erro de permissão em um compartilhamento de rede não é o mesmo incidente que uma fórmula que o motor de cálculo não consegue avaliar, e nenhum dos dois é o mesmo que uma planilha que silenciosamente excedeu um limite de formato. Com apenas um resultado passa/falha para trabalhar, cada um desses vira um chamado de suporte idêntico, e alguém precisa abrir cada arquivo manualmente, no Excel, e ficar olhando até a causa se tornar óbvia. Essa triagem manual é o custo real de uma API booleana, e ela escala linearmente com o tamanho do lote, que é exatamente a propriedade que você não quer no tratamento de erros

Dentro do IXLSWorkbookProgress: o que um TXLSDiagnostic carrega

IXLSWorkbookProgress é a interface que o HotXLS usa para reportar tanto como uma operação está indo quanto o que deu errado dentro dela, e as duas metades compartilham um contrato por um motivo: ambas são coisas que uma chamada longa de Open, SaveAs ou Recalculate precisa comunicar sem levantar uma exceção no meio da operação. A metade de progresso é OnProgress e OnProgressEx, disparando com uma fase, um estado e um par atual/total. A metade de diagnósticos é sobre a qual este artigo trata: uma propriedade Diagnostics que retorna uma lista TXLSDiagnostics, um atalho LastDiagnostic para a entrada mais recente, e um evento OnDiagnostic que dispara no instante em que cada registro TXLSDiagnostic é criado. Cada registro carrega um Code numérico, uma TXLSDiagnosticSeverity, a TXLSDiagnosticOperation que o produziu, uma Message legível por humanos, um SheetIndex e SheetName, e um NativeCode que preserva qualquer valor de retorno de nível mais baixo que disparou a entrada

var
  Book: TXLSXWorkbook;
  Diag: TXLSDiagnostic;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.SaveAs('quarterly-report.xlsx') <> 1 then
      for I := 0 to Book.Diagnostics.Count - 1 do
      begin
        Diag := Book.Diagnostics[I];
        Writeln(Format('[%d] severity=%d sheet="%s": %s',
          [Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
      end;
  finally
    Book.Free;
  end;
end;

Ler Diagnostics assim já supera um resultado booleano por si só, porque Code e SheetName transformam um mistério em um fato específico e filtrável. O registro TXLSDiagnostic vai além do que este exemplo imprime: RecordId e StreamOffset existem para perícia em nível de byte dentro de um stream BIFF, e PartName guarda a entrada zip OOXML, como xl/worksheets/sheet3.xml, de onde um problema veio. Vale conhecer antes de construir ferramentas em torno deles: no lançamento atual, nenhum dos pontos de chamada de diagnóstico embutidos preenche RecordId ou StreamOffset, de modo que ambos permanecem no padrão de construtor de -1, significando "não aplicável" em vez de "zero". Trate sua ausência como normal, não como um bug no seu handler

Dois motores, uma forma, uma diferença sutil

O HotXLS vem com dois motores por trás desse mesmo modelo de relato, uma fachada BIFF8 para arquivos .xls legados e uma fachada OOXML para .xlsx, e eles não expõem IXLSWorkbookProgress de forma idêntica. TXLSWorkbook, o motor .xls, implementa formalmente IXLSWorkbookProgress, de modo que pode ser passado para qualquer lugar onde esse tipo de interface seja esperado. TXLSXWorkbook, o motor .xlsx, expõe os mesmos membros Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress e OnProgressEx com nomes e tipos idênticos, mas como uma classe simples em vez de uma implementação formal dessa interface, de modo que não vai satisfazer um parâmetro IXLSWorkbookProgress sozinho. Na prática isso raramente importa, porque a maioria do código trabalha contra uma classe concreta de pasta de trabalho por vez, mas significa que você não pode escrever um único auxiliar tipado como IXLSWorkbookProgress e entregar a ele o objeto de pasta de trabalho de qualquer um dos dois motores de forma intercambiável. A única diferença de campo que decorre diretamente da divisão de formato é PartName: só o motor XLSX o preenche, porque só o OOXML tem partes zip para nomear

O que torna um código de diagnóstico algo em que você pode ramificar com segurança?

O campo Code é a única parte de um diagnóstico que vale a pena fixar em uma comparação; Message não é, porque prosa é exatamente o tipo de coisa que é reformulada, retraduzida ou expandida com mais detalhe em um lançamento posterior sem que ninguém a trate como uma mudança que quebra compatibilidade. Os códigos de diagnóstico embutidos do HotXLS já parecem projetados com essa distinção em mente: códigos relacionados a salvamento vão de 1000 a 1005, códigos relacionados a abertura ficam em 1100 e 1101, códigos relacionados a cálculo em 1200 e 1201, e um código de formato não suportado em 1300, com lacunas deixadas dentro de cada faixa em vez de os códigos rodarem consecutivamente por todas elas. Esse espaçamento é o que permite a um fornecedor adicionar um novo modo de falha no momento do salvamento em, digamos, 1006, sem renumerar os códigos dos quais sua instrução switch já depende, e vale a pena verificar isso em qualquer API de diagnóstico antes de você se comprometer a comparar com um código em produção, não só nesta. Mantenha um ramo padrão em sua própria lógica de despacho, independentemente de quão estável a numeração pareça, porque novos modos de falha são exatamente o que um analisador ou escritor em evolução continua descobrindo. NativeCode e ExceptionClass ficam uma camada abaixo de Code para quando você precisa escalar: NativeCode preserva o valor de retorno subjacente, um HRESULT de uma chamada de Structured Storage entre eles, e ExceptionClass registra o tipo de exceção Delphi quando um estava envolvido, o que geralmente já é suficiente para abrir um chamado de suporte preciso sem anexar um stack trace completo

Severidade e operação decidem o que seu código faz em seguida

Severidade e operação são o que transformam um diagnóstico de uma linha de log em uma decisão de roteamento. TXLSDiagnosticSeverity vai de Info, Warning, Error, a Fatal, e TXLSDiagnosticOperation marca cada entrada com a chamada que a produziu: Open, Save, Calculate ou Export. Os dois eixos são independentes por design: xlsDiagnosticUnhandledException é um código fixo único que dispara com Operation definido para qualquer chamada que de fato o levantou, de modo que Code responde o que deu errado enquanto Operation separadamente responde onde, em vez de precisar de um código distinto para uma exceção durante a abertura versus uma durante o salvamento. Essa composabilidade também é o que torna o roteamento mecânico: registrar um aviso e seguir em frente, um salvamento cancelado por meio da flag Aborted é um exemplo típico; contar um erro e manter o lote rodando, uma planilha que falhou ao serializar é um exemplo típico; parar o lote em severidade fatal, porque esse nível significa que uma exceção não tratada já desenrolou a chamada e continuar arrisca trabalhar a partir de um estado parcialmente atualizado. Uma ressalva honesta: Info existe no enum como o padrão com que um TXLSDiagnostic recém-criado começa, mas todo ponto de chamada de diagnóstico embutido no lançamento atual do HotXLS só levanta Warning, Error ou Fatal; Info está reservado para uso futuro, não algo que o motor emite hoje

// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
  Diag := Book.Diagnostics[I];
  case Diag.Severity of
    xlsDiagnosticWarning:
      Writeln(Format('WARN  [%d] %s', [Diag.Code, Diag.Message]));
    xlsDiagnosticError:
      begin
        Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
          [Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
        Inc(FailedSheetCount);
      end;
    xlsDiagnosticFatal:
      raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
  end;
end;

Conectando OnDiagnostic a um pipeline em lote

Consultar Diagnostics depois de cada chamada funciona para um único arquivo; para de funcionar assim que você está de volta àquele lote noturno de dez mil, porque Diagnostics é limpo no início de cada chamada Open, SaveAs e Recalculate. Leia-o depois do terceiro arquivo em um loop e você vê apenas os diagnósticos do terceiro arquivo; o que quer que os dois primeiros arquivos tenham reportado já se foi. OnDiagnostic resolve isso transformando a coleção em um stream: inscreva-se uma vez antes de o loop começar, e o mesmo handler dispara para cada arquivo, em ordem, com o nome do arquivo ainda em escopo por meio de um campo de instância

type
  TBatchConverter = class
  private
    FCurrentFile: string;
    FFailedFiles: TStringList;
    procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
  end;

procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
  if Diagnostic.Severity >= xlsDiagnosticError then
    FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
      [FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;

// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
  FCurrentFile := FileNames[I];
  if Book.Open(FCurrentFile) = 1 then
    Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;

O que o callback realmente custa

OnDiagnostic é barato por um motivo estrutural: só dispara quando algo já está errado, e errado é raro comparado ao número de células, linhas ou planilhas que uma pasta de trabalho contém. Compare isso com OnProgress e OnProgressEx, que reportam progresso rotineiro e tiveram que ser projetados em torno da frequência de chamada desde o início. O HotXLS dispara progresso em nível de planilha uma vez por planilha durante Open e SaveAs, não uma vez por célula ou linha, o que é o que mantém o overhead por chamada pequeno mesmo em pastas de trabalho com milhões de células; Recalculate vai além e limita seu próprio evento de progresso a aproximadamente a cada quatro por cento do grafo de dependências, de modo que um recálculo completo dá a você um pulso em vez de inundar sua thread de UI com eventos. Diagnósticos não precisaram de nenhuma dessa limitação, porque a contagem de eventos é limitada pelo número de problemas reais, não pelo tamanho do arquivo

O único lugar onde o desempenho ainda depende de você é dentro do próprio handler. OnDiagnostic dispara de forma síncrona, na thread que roda Open, SaveAs ou Recalculate, de modo que um handler que bloqueia, uma escrita síncrona para um serviço de log remoto, por exemplo, se torna parte do tempo de parede daquela chamada. Para um único arquivo isso é invisível. Multiplicado por um lote de dez mil arquivos, é a diferença entre um job que termina durante a noite e um que ainda está rodando na hora do almoço, então armazene em buffer o que o handler precisa fazer e descarregue isso de forma assíncrona, em vez de fazer a parte lenta em linha

Diagnósticos estruturados são mais valiosos exatamente onde um resultado booleano é mais fraco, em fluxos de trabalho que tocam muitos arquivos em vez de um. Um pipeline de auditoria e conversão de pastas de trabalho é o exemplo mais claro: em vez de registrar um simples passa/falha por arquivo, anexe a lista Diagnostics de cada arquivo ao seu registro de auditoria, e o relatório diz não apenas o que falhou, mas por quê, que é a maior parte do que nosso artigo sobre construir uma bancada de auditoria e conversão de pastas de trabalho está tentando acertar, para começo de conversa. O mesmo pareamento de progresso e diagnósticos também pertence a qualquer fluxo de trabalho que já precise de relato de progresso por si só, que é exatamente o território coberto em nosso guia sobre desempenho de pastas de trabalho grandes no HotXLS, onde uma chamada longa de Open ou SaveAs é comum o suficiente para que OnProgress já esteja conectado e OnDiagnostic seja uma adição natural e quase gratuita ao lado dele

Nada disso exige o Excel instalado em nenhum lugar do pipeline, e nada disso exige capturar uma exceção genérica e adivinhar o que ela significava. IXLSWorkbookProgress e seus membros Diagnostics, LastDiagnostic e OnDiagnostic fazem parte do Componente HotXLS padrão para Delphi e C++Builder, ao lado da referência completa de códigos de diagnóstico e do restante da superfície Open, SaveAs e Recalculate por que este artigo veio passando