Article technique

Diagnostics structurés plutôt que des résultats booléens dans HotXLS

Exécutez une conversion par lot sur dix mille tableurs pendant la nuit, et au matin trois d'entre eux reviennent avec False. C'est tout le post-mortem qu'un résultat de sauvegarde booléen vous donne : un décompte d'échecs, sans aucune information sur quel fichier, quelle feuille, ou laquelle d'une douzaine de causes possibles en était responsable. HotXLS, le composant natif losLab pour Delphi et C++Builder pour les fichiers Excel, remplace ce bit unique par des diagnostics structurés. L'interface IXLSWorkbookProgress expose une liste Diagnostics et un événement OnDiagnostic qui rapportent un code numérique stable, un niveau de sévérité, l'opération qui a échoué, et la feuille où cela s'est produit, pour chaque appel à Open, SaveAs, et Recalculate

Pourquoi un résultat de sauvegarde booléen échoue-t-il à l'échelle ?

Un seul fichier en échec n'est pas le problème qu'un résultat booléen crée ; mille d'entre eux le sont. Lorsque SaveAs renvoie autre chose qu'un succès pour trois fichiers sur dix mille, la question suivante est toujours la même : ces trois-là sont-ils réessayables, ou ont-ils besoin d'un humain ? Une erreur de permission sur un partage réseau n'est pas le même incident qu'une formule que le moteur de calcul ne peut pas évaluer, et ni l'un ni l'autre n'est identique à une feuille de calcul qui a silencieusement dépassé une limite de format. Avec seulement un résultat réussite/échec à disposition, chacun de ces cas devient un ticket de support identique, et quelqu'un doit ouvrir chaque fichier à la main, dans Excel, et le fixer jusqu'à ce que la cause devienne évidente. Ce triage manuel est le véritable coût d'une API booléenne, et il s'échelonne linéairement avec la taille du lot, ce qui est exactement la propriété que vous ne voulez pas d'une gestion d'erreurs

À l'intérieur d'IXLSWorkbookProgress : ce que porte un TXLSDiagnostic

IXLSWorkbookProgress est l'interface que HotXLS utilise pour rapporter à la fois comment une opération se déroule et ce qui s'est mal passé à l'intérieur, et les deux moitiés partagent un seul contrat pour une raison : les deux sont des choses qu'un appel de longue durée à Open, SaveAs, ou Recalculate a besoin de communiquer sans lever d'exception en cours d'opération. La moitié progression est OnProgress et OnProgressEx, se déclenchant avec une phase, un état, et une paire actuel/total. La moitié diagnostics est celle dont traite cet article : une propriété Diagnostics qui renvoie une liste TXLSDiagnostics, un raccourci LastDiagnostic pour l'entrée la plus récente, et un événement OnDiagnostic qui se déclenche au moment où chaque enregistrement TXLSDiagnostic est créé. Chaque enregistrement porte un Code numérique, une TXLSDiagnosticSeverity, la TXLSDiagnosticOperation qui l'a produit, un Message lisible par un humain, un SheetIndex et un SheetName, et un NativeCode qui préserve quelle que soit la valeur de retour de niveau inférieur ayant déclenché l'entrée

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;

Lire Diagnostics comme ceci bat déjà un résultat booléen à lui seul, car Code et SheetName transforment un mystère en un fait spécifique et filtrable. L'enregistrement TXLSDiagnostic va plus loin que ce que cet exemple imprime : RecordId et StreamOffset existent pour l'expertise au niveau des octets à l'intérieur d'un flux BIFF, et PartName contient l'entrée zip OOXML, telle que xl/worksheets/sheet3.xml, d'où provient un problème. Un point qui mérite d'être connu avant de construire un outillage autour de ces champs : dans la version actuelle, aucun des points d'appel de diagnostic intégrés ne renseigne RecordId ou StreamOffset, si bien que les deux restent à leur valeur par défaut de constructeur de -1, signifiant « non applicable » plutôt que « zéro ». Traitez leur absence comme normale, pas comme un bogue dans votre gestionnaire

Deux moteurs, une forme, une différence discrète

