מאמר טכני

אבחון מובנה במקום תוצאות בוליאניות ב-HotXLS

הרץ המרת אצווה על עשרת אלפים גיליונות אלקטרוניים במהלך הלילה, ועד הבוקר שלושה מהם חוזרים False. זו כל הבדיקה-בדיעבד (postmortem) שתוצאת שמירה בוליאנית נותנת לך: ספירת כשלונות, ללא שום דבר על איזה קובץ, איזה גיליון, או איזו מתריסר סיבות אפשריות הייתה אחראית. HotXLS, הרכיב הילידי של losLab עבור Delphi ו-C++Builder לקובצי Excel, מחליפה את הביט הבודד הזה באבחון מובנה. ‏הממשק IXLSWorkbookProgress חושף רשימת Diagnostics ואירוע OnDiagnostic שמדווחים קוד מספרי יציב, רמת חומרה, הפעולה שנכשלה, והגיליון שבו זה קרה, עבור כל קריאת Open, ‏SaveAs, ו-Recalculate

למה תוצאת שמירה בוליאנית נכשלת בקנה מידה?

קובץ בודד שנכשל אינו הבעיה שתוצאה בוליאנית יוצרת; אלף כאלה כן. כאשר SaveAs מחזירה משהו שאינו הצלחה עבור שלושה קבצים מתוך עשרת אלפים, השאלה הבאה תמיד זהה: האם השלושה האלה ניתנים-לניסיון-חוזר, או שהם זקוקים לבן-אדם? שגיאת הרשאה בשיתוף רשת אינה אותו אירוע כמו נוסחה שמנוע החישוב לא יכול להעריך, וגם זה לא זהה לגיליון עבודה שחרג בשקט ממגבלת פורמט. עם רק תוצאת עבר/נכשל לעבוד איתה, כל אחד מאלה הופך לכרטיס תמיכה זהה, ומישהו חייב לפתוח כל קובץ ביד, ב-Excel, ולבהות בו עד שהסיבה הופכת ברורה. המיון הידני הזה הוא העלות האמיתית של API בוליאני, והוא גדל ליניארית עם גודל האצווה, שזו בדיוק התכונה שאתה לא רוצה מטיפול שגיאות

בתוך IXLSWorkbookProgress: מה TXLSDiagnostic נושא

‏IXLSWorkbookProgress הוא הממשק ש-HotXLS משתמשת בו כדי לדווח גם איך פעולה מתקדמת וגם מה השתבש בתוכה, ושני החצאים חולקים חוזה אחד מסיבה: שניהם דברים שקריאת Open, ‏SaveAs, או Recalculate ארוכת-ריצה זקוקה לתקשר בלי להעלות חריגה באמצע-פעולה. חצי ההתקדמות הוא OnProgress ו-OnProgressEx, מופעל עם שלב, מצב, וזוג נוכחי/סה"כ. חצי האבחון הוא זה שהמאמר הזה עוסק בו: מאפיין Diagnostics שמחזיר רשימת TXLSDiagnostics, קיצור LastDiagnostic עבור הרשומה האחרונה, ואירוע OnDiagnostic שמופעל ברגע שכל רשומת TXLSDiagnostic נוצרת. כל רשומה נושאת Code מספרי, ‏TXLSDiagnosticSeverity, ‏TXLSDiagnosticOperation שייצרה אותה, ‏Message קריא-לאדם, ‏SheetIndex ו-SheetName, ו-NativeCode ששומר על כל ערך החזרה ברמה-נמוכה יותר שהפעיל את הרשומה

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;

קריאת Diagnostics כך כבר מנצחת תוצאה בוליאנית בזכות עצמה, משום ש-Code ו-SheetName הופכים תעלומה לעובדה ספציפית, ניתנת-לסינון. רשומת TXLSDiagnostic מגיעה רחוק יותר ממה שהדוגמה הזו מדפיסה: RecordId ו-StreamOffset קיימים עבור פורנזיקה ברמת-בייט בתוך זרם BIFF, ו-PartName מחזיק את רשומת ה-zip של OOXML, כמו xl/worksheets/sheet3.xml, שבעיה הגיעה ממנה. שווה לדעת לפני שאתה בונה כלים סביבם: בגרסה הנוכחית אף אחת מנקודות קריאת האבחון המובנות לא ממלאת את RecordId או StreamOffset, כך ששניהם נשארים בברירת המחדל של הבנאי שלהם, ‏-1, כלומר "לא רלוונטי" ולא "אפס". התייחס להיעדרם כרגיל, לא כבאג ב-handler שלך

שני מנועים, צורה אחת, הבדל שקט אחד

