מאמר טכני

כתיבת זרימה ב-HotXLS לעבודות אצווה בצד שרת ב-Delphi

נניח ששירות Delphi לילי מייצר XLSX אחד לכל לקוח, כמה מאות קבצים, חלקם ברוחב 400,000 שורות. תכּנו את הביצועים והפתעה לעתים רחוקות היא לולאת מילוי-התאים. זוהי הקריאה ל-SaveAs. עם הכותב שבברירת מחדל, כל גיליון עבודה מסורסר אל מחרוזת XML יחידה שבזיכרון לפני שהמחרוזת הזו נדחסת אל ה-zip של OOXML, ועבור גיליון רחב המחרוזת הזמנית יכולה להאפיל על מודל התאים שממנו נבנתה. כך שעבודה שבונה בנוחות את נתוניה ויושבת על 800 MB תזנק מעבר למגבלת מכל של 2 GB במהלך השמירה, וקוטל ה-OOM מגיש את דוח הבאג ב-03:00 כשאיש אינו צופה. ל-HotXLS, ספריית הגיליונות הילידית של losLab ל-Delphi ול-C++Builder, יש מאפיין המכוון ישירות אל אותה זינוק: StreamingWrite. סביבו יושבות שתי ידיות נוספות שמכריעות אם עובד אצווה נשאר בתוך תקציב הזיכרון והזמן שלו, היינו התקשרויות-חזרה לכתיבה ברמת שורה והאופן שבו מאגר הסגנונות מתנהג בתוך לולאה צפופה

מה מסלול השמירה שבברירת מחדל מאגֵר, ומה StreamingWrite משנה

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

הגדרת Book.StreamingWrite := True מחליפה את SaveAs לכותב גיליון עבודה שפולט XML של גיליון ישירות אל זרם ה-zip תוך כדי יצירתו. מחרוזת הביניים לעולם אינה מוקצית, והזינוק המשולשי משתטח אל תוך הרעש

היו מדויקים לגבי מה זה באמת קונה לכם, משום שמכירת-יתר שלו מובילה לתוכניות קיבולת שגויות. הדגל משנה רק את מסלול השמירה. בניית חוברת העבודה עדיין מקצה את מודל התאים המלא שבזיכרון, ולכן הרמה במהלך שלב המילוי גבוהה בדיוק כמו קודם. מה שנעלם הוא זינוק הסריאליזציה שנהג להיערם מעל לרמה הזו בזמן השמירה, ועבור עבודה הממלאת 400 אלף שורות הזינוק הזה הוא דרך קבע כל ההבדל בין כניסה לתקציב זיכרון לבין פיצוצו. המאפיין ברירת מחדל ל-False כדי לשמר את ההתנהגות ההיסטורית, ולכן בחירה פנימה היא שורה מפורשת אחת שאתם כותבים בכוונה

ייצוא גורף עם הדגל מופעל

Book := TXLSXWorkbook.Create;
try
  BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // אינדקס מאגר, מבוסס-0
  Sheet := Book.Sheets.Add('Bulk');
  for R := 1 to 100000 do
  begin
    Sheet.Cells[R, 1].Value := R;
    Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
    Sheet.Cells[R, 3].Value := R * 1.5;
    if (R mod 1000) = 0 then
      Sheet.Cells[R, 2].FontIndex := BoldIdx + 1;        // מבוסס-1 בתא
  end;
  Book.StreamingWrite := True;   // הזרם XML של גיליון ישירות אל ה-zip
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Cells[R, C] יוצר תאים לפי דרישה, מה ששומר על גוף הלולאה נקי. שתי תקרות רשת ששווה להפנים: 1,048,576 שורות ו-16,384 עמודות, חשופות כ-XlsxMaxRow ו-XlsxMaxCol. הזנת נתונים שחורגת מתקרת השורות חייבת להתפצל על פני גיליונות בקוד שלכם. שום דבר במורד הזרם אינו מבחין בחריגה או מתקן אותה עבורכם, והקובץ פשוט נקטע במגבלה