HotXLS fournit deux moteurs derrière ce même modèle de rapport, une façade BIFF8 pour les fichiers .xls hérités et une façade OOXML pour .xlsx, et ils n'exposent pas IXLSWorkbookProgress de façon identique. TXLSWorkbook, le moteur .xls, implémente formellement IXLSWorkbookProgress, si bien qu'il peut être transmis partout où ce type d'interface est attendu. TXLSXWorkbook, le moteur .xlsx, expose les mêmes membres Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress, et OnProgressEx avec des noms et types identiques, mais comme une classe simple plutôt qu'une implémentation formelle de cette interface, si bien qu'il ne satisfera pas un paramètre IXLSWorkbookProgress à lui seul. En pratique, cela importe rarement, car la plupart du code travaille avec une seule classe de classeur concrète à la fois, mais cela signifie que vous ne pouvez pas écrire un seul assistant typé IXLSWorkbookProgress et lui transmettre l'objet classeur de l'un ou l'autre moteur de façon interchangeable. La seule différence de champ qui découle directement de la scission de format est PartName : seul le moteur XLSX le renseigne, car seul OOXML a des parties zip à nommer

Qu'est-ce qui fait qu'un code de diagnostic est sûr pour brancher dessus ?

Le champ Code est la seule partie d'un diagnostic qui mérite qu'on code en dur une comparaison contre elle ; Message ne l'est pas, car la prose est exactement le genre de chose qui se voit reformulée, retraduite, ou enrichie de plus de détails dans une version ultérieure sans que personne ne le traite comme un changement cassant. Les codes de diagnostic intégrés de HotXLS se lisent déjà comme s'ils avaient été conçus en gardant cette distinction à l'esprit : les codes liés à la sauvegarde vont de 1000 à 1005, les codes liés à l'ouverture se trouvent à 1100 et 1101, les codes liés au calcul à 1200 et 1201, et un code de format non pris en charge à 1300, avec des écarts laissés à l'intérieur de chaque bande plutôt que des codes se succédant consécutivement sur toutes. Cet espacement est ce qui permet à un éditeur d'ajouter un nouveau mode d'échec au moment de la sauvegarde à, disons, 1006 sans renuméroter les codes dont dépend déjà votre instruction switch, et cela mérite d'être vérifié dans n'importe quelle API de diagnostics avant de s'engager à correspondre sur un code en production, pas seulement celle-ci. Gardez une branche par défaut dans votre propre logique de répartition quelle que soit la stabilité apparente de la numérotation, car de nouveaux modes d'échec sont exactement ce qu'un analyseur ou un écrivain en évolution continue de découvrir. NativeCode et ExceptionClass se situent une couche en dessous de Code pour quand vous avez besoin d'escalader : NativeCode préserve la valeur de retour sous-jacente, un HRESULT provenant d'un appel de stockage structuré parmi elles, et ExceptionClass enregistre le type d'exception Delphi lorsqu'une exception était impliquée, ce qui suffit généralement pour ouvrir une demande de support précise sans joindre une trace de pile complète

La sévérité et l'opération décident de ce que votre code fait ensuite

La sévérité et l'opération sont ce qui transforme un diagnostic d'une ligne de journal en une décision d'aiguillage. TXLSDiagnosticSeverity comprend Info, Warning, Error, et Fatal, et TXLSDiagnosticOperation étiquette chaque entrée avec l'appel qui l'a produite : Open, Save, Calculate, ou Export. Les deux axes sont indépendants par conception : xlsDiagnosticUnhandledException est un code fixe unique qui se déclenche avec Operation réglé sur quel que soit l'appel qui l'a réellement levée, si bien que Code répond à ce qui s'est mal passé tandis qu'Operation répond séparément à où, plutôt que de nécessiter un code distinct pour une exception pendant l'ouverture par rapport à une pendant la sauvegarde. Cette composabilité est aussi ce qui rend l'aiguillage mécanique : journaliser un avertissement et continuer, une sauvegarde annulée via l'indicateur Aborted en est un exemple typique ; compter une erreur et garder le lot en cours d'exécution, une feuille de calcul qui a échoué à se sérialiser en est un exemple typique ; arrêter le lot sur une sévérité fatale, car ce niveau signifie qu'une exception non gérée a déjà déroulé l'appel et que continuer risque de travailler à partir d'un état à moitié mis à jour. Une mise en garde honnête : Info existe dans l'énumération comme valeur par défaut avec laquelle démarre un TXLSDiagnostic tout neuf, mais chaque point d'appel de diagnostic intégré à la version actuelle de HotXLS ne lève jamais que Warning, Error, ou Fatal ; Info est réservé pour un usage futur, pas quelque chose que le moteur émet aujourd'hui

