Artykuł techniczny

Strukturalna diagnostyka zamiast wyników logicznych w HotXLS

Uruchom konwersję wsadową na dziesięciu tysiącach arkuszy kalkulacyjnych przez noc, a rano trzy z nich wrócą jako False. To cały raport końcowy, jaki daje logiczny wynik zapisu: liczba niepowodzeń, bez żadnej informacji, którego pliku, którego arkusza i która z kilkunastu możliwych przyczyn była odpowiedzialna. HotXLS, natywny komponent losLab dla Delphi i C++Builder do obsługi plików Excel, zastępuje ten pojedynczy bit strukturalną diagnostyką. Interfejs IXLSWorkbookProgress udostępnia listę Diagnostics oraz zdarzenie OnDiagnostic, które raportują stabilny kod liczbowy, poziom ważności, operację, która zawiodła, oraz arkusz, w którym to się stało, dla każdego wywołania Open, SaveAs i Recalculate

Dlaczego logiczny wynik zapisu zawodzi w dużej skali?

Jeden nieudany plik nie jest problemem, który tworzy logiczny wynik; tysiąc takich plików już jest. Gdy SaveAs zwraca coś innego niż sukces dla trzech plików na dziesięć tysięcy, kolejne pytanie zawsze brzmi tak samo: czy te trzy nadają się do ponowienia, czy potrzebują interwencji człowieka? Błąd uprawnień na udziale sieciowym to nie ten sam incydent, co formuła, której silnik obliczeniowy nie potrafi obliczyć, i żaden z nich nie jest tym samym, co arkusz, który po cichu przekroczył limit formatu. Mając do dyspozycji jedynie wynik zdał/nie zdał, każdy z tych przypadków staje się identycznym zgłoszeniem do wsparcia technicznego, a ktoś musi ręcznie otworzyć każdy plik w Excelu i wpatrywać się w niego, dopóki przyczyna nie stanie się oczywista. Ta ręczna segregacja to prawdziwy koszt logicznego API, i skaluje się on liniowo wraz z rozmiarem partii — a to dokładnie ta właściwość, której nie chcemy w obsłudze błędów

Wnętrze IXLSWorkbookProgress: co niesie ze sobą TXLSDiagnostic

IXLSWorkbookProgress to interfejs, którego HotXLS używa do raportowania zarówno przebiegu operacji, jak i tego, co poszło w niej nie tak, a te dwie połówki dzielą jeden kontrakt nie bez powodu: obie są rzeczami, które długo trwające wywołanie Open, SaveAs lub Recalculate musi zakomunikować bez zgłaszania wyjątku w trakcie operacji. Połowa dotycząca postępu to OnProgress i OnProgressEx, uruchamiane z fazą, stanem oraz parą bieżący/łączny. Połowa dotycząca diagnostyki to ta, o której jest ten artykuł: właściwość Diagnostics zwracająca listę TXLSDiagnostics, skrót LastDiagnostic do ostatniego wpisu oraz zdarzenie OnDiagnostic, uruchamiane w chwili utworzenia każdego rekordu TXLSDiagnostic. Każdy rekord niesie liczbowy Code, TXLSDiagnosticSeverity, TXLSDiagnosticOperation, która go wygenerowała, czytelny dla człowieka Message, SheetIndex i SheetName, a także NativeCode, który zachowuje jakąkolwiek niżej-poziomową wartość zwrotną, która wywołała ten wpis

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;

Odczytywanie Diagnostics w ten sposób samo w sobie bije logiczny wynik, ponieważ Code i SheetName zamieniają zagadkę w konkretny, filtrowalny fakt. Rekord TXLSDiagnostic sięga dalej niż to, co drukuje ten przykład: RecordId i StreamOffset istnieją do analizy kryminalistycznej na poziomie bajtów wewnątrz strumienia BIFF, a PartName przechowuje wpis zip OOXML, taki jak xl/worksheets/sheet3.xml, z którego pochodził problem. Warto wiedzieć, zanim zbudujesz wokół nich narzędzia: w bieżącym wydaniu żadne z wbudowanych miejsc zgłaszania diagnostyki nie wypełnia RecordId ani StreamOffset, więc oba pozostają przy domyślnej wartości konstruktora -1, co oznacza „nie dotyczy", a nie „zero". Traktuj ich brak jako normę, nie jako błąd w swoim handlerze

Dwa silniki, jeden kształt, jedna cicha różnica