מילוי שורות ללא תקורת Variant לכל תא

כל הקצאת Cells[R, C].Value משלמת על חיפוש תא והמרת Variant. בעשרת אלפים שורות איש אינו מבחין. במיליון שורות של עשרים עמודות כל אחת, התקורה הזו לכל קריאה הופכת לעלות השלטת של שלב המילוי, והמתכּנן יצביע ישר עליה. ממשקי האצווה מאפשרים לכם למסור לכותב שורה שלמה בכל פעם במקום. WriteRows מניע התקשרות-חזרה שמספקת שורה אחת לכל הפעלה:

procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
  LastCol: Integer; var Values: Variant; var Skip: Boolean;
  var Cancel: Boolean);
begin
  if not FReader.Next then
  begin
    Cancel := True;              // מקור הנתונים התרוקן: עצור בנקיון
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// מלא שורות 2..100001, עמודות A..C, במשיכה מהקורא
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

דגל ה-Cancel הוא מה שהופך טווח שורות קבוע ל"עד N שורות", שזו הצורה הטבעית כשספירת השורות מגיעה משאילתה שלא סיימתם להריץ. Skip הוא הנגיעה הקלה יותר: הוא משאיר שורה בודדת ריקה מבלי לעצור את ההרצה. מעבר למילוי תאים, ההתקשרות-חזרה מתבררת כבית טוב לדאגות התפעוליות שאחרת מוברגות אל לולאת מילוי בדרכים מסורבלות. מונה התקדמות שמתקתק כל אלף שורות, אסימון ביטול שנדגם ממתזמן העבודות, מגביל קצב על קריאות ממסד הנתונים המקורי: כל זה חי במקום אחד במקום להיות שזור דרך קוד כתיבת-תאים. בצד הקריאה, ForEachRow ו-ForEachCell משקפים את אותו דפוס, מה שחשוב כשעבודת אצווה גם צורכת וגם מפיקה קבצים גדולים

מאגרי סגנונות מתגמלים הוצאה החוצה

מודל העיצוב של XLSX הוא קבוצה של מאגרים משותפים. Fonts.Add, Fills.AddSolid ו-Borders.Add כולם מחזירים אינדקס מאגר מבוסס-0, ותא מפנה לגופן על ידי אחסון אותו אינדקס בתוספת אחד ב-FontIndex, היכן שאפס שמור לברירת המחדל של חוברת העבודה. ה-+1 נמצא שם בדוגמה הגורפת למעלה. שכחו אותו והתא קולט בשקט את הסגנון השגוי, משום שסטייה-באחד באינדקס מאגר סגנונות עדיין היא אינדקס תקף ושום דבר אינו מעלה

המשמעת שנובעת היא ליצור כל אובייקט סגנון לפני לולאת השורות ולהפנות לאינדקס שלו בתוך הלולאה. Fonts.Add מבטל כפילות של הגדרות זהות, ולכן קריאה לו פעם בכל שורה רק מבזבזת CPU. Alignments.Add היא המלכודת, משום שהוא מחזיר רשומה חדשה בכל קריאה. בתוך לולאה בת 100 אלף שורות זה קובר את styles.xml תחת מאה אלף רשומות יישור כפולות, מה שמנפח את הקובץ על הדיסק ומאט כל פתיחה מאוחרת ב-Excel ככל שהכפילויות מנותחות מחדש. בנו כל סגנון פעם אחת מחוץ ללולאה, ואז הפנו לאינדקס שלו כמה פעמים שאתם צריכים

זרמים, תיקיות זמני, ולולאת האצווה סביב כל זה

