ลองรันการแปลงแบบ batch กับสเปรดชีตหนึ่งหมื่นไฟล์ข้ามคืน แล้วพอถึงเช้าสามไฟล์ในนั้นได้ผลลัพธ์ False กลับมา นั่นคือ postmortem ทั้งหมดที่ผลลัพธ์การบันทึกแบบ boolean ให้คุณได้ คือจำนวนความล้มเหลว โดยไม่มีอะไรบอกว่าไฟล์ไหน ชีตไหน หรือสาเหตุใดในสาเหตุนับสิบที่เป็นต้นเหตุ HotXLS คอมโพเนนต์เนทีฟของ losLab สำหรับไฟล์ Excel ใน Delphi และ C++Builder แทนที่บิตเดียวนั้นด้วยการวินิจฉัยแบบมีโครงสร้าง interface IXLSWorkbookProgress เปิดรายการ Diagnostics และ event OnDiagnostic ที่รายงานรหัสตัวเลขที่เสถียร ระดับความรุนแรง การดำเนินการที่ล้มเหลว และชีตที่มันเกิดขึ้น สำหรับทุกการเรียก Open, SaveAs และ Recalculate
ทำไมผลลัพธ์การบันทึกแบบ boolean ถึงล้มเหลวเมื่อขยายสเกล
ไฟล์ที่ล้มเหลวไฟล์เดียวไม่ใช่ปัญหาที่ผลลัพธ์แบบ boolean สร้างขึ้น พันไฟล์ต่างหากที่เป็นปัญหา เมื่อ SaveAs คืนค่าอะไรบางอย่างที่ไม่ใช่ความสำเร็จสำหรับสามไฟล์จากหนึ่งหมื่นไฟล์ คำถามถัดไปเป็นแบบเดิมเสมอ สามไฟล์นี้ retry ได้ไหม หรือต้องการคนมาดู ข้อผิดพลาดด้านสิทธิ์บน network share ไม่ใช่เหตุการณ์เดียวกับสูตรที่ calculation engine ประเมินไม่ได้ และทั้งสองก็ไม่ใช่เหตุการณ์เดียวกับเวิร์กชีตที่เกินขีดจำกัดฟอร์แมตอย่างเงียบๆ ด้วยผลลัพธ์ผ่าน/ไม่ผ่านเพียงอย่างเดียวให้ทำงานด้วย ทุกกรณีเหล่านั้นกลายเป็น support ticket ที่เหมือนกัน และต้องมีใครสักคนเปิดไฟล์แต่ละไฟล์ด้วยมือใน Excel แล้วจ้องมันจนกว่าสาเหตุจะชัดเจน การคัดแยกด้วยมือแบบนั้นคือต้นทุนจริงของ API แบบ boolean และมันขยายสเกลแบบเชิงเส้นตามขนาดของ batch ซึ่งเป็นคุณสมบัติที่คุณไม่ต้องการจาก error handling เลย
ภายใน IXLSWorkbookProgress: TXLSDiagnostic พกอะไรไว้บ้าง
IXLSWorkbookProgress คือ interface ที่ HotXLS ใช้รายงานทั้งความคืบหน้าของการดำเนินการและสิ่งที่ผิดพลาดภายในมัน และสองส่วนนี้ใช้สัญญาเดียวกันด้วยเหตุผล ทั้งคู่เป็นสิ่งที่การเรียก Open, SaveAs หรือ Recalculate ที่ใช้เวลานานต้องสื่อสารโดยไม่ยก exception กลางการทำงาน ครึ่งหนึ่งด้านความคืบหน้าคือ OnProgress และ OnProgressEx ซึ่งยิงด้วยเฟส สถานะ และคู่ current/total ครึ่งหนึ่งด้านการวินิจฉัยคือสิ่งที่บทความนี้พูดถึง property Diagnostics ที่คืนรายการ TXLSDiagnostics, ทางลัด LastDiagnostic สำหรับ entry ล่าสุด และ event OnDiagnostic ที่ยิงทันทีที่ record TXLSDiagnostic แต่ละตัวถูกสร้างขึ้น แต่ละ record พก Code แบบตัวเลข, TXLSDiagnosticSeverity, TXLSDiagnosticOperation ที่สร้างมัน, Message ที่มนุษย์อ่านได้, SheetIndex กับ SheetName และ NativeCode ที่เก็บค่าที่คืนกลับมาระดับล่างใดก็ตามที่กระตุ้น entry นั้น
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 แบบนี้ก็เอาชนะผลลัพธ์แบบ boolean ได้เองอยู่แล้ว เพราะ Code และ SheetName เปลี่ยนปริศนาให้เป็นข้อเท็จจริงที่เจาะจงและกรองได้ record TXLSDiagnostic ไปไกลกว่าสิ่งที่ตัวอย่างนี้พิมพ์ออกมา RecordId และ StreamOffset มีไว้สำหรับการสืบสวนระดับไบต์ภายใน BIFF stream และ PartName ถือ zip entry ของ OOXML เช่น xl/worksheets/sheet3.xml ที่ปัญหามาจาก สิ่งที่ควรรู้ก่อนที่คุณจะสร้างเครื่องมือรอบสิ่งเหล่านี้ ในรุ่นปัจจุบัน จุดเรียกการวินิจฉัยในตัวไม่มีตัวใดเลยที่ใส่ค่า RecordId หรือ StreamOffset ดังนั้นทั้งคู่จึงคงค่าเริ่มต้นของ constructor ไว้ที่ -1 หมายถึง "ไม่เกี่ยวข้อง" ไม่ใช่ "ศูนย์" ให้ถือว่าการไม่มีค่าของมันเป็นเรื่องปกติ ไม่ใช่บั๊กใน handler ของคุณ
สองเอนจิ้น รูปร่างเดียว ความแตกต่างเงียบๆ หนึ่งอย่าง
HotXLS มาพร้อมสองเอนจิ้นเบื้องหลังโมเดลการรายงานเดียวกันนี้ คือ facade แบบ BIFF8 สำหรับไฟล์ .xls รุ่นเก่า และ facade แบบ OOXML สำหรับ .xlsx และทั้งคู่ไม่ได้เปิด IXLSWorkbookProgress เหมือนกันเป๊ะ TXLSWorkbook เอนจิ้น .xls implement IXLSWorkbookProgress อย่างเป็นทางการ ดังนั้นจึงส่งผ่านไปยังที่ใดก็ตามที่คาดหวัง interface type นั้นได้ TXLSXWorkbook เอนจิ้น .xlsx เปิด member Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress และ OnProgressEx เดียวกัน ด้วยชื่อและ type เดียวกัน แต่ในฐานะคลาสธรรมดา ไม่ใช่การ implement interface นั้นอย่างเป็นทางการ ดังนั้นมันจะไม่ตอบสนองพารามิเตอร์ IXLSWorkbookProgress ด้วยตัวเองได้ ในทางปฏิบัติเรื่องนี้แทบไม่สำคัญ เพราะโค้ดส่วนใหญ่ทำงานกับคลาส workbook ที่เป็นรูปธรรมตัวเดียวในแต่ละครั้ง แต่มันหมายความว่าคุณไม่สามารถเขียน helper ตัวเดียวที่ type เป็น IXLSWorkbookProgress แล้วส่ง object workbook ของเอนจิ้นใดก็ได้แบบสลับกันได้ ความแตกต่างของฟิลด์หนึ่งที่ตามมาโดยตรงจากการแยกฟอร์แมตคือ PartName มีแค่เอนจิ้น XLSX เท่านั้นที่ใส่ค่าให้ เพราะมีแค่ OOXML เท่านั้นที่มี zip part ให้ตั้งชื่อ
อะไรทำให้รหัสวินิจฉัยเป็นสิ่งที่คุณ branch ได้อย่างปลอดภัย
ฟิลด์ Code เป็นส่วนเดียวของการวินิจฉัยที่ควรค่าแก่การ hardcode การเปรียบเทียบ Message ไม่ใช่ เพราะข้อความร้อยแก้วเป็นสิ่งที่จะถูกเรียบเรียงใหม่ แปลใหม่ หรือขยายรายละเอียดเพิ่มในรุ่นถัดไปโดยไม่มีใครถือว่ามันเป็น breaking change รหัสวินิจฉัยในตัวของ HotXLS อ่านดูเหมือนถูกออกแบบมาโดยคำนึงถึงความแตกต่างนั้นอยู่แล้ว รหัสที่เกี่ยวกับการบันทึกอยู่ในช่วง 1000 ถึง 1005 รหัสที่เกี่ยวกับการเปิดอยู่ที่ 1100 และ 1101 รหัสที่เกี่ยวกับการคำนวณที่ 1200 และ 1201 และรหัสฟอร์แมตที่ไม่รองรับที่ 1300 โดยเว้นช่องว่างไว้ภายในแต่ละแถบ แทนที่จะให้รหัสวิ่งต่อเนื่องกันข้ามทั้งหมด การเว้นระยะแบบนั้นคือสิ่งที่ทำให้ vendor เพิ่มโหมดความล้มเหลวเวลาบันทึกใหม่ได้ เช่น ที่ 1006 โดยไม่ต้องกำหนดหมายเลขรหัสที่ switch statement ของคุณพึ่งพาอยู่แล้วใหม่ และคุ้มค่าที่จะตรวจสอบสิ่งนี้ใน diagnostics API ใดๆ ก่อนที่คุณจะยึดมั่นกับการ match รหัสใน production ไม่ใช่แค่ตัวนี้ตัวเดียว เก็บ branch แบบ default ไว้ใน dispatch logic ของคุณเองเสมอ ไม่ว่าการกำหนดหมายเลขจะดูเสถียรแค่ไหนก็ตาม เพราะโหมดความล้มเหลวใหม่ๆ คือสิ่งที่ parser หรือ writer ที่กำลังพัฒนาต่อเนื่องยังคงค้นพบอยู่เรื่อยๆ NativeCode และ ExceptionClass อยู่ชั้นล่างของ Code หนึ่งชั้นสำหรับเวลาที่คุณต้อง escalate NativeCode เก็บค่าที่คืนกลับมาระดับล่างไว้ เช่น HRESULT จากการเรียก Structured Storage และ ExceptionClass บันทึกชนิด exception ของ Delphi เมื่อมีเกี่ยวข้อง ซึ่งปกติเพียงพอที่จะเปิด support request ที่แม่นยำได้โดยไม่ต้องแนบ stack trace เต็มรูปแบบ
ความรุนแรงและการดำเนินการตัดสินว่าโค้ดของคุณจะทำอะไรต่อไป
ความรุนแรงและการดำเนินการคือสิ่งที่เปลี่ยนการวินิจฉัยจากบรรทัด log ให้กลายเป็นการตัดสินใจด้านการจัดเส้นทาง TXLSDiagnosticSeverity มี Info, Warning, Error และ Fatal และ TXLSDiagnosticOperation ติดแท็กทุก entry ด้วยการเรียกที่สร้างมัน คือ Open, Save, Calculate หรือ Export สองแกนนี้เป็นอิสระต่อกันโดยการออกแบบ xlsDiagnosticUnhandledException เป็นรหัสคงที่หนึ่งตัวที่ยิงพร้อมกับ Operation ที่ตั้งเป็นการเรียกใดก็ตามที่ยกมันขึ้นมาจริงๆ ดังนั้น Code ตอบว่าอะไรผิดพลาด ในขณะที่ Operation ตอบแยกต่างหากว่ามันเกิดที่ไหน แทนที่จะต้องมีรหัสต่างกันสำหรับ exception ระหว่างเปิดกับระหว่างบันทึก ความสามารถในการประกอบกันนั้นก็เป็นสิ่งที่ทำให้การจัดเส้นทางเป็นกลไก log คำเตือนแล้วดำเนินต่อ การบันทึกที่ถูกยกเลิกผ่าน flag Aborted เป็นตัวอย่างทั่วไป นับข้อผิดพลาดแล้วให้ batch ทำงานต่อ เวิร์กชีตที่ serialize ไม่สำเร็จเป็นตัวอย่างทั่วไป หยุด batch เมื่อความรุนแรงเป็น fatal เพราะระดับนั้นหมายความว่า unhandled exception คลาย call ไปแล้ว และการทำงานต่อมีความเสี่ยงที่จะทำงานจาก state ที่อัปเดตไปครึ่งเดียว ข้อควรระวังที่ตรงไปตรงมาอย่างหนึ่ง Info มีอยู่ใน enum เป็นค่าเริ่มต้นที่ 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 เข้ากับ pipeline แบบ batch
การ poll Diagnostics หลังทุกการเรียกใช้ได้กับไฟล์เดียว มันหยุดใช้ได้ทันทีที่คุณกลับไปสู่ batch หนึ่งหมื่นไฟล์ข้ามคืนนั้น เพราะ Diagnostics ถูกเคลียร์เมื่อเริ่มทุกการเรียก Open, SaveAs และ Recalculate อ่านมันหลังไฟล์ที่สามใน loop แล้วคุณจะเห็นแค่การวินิจฉัยของไฟล์ที่สามเท่านั้น อะไรก็ตามที่สองไฟล์แรกรายงานไว้หายไปแล้ว OnDiagnostic แก้ปัญหานั้นด้วยการเปลี่ยน collection ให้เป็นสตรีม subscribe ครั้งเดียวก่อนที่ loop จะเริ่ม แล้ว handler ตัวเดียวกันก็ยิงสำหรับทุกไฟล์ตามลำดับ โดยชื่อไฟล์ยังคงอยู่ใน scope ผ่าน instance field
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 ราคาถูกด้วยเหตุผลเชิงโครงสร้าง มันยิงแค่เมื่อมีบางอย่างผิดพลาดอยู่แล้วเท่านั้น และความผิดพลาดหายากเมื่อเทียบกับจำนวนเซลล์ แถว หรือเวิร์กชีตที่ workbook หนึ่งถือไว้ เปรียบเทียบกับ OnProgress และ OnProgressEx ซึ่งรายงานความคืบหน้าปกติและต้องถูกออกแบบรอบความถี่ของการเรียกตั้งแต่แรก HotXLS ยิง event ความคืบหน้าระดับเวิร์กชีตครั้งเดียวต่อชีตระหว่าง Open และ SaveAs ไม่ใช่ครั้งเดียวต่อเซลล์หรือแถว ซึ่งเป็นสิ่งที่ทำให้ overhead ต่อการเรียกเล็กแม้แต่กับ workbook ที่มีเซลล์นับล้าน Recalculate ไปไกลกว่านั้นและ throttle event ความคืบหน้าของตัวเองให้อยู่ที่ประมาณทุกสี่เปอร์เซ็นต์ของ dependency graph ดังนั้นการคำนวณใหม่แบบเต็มรูปแบบจึงให้ heartbeat แก่คุณแทนที่จะท่วม UI thread ด้วย event การวินิจฉัยไม่ต้องการการ throttle แบบนั้นเลย เพราะจำนวน event ถูกจำกัดด้วยจำนวนปัญหาจริง ไม่ใช่ด้วยขนาดของไฟล์
จุดเดียวที่ประสิทธิภาพยังคงขึ้นอยู่กับคุณคือภายใน handler เอง OnDiagnostic ยิงแบบ synchronous บน thread ที่กำลังรัน Open, SaveAs หรือ Recalculate ดังนั้น handler ที่บล็อค เช่น การเขียนแบบ synchronous ไปยัง remote logging service จะกลายเป็นส่วนหนึ่งของเวลานาฬิกาจริงของการเรียกนั้น สำหรับไฟล์เดียวสิ่งนี้มองไม่เห็น แต่คูณข้าม batch หนึ่งหมื่นไฟล์มันคือความแตกต่างระหว่างงานที่เสร็จข้ามคืนกับงานที่ยังรันอยู่ตอนพักเที่ยง ดังนั้นให้ buffer สิ่งที่ handler ต้องทำและ flush มันแบบ asynchronous แทนที่จะทำส่วนที่ช้าแบบ inline
การวินิจฉัยแบบมีโครงสร้างมีคุณค่ามากที่สุดตรงจุดที่ผลลัพธ์แบบ boolean อ่อนแอที่สุด ใน workflow ที่แตะไฟล์หลายไฟล์แทนที่จะเป็นไฟล์เดียว pipeline ตรวจสอบและแปลง workbook เป็นตัวอย่างที่ชัดเจนที่สุด แทนที่จะบันทึกแค่ผ่าน/ไม่ผ่านเปล่าๆ ต่อไฟล์ ให้แนบรายการ Diagnostics ของแต่ละไฟล์เข้ากับ audit record ของมัน แล้วรายงานก็บอกคุณไม่ใช่แค่ว่าอะไรล้มเหลว แต่บอกทำไมด้วย ซึ่งเป็นส่วนใหญ่ของสิ่งที่บทความเรื่องการสร้างเวิร์กเบนช์สำหรับตรวจสอบและแปลง workbookของเราพยายามทำให้ถูกต้องตั้งแต่แรก การจับคู่ความคืบหน้ากับการวินิจฉัยแบบเดียวกันนี้ยังอยู่ใน workflow ใดก็ตามที่ต้องการการรายงานความคืบหน้าเพื่อประโยชน์ของตัวเองอยู่แล้ว ซึ่งเป็นดินแดนที่ครอบคลุมพอดีในคู่มือประสิทธิภาพ workbook ขนาดใหญ่ใน HotXLSของเรา ที่ซึ่งการเรียก Open หรือ SaveAs ที่ยาวนานเป็นเรื่องปกติมากพอที่ OnProgress จะถูกต่อสายไว้อยู่แล้ว และ OnDiagnostic ก็เป็นสิ่งเพิ่มเติมที่เป็นธรรมชาติและแทบไม่มีต้นทุนข้างๆ มัน
ไม่มีอะไรในนี้ที่ต้องมี Excel ติดตั้งอยู่ที่ใดใน pipeline เลย และไม่มีอะไรที่ต้องดัก exception ทั่วไปแล้วเดาว่ามันหมายถึงอะไร IXLSWorkbookProgress และ member Diagnostics, LastDiagnostic และ OnDiagnostic ของมันเป็นส่วนหนึ่งของHotXLS Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder ควบคู่ไปกับเอกสารอ้างอิงรหัสวินิจฉัยแบบเต็มและพื้นผิว Open, SaveAs และ Recalculate ที่เหลือซึ่งบทความนี้เดินผ่านมา