// 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;

Câbler OnDiagnostic dans un pipeline par lot

Interroger Diagnostics après chaque appel fonctionne pour un seul fichier ; cela cesse de fonctionner une fois de retour à ce lot nocturne de dix mille fichiers, car Diagnostics est réinitialisée au début de chaque appel à Open, SaveAs, et Recalculate. Lisez-la après le troisième fichier dans une boucle et vous ne voyez que les diagnostics du troisième fichier ; ce que les deux premiers fichiers ont rapporté a déjà disparu. OnDiagnostic résout cela en transformant la collection en flux : abonnez-vous une fois avant que la boucle ne démarre, et le même gestionnaire se déclenche pour chaque fichier, dans l'ordre, avec le nom de fichier toujours dans la portée via un champ d'instance

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;

Ce que le callback coûte réellement

OnDiagnostic est bon marché pour une raison structurelle : il ne se déclenche que lorsque quelque chose ne va déjà pas, et ce cas est rare comparé au nombre de cellules, lignes, ou feuilles de calcul qu'un classeur contient. Contrastez cela avec OnProgress et OnProgressEx, qui rapportent une progression routinière et ont dû être conçus dès le départ en fonction de la fréquence d'appel. HotXLS déclenche la progression au niveau de la feuille une fois par feuille pendant Open et SaveAs, pas une fois par cellule ou par ligne, ce qui est ce qui garde la surcharge par appel faible même sur des classeurs comportant des millions de cellules ; Recalculate va plus loin et limite son propre événement de progression à environ tous les quatre pour cent du graphe de dépendances, si bien qu'un recalcul complet vous donne un battement de cœur au lieu d'inonder votre thread d'interface d'événements. Les diagnostics n'avaient besoin d'aucune de ces limitations, car le nombre d'événements est borné par le nombre de problèmes réels, pas par la taille du fichier

Le seul endroit où la performance dépend encore de vous est à l'intérieur du gestionnaire lui-même. OnDiagnostic se déclenche de façon synchrone, sur le thread exécutant Open, SaveAs, ou Recalculate, si bien qu'un gestionnaire qui bloque, une écriture synchrone vers un service de journalisation distant par exemple, devient partie intégrante du temps horloge de cet appel. Pour un seul fichier, c'est invisible. Multiplié sur un lot de dix mille fichiers, c'est la différence entre une tâche qui se termine pendant la nuit et une qui tourne encore à l'heure du déjeuner, donc mettez en tampon ce que le gestionnaire doit faire et videz-le de façon asynchrone plutôt que de faire la partie lente en ligne

Les diagnostics structurés sont les plus précieux exactement là où un résultat booléen est le plus faible, dans les flux de travail qui touchent de nombreux fichiers plutôt qu'un seul. Un pipeline d'audit et de conversion de classeurs en est l'exemple le plus clair : au lieu d'enregistrer un simple réussite/échec par fichier, joignez la liste Diagnostics de chaque fichier à son enregistrement d'audit, et le rapport vous dit non seulement ce qui a échoué mais pourquoi, ce qui est en grande partie ce que notre article sur la construction d'un atelier d'audit et de conversion de classeurs cherche à bien faire en premier lieu. Le même appariement de progression et de diagnostics a aussi sa place dans tout flux de travail qui a déjà besoin d'un rapport de progression pour lui-même, ce qui est exactement le territoire couvert dans notre guide sur la performance des grands classeurs dans HotXLS, où un long appel à Open ou SaveAs est assez courant pour qu'OnProgress soit déjà câblé et qu'OnDiagnostic soit un ajout naturel et presque gratuit à ses côtés

Rien de tout cela ne nécessite qu'Excel soit installé quelque part dans le pipeline, et rien de tout cela ne nécessite d'intercepter une exception générique et de deviner ce qu'elle signifiait. IXLSWorkbookProgress et ses membres Diagnostics, LastDiagnostic, et OnDiagnostic font partie du composant HotXLS standard pour Delphi et C++Builder, aux côtés de la référence complète des codes de diagnostic et du reste de la surface Open, SaveAs, et Recalculate que cet article a parcourue