שום דבר מזה אינו דורש מערכת קבצים. שתי החזיתות נושאות העמסות TStream על פני משטח ה-IO שלהן, Open ו-SaveAs ו-SaveAsCSV ו-SaveAsHTML ו-SaveAsODS ביניהם, ולכן עובד אצווה יכול לרנדר ישירות אל TMemoryStream המיועד לאחסון blob או לתגובת HTTP מבלי לגעת אי-פעם בדיסק. יש קצה חד אחד לזכור. SaveAs(Stream) כותב מהמיקום הנוכחי של הזרם ואינו מגלגל לאחור אחר כך, ולכן הגדירו Position := 0 בעצמכם לפני מסירת הזרם למה שמספק אותו, אחרת הצרכן קורא אפס בתים. חזית ה-XLS מוסיפה שני כפתורים משלה. SetTempDir מצביע את קובצי הזמני של כותב ה-BIFF אל אמצעי אחסון שיש לו את המקום ואת מרווח ה-IO לספוג אותם, מה שחשוב בשרתים שבהם נתיב הזמני שבברירת מחדל יושב על דיסק מערכת צפוף. UseSharedFormulas מקפל גופי נוסחה חוזרים אל קבוצות משותפות, צמצום גודל אמיתי לצורת הדוח הקלאסית שבה נוסחה אחת מועתקת לאורך עמודה שלמה

לולאת האצווה עצמה נשארת משעממת בכוונה:

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // מופע טרי: ללא דליפת מצב
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // קלט פגום אחד אסור לו להרוג את האצווה
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

מופע חוברת עבודה טרי לכל קובץ עולה מיקרו-שניות ומסיר קטגוריה שלמה של באג זיהום חוצה-קבצים: סגנונות, שמות מוגדרים ומאפייני מסמך מקובץ 17 אין להם נתיב לדלוף אל קובץ 18. הדילוג-והמשך בכישלון Open מצדיק את מקומו באותה מידה, משום שהעלאה קטועה אחת באצווה בת 600 קבצים צריכה לעלות לכם שורת רישום יחידה ולא את שאר ההרצה. שווה לסמן גם מה שרגל ה-CSV בכוונה אינה עושה. SaveAsCSV כותב נוסחאות כטקסט מילולי ולעולם אינו מעריך אותן, ולכן אצוות המרה שצרכניה מצפים למספרים מחושבים חייבת להריץ Calculate על התאים הרלוונטיים תחילה, או להתחיל מחוברות עבודה שכבר נושאות תוצאות במטמון מחישוב קודם

מודל המקביליות: חוברת עבודה אחת לכל תהליכון

אובייקטי אף אחת מהחזיתות אינם בטוחים-לתהליכונים, והתכנון מעולם לא העמיד פנים אחרת. משום שאין מצב גלובלי משותף בין מופעים, כלל קנה-המידה הוא פשוט חוברת עבודה אחת לכל תהליכון עובד, ללא שיתוף חוברת עבודה בין תהליכונים. בריכה של N עובדים, כל אחד מחזיק TXLSXWorkbook משלו, מתרחבת קרוב לליניארית עד שהזיכרון הופך לתקרה, ועל התקרה הזו אתם יכולים לשים מספר: מודל התאים המקבילי הגדול ביותר כפול ספירת העובדים, בתוספת כל תקורת זמן-שמירה ש-StreamingWrite השטיח. כשהתור רץ עמוק, החילו לחץ-נגדי בתור העבודות במקום בתוך הכותב. תהליכון מורעב שכתב חצי חוברת עבודה לא הפיק דבר שימושי, בעוד עבודה שהמתינה כמה שניות לעובד פנוי מסתיימת שלמה

לתמונת הכיוונון הרחבה יותר, כולל נוסחאות משותפות, דילוג גרפיקה בצד הקריאה והידיות הספציפיות ל-XLS, ראו את מדריך ביצועי חוברת העבודה הגדולה. עבודות אצווה שהשורות שלהן מגיעות ישירות משאילתה מכוסות בנפרד בדפוסי ייצוא מסד הנתונים לדוחות Delphi

HotXLS מהדרת אל תוך שירות ה-Delphi או ה-C++Builder שלכם כ-Object Pascal יליד ללא תלויות חיצוניות; מהדורות ורישוי נמצאים בעמוד המוצר של HotXLS Component