HotXLS שולחת שני מנועים מאחורי אותו מודל דיווח, חזית BIFF8 עבור קובצי .xls מדור-קודם וחזית OOXML עבור .xlsx, והם לא חושפים את IXLSWorkbookProgress באופן זהה. ‏TXLSWorkbook, מנוע ה-.xls, מיישם באופן פורמלי את IXLSWorkbookProgress, כך שאפשר להעביר אותו לכל מקום שסוג הממשק הזה מצופה. ‏TXLSXWorkbook, מנוע ה-.xlsx, חושף את אותם חברי Diagnostics, ‏LastDiagnostic, ‏OnDiagnostic, ‏OnProgress, ו-OnProgressEx עם שמות וסוגים זהים, אבל כמחלקה פשוטה ולא כיישום פורמלי של הממשק הזה, כך שהיא לא תספק פרמטר IXLSWorkbookProgress בעצמה. בפועל זה לעיתים רחוקות משנה, משום שרוב הקוד עובד מול מחלקת חוברת-עבודה קונקרטית אחת בכל פעם, אבל זה כן אומר שאתה לא יכול לכתוב עוזר יחיד שמוקלד ל-IXLSWorkbookProgress ולמסור לו את אובייקט חוברת העבודה של אחד המנועים לחילופין. הבדל השדה היחיד שנובע ישירות מהפיצול הפורמט הוא PartName: רק מנוע ה-XLSX ממלא אותו, משום שרק ל-OOXML יש חלקי zip לכנות

מה הופך קוד אבחון למשהו שבטוח להסתעף עליו?

שדה ה-Code הוא החלק היחיד באבחון ששווה לקודד-קשיח השוואה מולו; Message לא, משום שפרוזה היא בדיוק הסוג של דבר שמנוסח מחדש, מתורגם מחדש, או מורחב עם עוד פרטים בגרסה מאוחרת יותר בלי שאף אחד יתייחס לזה כשינוי-שובר. קודי האבחון המובנים של HotXLS כבר נקראים כאילו עוצבו עם ההבחנה הזו בראש: קודים הקשורים-לשמירה רצים 1000 עד 1005, קודים הקשורים-לפתיחה יושבים ב-1100 ו-1101, קודים הקשורים-לחישוב ב-1200 ו-1201, וקוד פורמט-לא-נתמך ב-1300, עם פערים שהושארו בתוך כל להקה במקום שהקודים ירוצו ברצף על פני כולם. הריווח הזה הוא מה שמאפשר לספק להוסיף מצב-כשל חדש בזמן-שמירה, נניח, ב-1006 בלי למספר-מחדש את הקודים שהוראת ה-switch שלך כבר תלויה בהם, ושווה לבדוק אחריו בכל API אבחון לפני שאתה מתחייב להתאים על קוד בייצור, לא רק זה. שמור ענף ברירת-מחדל בלוגיקת השיגור שלך עצמך ללא קשר לכמה יציב המספור נראה, משום שמצבי כשל חדשים הם בדיוק מה שמפענח או כותב מתפתח ממשיך לגלות. ‏NativeCode ו-ExceptionClass יושבים שכבה אחת מתחת ל-Code עבור מתי שאתה צריך להסלים: NativeCode שומר על ערך ההחזרה הבסיסי, HRESULT מקריאת Structured Storage ביניהם, ו-ExceptionClass רושם את סוג חריגת Delphi כשאחת הייתה מעורבת, מה שבדרך כלל מספיק כדי לפתוח בקשת תמיכה מדויקת בלי לצרף stack trace מלא

חומרה ופעולה מחליטות מה הקוד שלך עושה הלאה

חומרה ופעולה הם מה שהופך אבחון משורת יומן להחלטת ניתוב. ‏TXLSDiagnosticSeverity רצה Info, ‏Warning, ‏Error, ו-Fatal, ו-TXLSDiagnosticOperation מתייגת כל רשומה עם הקריאה שייצרה אותה: Open, ‏Save, ‏Calculate, או Export. שני הצירים עצמאיים בעיצוב: xlsDiagnosticUnhandledException הוא קוד קבוע אחד שמופעל עם Operation מוגדר לכל קריאה שבפועל העלתה אותו, כך ש-Code עונה מה השתבש בעוד Operation בנפרד עונה איפה, במקום לזקוק לקוד נבדל עבור חריגה במהלך פתיחה לעומת אחת במהלך שמירה. ההרכבה הזו היא גם מה שהופך ניתוב למכני: רשום אזהרה והמשך, שמירה שבוטלה דרך הדגל Aborted היא דוגמה טיפוסית; ספור שגיאה והמשך להריץ את האצווה, גיליון עבודה שנכשל בסידור (serialize) הוא דוגמה טיפוסית; עצור את האצווה בחומרה קטלנית, משום שהרמה הזו אומרת שחריגה בלתי-מטופלת כבר פרמה את הקריאה והמשך מסתכן בעבודה ממצב חצי-מעודכן. הסתייגות כנה אחת: Info קיימת בטיפוס-המנייה כברירת המחדל ש-TXLSDiagnostic טרי מתחיל בה, אבל כל נקודת קריאת אבחון מובנית בגרסת HotXLS הנוכחית רק אי-פעם מעלה Warning, ‏Error, או Fatal; ‏Info שמור לשימוש עתידי, לא משהו שהמנוע פולט היום

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