HotXLS dostarcza dwa silniki za tym samym modelem raportowania — fasadę BIFF8 dla starszych plików .xls oraz fasadę OOXML dla .xlsx — i nie udostępniają one IXLSWorkbookProgress identycznie. TXLSWorkbook, silnik .xls, formalnie implementuje IXLSWorkbookProgress, więc można go przekazać wszędzie tam, gdzie oczekiwany jest ten typ interfejsu. TXLSXWorkbook, silnik .xlsx, udostępnia te same składowe Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress i OnProgressEx o identycznych nazwach i typach, ale jako zwykła klasa, a nie formalna implementacja tego interfejsu, więc sam w sobie nie spełni wymogu parametru typu IXLSWorkbookProgress. W praktyce rzadko ma to znaczenie, ponieważ większość kodu pracuje na jednej konkretnej klasie skoroszytu naraz, ale oznacza to, że nie można napisać jednej funkcji pomocniczej typowanej na IXLSWorkbookProgress i przekazywać jej zamiennie obiekt skoroszytu z dowolnego silnika. Jedyna różnica pól wynikająca bezpośrednio z podziału formatów to PartName: wypełnia go tylko silnik XLSX, ponieważ tylko OOXML ma części zip do nazwania

Co sprawia, że kod diagnostyczny nadaje się do bezpiecznego rozgałęziania?

Pole Code to jedyna część diagnostyki, warta zaszycia w porównaniu na stałe w kodzie; Message już nie, ponieważ tekst opisowy to dokładnie ten rodzaj treści, który bywa przeredagowywany, tłumaczony na nowo lub rozszerzany o dalsze szczegóły w kolejnym wydaniu, bez traktowania tego jako zmiany łamiącej kompatybilność. Wbudowane kody diagnostyczne HotXLS już wyglądają, jakby zaprojektowano je z myślą o tym rozróżnieniu: kody związane z zapisem obejmują 1000–1005, kody związane z otwieraniem znajdują się na 1100 i 1101, kody związane z obliczeniami na 1200 i 1201, a kod nieobsługiwanego formatu na 1300, z lukami pozostawionymi wewnątrz każdego pasma, zamiast kodów idących kolejno przez wszystkie z nich. Ten odstęp pozwala dostawcy dodać nowy tryb awarii przy zapisie, powiedzmy, pod numerem 1006, bez konieczności przenumerowania kodów, na których opiera się już twoja instrukcja switch — i warto sprawdzić, czy tak jest w każdym API diagnostycznym, zanim zdecydujesz się dopasowywać po kodzie w produkcji, nie tylko w tym jednym przypadku. Zachowaj gałąź domyślną w swojej logice obsługi bez względu na to, jak stabilne wygląda numerowanie, ponieważ nowe tryby awarii to dokładnie to, co rozwijający się parser lub writer wciąż odkrywa. NativeCode i ExceptionClass znajdują się jedną warstwę poniżej Code na wypadek, gdy trzeba eskalować: NativeCode zachowuje bazową wartość zwrotną, między innymi HRESULT z wywołania Structured Storage, a ExceptionClass zapisuje typ wyjątku Delphi, gdy taki wystąpił, co zwykle wystarcza, aby otworzyć precyzyjne zgłoszenie do wsparcia bez dołączania pełnego śladu stosu

Ważność i operacja decydują o tym, co robi dalej twój kod

Ważność i operacja to elementy, które zamieniają diagnostykę z linii logu w decyzję o routingu. TXLSDiagnosticSeverity przyjmuje wartości Info, Warning, Error i Fatal, a TXLSDiagnosticOperation etykietuje każdy wpis wywołaniem, które go wygenerowało: Open, Save, Calculate lub Export. Te dwie osie są zaprojektowane jako niezależne: xlsDiagnosticUnhandledException to jeden stały kod, który uruchamia się z Operation ustawionym na to wywołanie, które faktycznie go zgłosiło, więc Code odpowiada na pytanie, co poszło nie tak, a Operation oddzielnie odpowiada, gdzie, zamiast wymagać osobnego kodu na wyjątek podczas otwierania a osobnego podczas zapisu. Ta kompozycyjność sprawia też, że routing staje się mechaniczny: zaloguj ostrzeżenie i idź dalej — typowym przykładem jest zapis anulowany przez flagę Aborted; policz błąd i kontynuuj partię — typowym przykładem jest arkusz, którego nie udało się zserializować; zatrzymaj partię przy ważności fatal, ponieważ ten poziom oznacza, że nieobsłużony wyjątek już rozwinął wywołanie, a kontynuowanie ryzykuje pracę na częściowo zaktualizowanym stanie. Jedno szczere zastrzeżenie: Info istnieje w wyliczeniu jako wartość domyślna, od której zaczyna świeży TXLSDiagnostic, ale każde miejsce zgłaszania diagnostyki wbudowane w dzisiejsze wydanie HotXLS zgłasza wyłącznie Warning, Error lub Fatal; Info jest zarezerwowane na przyszłość, a nie coś, co silnik emituje obecnie

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

