Artigo Técnico

Diagnósticos Estruturados em Vez de Resultados Booleanos no HotXLS

Execute uma conversão em lote sobre dez mil folhas de cálculo durante a noite, e de manhã três delas voltam False. Esse é todo o relatório pós-morte que um resultado booleano de gravação lhe dá: uma contagem de falhas, sem nada sobre qual ficheiro, qual folha, ou qual de uma dúzia de causas possíveis foi responsável. O HotXLS, o componente nativo da losLab para Delphi e C++Builder para ficheiros 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 folha onde aconteceu, para cada chamada a Open, SaveAs, e Recalculate

Porque falha um resultado booleano de gravação à escala?

Um único ficheiro falhado não é o problema que um resultado booleano cria; mil deles são. Quando SaveAs devolve algo diferente de sucesso para três ficheiros em dez mil, a pergunta seguinte é sempre a mesma: são estes três repetíveis, ou precisam de uma pessoa? Um erro de permissão numa partilha 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 folha de cálculo que ultrapassou silenciosamente um limite de formato. Com apenas um resultado de sucesso/falha para trabalhar, cada um destes torna-se um pedido de suporte idêntico, e alguém tem de abrir cada ficheiro à mão, no Excel, e olhar para ele até a causa se tornar óbvia. Essa triagem manual é o custo real de uma API booleana, e escala linearmente com o tamanho do lote, que é exatamente a propriedade que não se quer no tratamento de erros

Dentro do IXLSWorkbookProgress: o que um TXLSDiagnostic transporta

IXLSWorkbookProgress é a interface que o HotXLS usa para reportar tanto como uma operação está a decorrer como o que correu mal dentro dela, e as duas metades partilham um contrato por uma razão: ambas são coisas que uma chamada longa a Open, SaveAs, ou Recalculate precisa de comunicar sem levantar uma exceção a meio da operação. A metade do progresso é OnProgress e OnProgressEx, que disparam com uma fase, um estado, e um par atual/total. A metade dos diagnósticos é a de que este artigo trata: uma propriedade Diagnostics que devolve uma lista TXLSDiagnostics, um atalho LastDiagnostic para a entrada mais recente, e um evento OnDiagnostic que dispara no momento em que cada registo TXLSDiagnostic é criado. Cada registo transporta 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 inferior que tenha disparado 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 desta forma já supera um resultado booleano por si só, porque Code e SheetName transformam um mistério num facto específico e filtrável. O registo TXLSDiagnostic vai mais longe do que este exemplo imprime: RecordId e StreamOffset existem para perícia ao nível de byte dentro de um fluxo BIFF, e PartName guarda a entrada zip OOXML, como xl/worksheets/sheet3.xml, de onde um problema veio. Vale a pena saber antes de construir ferramentas à volta destes: na versão atual, nenhum dos pontos de chamada de diagnóstico incorporados preenche RecordId ou StreamOffset, pelo que ambos ficam no seu valor predefinido de construtor de -1, significando "não aplicável" e não "zero". Trate a sua ausência como normal, e não como um bug no seu manipulador

Dois motores, uma forma, uma diferença discreta

O HotXLS distribui dois motores por trás deste mesmo modelo de relatório, uma fachada BIFF8 para ficheiros .xls legados e uma fachada OOXML para .xlsx, e não expõem IXLSWorkbookProgress de forma idêntica. TXLSWorkbook, o motor .xls, implementa formalmente IXLSWorkbookProgress, pelo que pode ser passado onde quer que 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, pelo que não satisfará um parâmetro IXLSWorkbookProgress por si só. Na prática isto raramente importa, porque a maioria do código trabalha contra um objeto de livro de cálculo concreto de cada vez, mas significa que não se pode escrever um único auxiliar tipado como IXLSWorkbookProgress e entregar-lhe o objeto de livro de cálculo de qualquer um dos motores de forma intercambiável. A única diferença de campo que decorre diretamente da divisão de formatos é 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 se pode ramificar com segurança?

O campo Code é a única parte de um diagnóstico que vale a pena fixar numa comparação; Message não é, porque prosa é exatamente o tipo de coisa que é reformulada, retraduzida, ou expandida com mais detalhe numa versão posterior sem que ninguém a trate como uma alteração de rutura. Os códigos de diagnóstico incorporados no HotXLS já leem como tendo sido projetados com essa distinção em mente: os códigos relacionados com gravação vão de 1000 a 1005, os códigos relacionados com abertura situam-se em 1100 e 1101, os códigos relacionados com 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 correrem consecutivamente através de todas elas. Esse espaçamento é o que permite a um fornecedor acrescentar um novo modo de falha em tempo de gravação, digamos, em 1006, sem renumerar os códigos de que a sua instrução switch já depende, e vale a pena verificar isto em qualquer API de diagnóstico antes de se comprometer a corresponder a um código em produção, não apenas esta. Mantenha um ramo predefinido na 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 a descobrir. NativeCode e ExceptionClass situam-se uma camada abaixo de Code para quando é preciso escalar: NativeCode preserva o valor de retorno subjacente, um HRESULT de uma chamada de Structured Storage entre eles, e ExceptionClass regista o tipo de exceção Delphi quando uma esteve envolvida, o que normalmente é suficiente para abrir um pedido de suporte preciso sem anexar um stack trace completo