חיווט OnDiagnostic לתוך צינור אצווה

סקירת Diagnostics אחרי כל קריאה עובדת עבור קובץ בודד; היא מפסיקה לעבוד ברגע שאתה חוזר לאותה אצוות-לילה של עשרת אלפים, משום ש-Diagnostics מנוקה בתחילת כל קריאת Open, ‏SaveAs, ו-Recalculate. קרא אותה אחרי הקובץ השלישי בלולאה ואתה רואה רק את האבחונים של הקובץ השלישי; מה ששני הקבצים הראשונים דיווחו כבר נעלם. ‏OnDiagnostic פותר את זה על ידי הפיכת האוסף לזרם: הירשם פעם אחת לפני שהלולאה מתחילה, ואותו handler מופעל עבור כל קובץ, בסדר, עם שם הקובץ עדיין בתחום דרך שדה מופע

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;

מה ה-callback באמת עולה

‏OnDiagnostic זול מסיבה מבנית: הוא מופעל רק כשמשהו כבר לא בסדר, ולא-בסדר נדיר בהשוואה למספר התאים, השורות, או גיליונות העבודה שחוברת עבודה מחזיקה. השווה את זה עם OnProgress ו-OnProgressEx, שמדווחים התקדמות שגרתית והיו צריכים להיות מעוצבים סביב תדירות-קריאה מההתחלה. HotXLS מפעילה התקדמות ברמת-גיליון-עבודה פעם אחת לגיליון במהלך Open ו-SaveAs, לא פעם אחת לתא או שורה, וזה מה ששומר את התקורה לכל-קריאה קטנה אפילו על חוברות עבודה עם מיליוני תאים; ‏Recalculate הולכת רחוק יותר וממתגת את אירוע ההתקדמות שלה עצמה בערך כל ארבעה אחוזים של גרף התלות, כך שחישוב מלא נותן לך פעימת-לב במקום להציף את תהליכון ה-UI שלך באירועים. אבחון לא זקק לשום ריסון (throttling) כזה, משום שספירת האירועים חסומה על ידי מספר הבעיות בפועל, לא על ידי גודל הקובץ

המקום היחיד שביצועים עדיין תלויים בך הוא בתוך ה-handler עצמו. ‏OnDiagnostic מופעל סינכרונית, על התהליכון שמריץ את Open, ‏SaveAs, או Recalculate, כך ש-handler שחוסם, כתיבה סינכרונית לשירות רישום מרוחק למשל, הופך לחלק מזמן-הקיר של הקריאה ההיא. עבור קובץ בודד זה בלתי-נראה. מוכפל על פני אצווה בת עשרת-אלפים-קבצים זה ההבדל בין עבודה שמסתיימת במהלך הלילה לבין אחת שעדיין רצה בזמן ארוחת הצהריים, כך שאגור מה שה-handler צריך לעשות ושטוף אותו אסינכרונית במקום לעשות את החלק האיטי inline

אבחון מובנה שווה ביותר בדיוק היכן שתוצאה בוליאנית הכי חלשה, בזרימות עבודה שנוגעות בהרבה קבצים ולא באחד. צינור ביקורת והמרת חוברות עבודה הוא הדוגמה הברורה ביותר: במקום לרשום עבר/נכשל ערום לכל קובץ, צרף את רשימת ה-Diagnostics של כל קובץ לרשומת הביקורת שלו, והדוח מספר לך לא רק מה נכשל אלא למה, שזה רוב מה שמאמרנו על בניית שולחן עבודה לביקורת והמרת חוברות עבודה מנסה לתפוס נכון מלכתחילה. אותו צימוד של התקדמות ואבחון גם שייך בכל זרימת עבודה שכבר זקוקה לדיווח התקדמות למען עצמו, שזה בדיוק הטריטוריה המכוסה בהמדריך שלנו לביצועי חוברת עבודה גדולה ב-HotXLS, שם קריאת Open או SaveAs ארוכה נפוצה מספיק ש-OnProgress כבר מחווטת ו-OnDiagnostic היא תוספת טבעית, כמעט חינמית, לצידה

שום דבר מזה לא דורש ש-Excel יהיה מותקן בשום מקום בצינור, ושום דבר מזה לא דורש לתפוס חריגה גנרית ולנחש מה היא התכוונה. ‏IXLSWorkbookProgress וחברי ה-Diagnostics, ‏LastDiagnostic, ו-OnDiagnostic שלו הם חלק מרכיב HotXLS הסטנדרטי עבור Delphi ו-C++Builder, לצד מסמך העזר המלא לקודי האבחון ושאר משטח ה-Open, ‏SaveAs, ו-Recalculate שהמאמר הזה עבר עליו