Podłączanie OnDiagnostic do potoku wsadowego

Odpytywanie Diagnostics po każdym wywołaniu działa dla pojedynczego pliku; przestaje działać, gdy wracamy do tej nocnej partii dziesięciu tysięcy plików, ponieważ Diagnostics jest czyszczone na początku każdego wywołania Open, SaveAs i Recalculate. Odczytaj ją po trzecim pliku w pętli, a zobaczysz tylko diagnostykę trzeciego pliku; cokolwiek zgłosiły pierwsze dwa pliki, już zniknęło. OnDiagnostic rozwiązuje to, zamieniając kolekcję w strumień: zasubskrybuj raz przed rozpoczęciem pętli, a ten sam handler uruchomi się dla każdego pliku, po kolei, z nazwą pliku wciąż dostępną przez pole instancji

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;

Ile faktycznie kosztuje ten callback

OnDiagnostic jest tani ze strukturalnego powodu: uruchamia się tylko wtedy, gdy coś już jest nie tak, a to, co nie tak, jest rzadkością w porównaniu z liczbą komórek, wierszy czy arkuszy, które zawiera skoroszyt. Zestaw to z OnProgress i OnProgressEx, które raportują rutynowy postęp i musiały zostać zaprojektowane od początku z myślą o częstotliwości wywołań. HotXLS uruchamia postęp na poziomie arkusza raz na arkusz podczas Open i SaveAs, a nie raz na komórkę czy wiersz, co utrzymuje niski narzut na wywołanie nawet dla skoroszytów z milionami komórek; Recalculate idzie dalej i ogranicza własne zdarzenie postępu do mniej więcej co cztery procent grafu zależności, więc pełne przeliczenie daje ci sygnał życia zamiast zalewać wątek interfejsu użytkownika zdarzeniami. Diagnostyka nie potrzebowała żadnego z tych ograniczeń, ponieważ liczba zdarzeń jest ograniczona liczbą rzeczywistych problemów, a nie rozmiarem pliku

Jedynym miejscem, w którym wydajność wciąż zależy od ciebie, jest sam handler. OnDiagnostic uruchamia się synchronicznie, w wątku wykonującym Open, SaveAs lub Recalculate, więc handler, który blokuje — na przykład synchroniczny zapis do zdalnej usługi logowania — staje się częścią czasu trwania tego wywołania. Dla pojedynczego pliku jest to niewidoczne. Pomnożone przez partię dziesięciu tysięcy plików to różnica między zadaniem, które kończy się przez noc, a takim, które wciąż działa w porze lunchu, więc buforuj to, co handler musi zrobić, i opróżniaj bufor asynchronicznie, zamiast wykonywać wolną część w linii

Strukturalna diagnostyka jest najcenniejsza dokładnie tam, gdzie logiczny wynik jest najsłabszy — w przepływach pracy dotykających wielu plików zamiast jednego. Potok audytu i konwersji skoroszytów to najbardziej wyraźny przykład: zamiast zapisywać goły wynik zdał/nie zdał dla każdego pliku, dołącz listę Diagnostics danego pliku do jego rekordu audytu, a raport powie ci nie tylko, co zawiodło, ale i dlaczego — co jest w dużej mierze tym, co nasz artykuł o budowie warsztatu audytu i konwersji skoroszytów stara się osiągnąć od samego początku. To samo połączenie postępu i diagnostyki należy również do każdego przepływu pracy, który i tak już potrzebuje raportowania postępu ze swojego własnego powodu, co jest dokładnie tym terytorium, które obejmuje nasz przewodnik po wydajności dużych skoroszytów w HotXLS, gdzie długie wywołanie Open lub SaveAs jest na tyle powszechne, że OnProgress jest już podłączone, a OnDiagnostic to naturalny, niemal darmowy dodatek obok niego

Nic z tego nie wymaga zainstalowanego Excela nigdzie w potoku ani łapania ogólnego wyjątku i zgadywania, co miał na myśli. IXLSWorkbookProgress oraz jego składowe Diagnostics, LastDiagnostic i OnDiagnostic są częścią standardowego komponentu HotXLS dla Delphi i C++Buildera, obok pełnego odniesienia do kodów diagnostycznych i reszty powierzchni Open, SaveAs i Recalculate, którą przechodził ten artykuł