Severidade e operação decidem o que o seu código faz a seguir

Severidade e operação são o que transforma um diagnóstico de uma linha de registo numa decisão de encaminhamento. TXLSDiagnosticSeverity percorre Info, Warning, Error, e 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 que dispara com Operation definido como seja qual for a chamada que efetivamente o levantou, pelo que Code responde ao que correu mal enquanto Operation responde separadamente onde, em vez de precisar de um código distinto para uma exceção durante a abertura versus uma durante a gravação. Essa composabilidade é também o que torna o encaminhamento mecânico: registar um aviso e continuar, uma gravação cancelada através da flag Aborted é um exemplo típico; contar um erro e manter o lote a correr, uma folha de cálculo que falhou a serialização é um exemplo típico; parar o lote numa 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 meio atualizado. Uma ressalva honesta: Info existe na enumeração como o valor predefinido com que um TXLSDiagnostic recém-criado começa, mas cada ponto de chamada de diagnóstico incorporado na versão atual do HotXLS só alguma vez levanta Warning, Error, ou Fatal; Info está reservado para uso futuro, não algo que o motor emita 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;

Ligar o OnDiagnostic a um pipeline de lote

Fazer polling de Diagnostics depois de cada chamada funciona para um único ficheiro; deixa de funcionar assim que se está de volta àquele lote noturno de dez mil, porque Diagnostics é limpo no início de cada chamada a Open, SaveAs, e Recalculate. Leia-o depois do terceiro ficheiro num ciclo e só vê os diagnósticos do terceiro ficheiro; o que os dois primeiros ficheiros reportaram já desapareceu. OnDiagnostic resolve isto transformando a coleção num fluxo: subscreva uma vez antes de o ciclo começar, e o mesmo manipulador dispara para cada ficheiro, por ordem, com o nome do ficheiro ainda em âmbito através 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 efetivamente custa

OnDiagnostic é barato por uma razão estrutural: só dispara quando algo já está errado, e errado é raro comparado com o número de células, linhas, ou folhas que um livro de cálculo contém. Compare isso com OnProgress e OnProgressEx, que reportam progresso de rotina e tiveram de ser projetados à volta da frequência de chamadas desde o início. O HotXLS dispara progresso ao nível de folha uma vez por folha durante Open e SaveAs, não uma vez por célula ou linha, o que é o que mantém a sobrecarga por chamada pequena mesmo em livros de cálculo com milhões de células; Recalculate vai mais longe e limita o seu próprio evento de progresso a aproximadamente cada quatro por cento do grafo de dependências, pelo que um recálculo completo dá um sinal de vida em vez de inundar a sua thread de UI com eventos. Os 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 ficheiro

O único sítio onde o desempenho ainda depende do utilizador é dentro do próprio manipulador. OnDiagnostic dispara de forma síncrona, na thread que executa Open, SaveAs, ou Recalculate, pelo que um manipulador que bloqueie, uma escrita síncrona para um serviço de registo remoto por exemplo, torna-se parte do tempo de relógio dessa chamada. Para um único ficheiro isso é invisível. Multiplicado por um lote de dez mil ficheiros é a diferença entre um trabalho que termina durante a noite e um que ainda está a correr à hora de almoço, pelo que se deve armazenar em buffer o que o manipulador precisa de fazer e libertá-lo de forma assíncrona, em vez de fazer a parte lenta em linha

Os diagnósticos estruturados são mais valiosos precisamente onde um resultado booleano é mais fraco, em fluxos de trabalho que tocam em muitos ficheiros em vez de um. Um pipeline de auditoria e conversão de livros de cálculo é o exemplo mais claro: em vez de registar um simples sucesso/falha por ficheiro, anexe a lista Diagnostics de cada ficheiro ao seu registo de auditoria, e o relatório diz não só o que falhou mas porquê, que é a maior parte do que o nosso artigo sobre construir um banco de trabalho de auditoria e conversão de livros de cálculo tenta acertar em primeiro lugar. O mesmo emparelhamento de progresso e diagnósticos também pertence a qualquer fluxo de trabalho que já precise de relatório de progresso por si só, que é exatamente o território abordado no nosso guia de desempenho de grandes livros de cálculo no HotXLS, onde uma chamada longa a Open ou SaveAs é suficientemente comum para que OnProgress já esteja ligado e OnDiagnostic seja um acréscimo natural, quase gratuito, ao seu lado

Nada disto exige o Excel instalado em qualquer ponto do pipeline, e nada disto exige apanhar uma exceção genérica e adivinhar o que significava. IXLSWorkbookProgress e os seus membros Diagnostics, LastDiagnostic, e OnDiagnostic fazem parte do componente HotXLS standard para Delphi e C++Builder, a par da referência completa de códigos de diagnóstico e do resto da superfície de Open, SaveAs, e Recalculate que este artigo tem vindo a percorrer