目錄 / Table of Contents

效能基準線

本文件說明 OdfKit 的效能基準線、穩定量測設定檔與報告產生方式。基準測試結果受 CPU、記憶體、磁碟、作業系統、電源設定與 JIT 狀態影響,因此本文件記錄「如何量測」與「目前關卡」,不把單一本機輸出宣告為跨機器服務等級承諾。

ODS/ODT 串流 I/O 的維護決策、隨機存取邊界與安全依據見 ODS/ODT 串流 Reader/Writer 設計與最佳化準則。

目前回歸關卡

eng/Benchmark-Regression.ps1 會執行 DomInsertBenchmarks.SequentialInsertAfter 與 OdsStreamWriterBenchmarks.WriteRows,並與 eng/baselines/performance-baselines.json 比對。

基準測試 基準線平均值 容許範圍 用途
DomInsertBenchmarks.SequentialInsertAfter 123.9 us +40% 偵測 DOM 循序插入效能的重大回歸
OdsStreamWriterBenchmarks.WriteRows 以 eng/baselines/performance-baselines.json 為準 時間 +40%、配置量 +15% 保護 200,000 列雙欄 ODS 串流寫入的時間與配置量
OdfPackageLoadBenchmarks.LoadFileMmf(128 KB) 以 eng/baselines/performance-baselines.json 為準 時間 +40%、配置量 +15% 保護主要檔案路徑封裝載入入口
FormulaEvaluationBenchmarks.FullRecalculation10000 223.9 ms/98.83 MB 時間 +40%、配置量 +15% 保護 10K 獨立公式交易式全量重算
FormulaEvaluationBenchmarks.IncrementalOnePercentRecalculation10000 2.058 ms/1.82 MB 時間 +40%、配置量 +15% 保護 10K 公式中 1% 受影響子圖的交易式增量重算

執行:

pwsh eng/Benchmark-Regression.ps1 -Configuration Release

排程 CI 另傳入 -ReportTimingRegression:共享 runner 的耗時超標會保留明確 notice, 配置量超標仍為硬性失敗。本機穩定環境省略此參數時,耗時與配置量都維持硬閘門。

更新基準線只應在刻意重訂效能基準時執行:

pwsh eng/Benchmark-Regression.ps1 -Configuration Release -UpdateBaseline

最新本機跨套件重新驗證

2026-07-26 以 Release 組件重新執行一百萬列 × 十欄的手動跨套件量測; 這是單機單次重新驗證,不取代回歸關卡,也不構成跨機器效能承諾。

情境 耗時 GC 累積配置量 峰值工作集
OdsStreamWriter 6,608 ms 472.4 MB 36.6 MB
MiniExcel 6,720 ms 3,354.3 MB 46.7 MB
ClosedXml 47,505 ms 10,949.9 MB 2,207.2 MB

完整環境、方法、輸出檔案大小與歷史結果見 OdfKit 與同類套件的串流寫入效能對比。 同日亦完成 ODT 串流寫入、ODS/ODT 串流讀取、ODP 結構讀取及三格式 DOM 來回讀寫的九情境本機重新驗證,結果見 ODS、ODT、ODP 標準效能基準。

資源限制壓力測試

載入與串流讀取安全限制 記錄的限制與「不得崩潰」的行為,由 OdfKit.Benchmarks 的壓力測試執行器轉為可執行檢查。BenchmarkDotNet 無法量測「壞了就整個 處理程序崩潰」的情境(堆疊溢位、記憶體耗盡),因此每個情境都在獨立子處理程序執行,崩潰即失敗。

pwsh eng/Benchmark-Stress.ps1
情境 判定方式 保護的行為
逐列建立工作表(8,000、16,000、32,000 列) 資料量放大 4 倍,耗時比值不得超過 8.0(線性約 4,二次方約 16) 逐列附加不得每列全表重掃;修正前 8,000 列約 9.5 秒
運算子連鎖公式(4,000 與 16,000 項,256 KB 與 128 KB 堆疊執行緒) 處理程序不得崩潰,且只能成功或被上限乾淨拒絕 公式遞迴堆疊餘量與運算子節點上限
100 萬個連續前置運算子 必須被乾淨拒絕 公式剖析巢狀深度上限
以引數為迴圈上限的公式(CRITBINOM、COMBIN、FACT 等 8 條) 每條在 10 秒內結束 迴圈上限與溢位提早離開

縮放比與「不得崩潰」不受機器速度影響,因此這組檢查不需要基準線 JSON;絕對耗時只列印供參考。 它只在 performance-benchmark.yml 的排程與手動觸發時執行,不進入 PR 必跑的 CI。

跨套件寫入與讀取對比(MiniExcel、ClosedXML;效能對比)耗時與記憶體都高, 只能在 performance-benchmark.yml 手動觸發並勾選 run_competitive_comparison 時執行,會在 Ubuntu 與 Windows 各跑一次完整的一百萬列寫入與讀取,並把輸出存為 competitive-comparison-<os> 產物(保留 14 天)。 讀取對比含內容檢查碼,不符即失敗。

Schema 驗證耗時

OdfPackageValidator 對 ODF 1.4 schema 的驗證耗時隨工作表列數成長。table:table 的內容模型含巢狀重複 oneOrMore(oneOrMore(table-row)),外層每個起點的展開都會回傳與列數同階的位置集合。先前的實作對每個起點 重新驗證整個子樹,並反覆走過整棵語法樹,耗時隨列數呈平方成長:同一台機器上 2,000 列約 72 秒,依平方成長 推估 5,000 列超過 7 分鐘。

現在的實作以位元集合表示位置、記憶「由某位置起重複比對可到達的位置」與各節點的比對結果,並由大到小處理 前緣位置,使巢狀重複每個位置只做一次集合聯集。序列中的重複節點之後若還有其他節點,每個起點都要把後續節點 套用到與列數同階的位置集合上;起點依遞減順序詢問時,後一次的輸入包含前一次的輸入,因此只對新增的位置計算 並併入前一次的輸出,列舉位置也改用不配置物件的結構列舉器。各起點記憶下來的可到達集合多半是連續的後綴區間, 位置集合因此以「起點加數量」表示連續區間,只有被加入不相鄰的位置時才轉成位元圖:

列數 耗時(暖機後) 未記憶序列推進(只換結構列舉器)
5,000 約 1 秒 未量測
20,000 約 3.6 秒 約 14 秒
40,000 約 7.4 秒 約 38 秒
80,000 約 20 秒 約 125 秒

先前版本(位元集合與迭代器)20,000 列約 16 秒。數字是一般開發機(Intel Core i7-9750H)上的單次量測,不是基準線;CI 開啟覆蓋率並使用較慢的執行器, 實測慢十倍以上。耗時略高於線性(80,000 列約為 20,000 列的 5 倍)。記憶體隨列數線性成長(約 17 KB/列): 峰值工作集 20,000 列約 340 MB、40,000 列約 680 MB、80,000 列約 1.4 GB(含建立文件與兩次驗證);區間表示之前 每個起點都記憶一份位元圖,40,000 列約 1.3 GB。完整驗證也會把整份 DOM 載入記憶體,更大的工作表仍建議使用 串流讀取器而不是完整驗證。

配置量(垃圾)另外量測:20,000 列的總配置約 3.6 GB(每列約 180 KB;以索引迴圈取代比對路徑的 LINQ、以小型陣列集合取代屬性狀態的 HashSet<BigInteger> 之前約 5.4 GB、每列約 270 KB)。剩下的配置主要是重複子元素的記憶字典與位置集合,與上述保留的記憶體同源,不是短命的垃圾;預先配置字典容量沒有效果,已還原。

RealWorldFidelityTests.SchemaValidationWorkGrowsLinearlyWithRowCount 以序列推進走訪的位置數(確定的計數, 不受執行器速度影響)鎖定成長階數:列數變成兩倍,走訪數不得接近四倍;修正前為 2,025,057 與 8,050,057(四倍)。 SchemaValidationOfManyRowsCompletesInReasonableTime 另以寬鬆的絕對上限(4,000 列在 120 秒內)攔截其他退化。

穩定本機量測設定檔

正式比較效能變更時,使用較長且時間導向的穩定量測設定檔:

pwsh eng/Benchmark-Stable.ps1 -Filter "*OdsStreamWriter*"

預設設定:

設定 值
BenchmarkDotNet 工作 Medium
單次迭代目標時間 250 ms
量測迭代次數 9 到 15
診斷欄位 記憶體、例外
輸出政策 BenchmarkDotNet 產物不提交,摘要報告可另存

產生基準線報告

使用 eng/Benchmark-BaselineReport.ps1 可用穩定量測設定檔跑指定基準測試,並產生 Markdown 報告:

pwsh eng/Benchmark-BaselineReport.ps1 -Filter "*OdsStreamWriter*" -OutputPath artifacts/performance/ods-stream-writer.md

此報告適合附在 pull request、發行說明或本機效能調查紀錄中。若要保留長期版本化摘要,請人工挑選穩定機器與代表性基準測試,再將摘要數字更新至本文件或基準線 JSON。

代表性基準測試分層

層級 Benchmark 觀察重點
封裝 / DOM DomInsertBenchmarks、DomTextContentBenchmarks、OdfPackageLoadBenchmarks DOM 變更、文字內容存取、封裝載入
試算表串流 OdsStreamWriterBenchmarks、OdfTableSheetCellAccessBenchmarks 大量列寫入、儲存格存取快取
文件來回讀寫 OdtRoundTripBenchmarks 建立、儲存、載入大型 ODT
三格式標準 StandardOdsBenchmarks、StandardOdtBenchmarks、StandardOdpBenchmarks ODS/ODT/ODP 標準讀寫、DOM 與語意檢查碼
共通封裝 StandardPackageOpenBenchmarks 分離 ZIP 封裝開啟與文件模型成本
公式 FormulaParseBenchmarks、FormulaEvaluationBenchmarks 公式剖析、1K/10K 全量重算、10K 線性鏈與寬 DAG、1% 交易式增量重算、大範圍及 100 × 100 陣列的配置量與延遲
樞紐 OdfPivotCalculatedBenchmarks 10K 列一般彙總、計算欄位、數值分組、列百分比與雙軸總計的時間及配置量
協作 CollaborationOperationBenchmarks TDF JSON operation 剖析與重播

ODS 可編輯 DOM 載入分層

StandardOdsBenchmarks 將一般 SpreadsheetDocument.Load 拆成三個不可混為單一 「開檔時間」的情境:

  • LoadComplexDomAndEnumerateSheets:只建立外層 DOM 與列舉工作表中繼資料,工作表列維持 lazy。
  • LoadComplexDomAndReadFirstSheet:載入後首次具現化第一張工作表。
  • LoadComplexDomAndReadLastSheet:驗證定位最後一張工作表不會連帶具現化前面的工作表。

核心 XML entry 已完成 ZIP 大小與 CRC 驗證後,直接以既有 UTF-8 記憶體交給 span parser; 大型 table:table 保留來源 memory slice,避免 ReadInnerXml() 建立大型 UTF-16 字串後再 複製成 UTF-8 陣列。載入期的 LibreOffice 擴充屬性正規化延後至該 lazy subtree 首次 具現化,且多執行緒首次存取只允許單次具現化。完整儲存對未觸碰的 lazy subtree 會直接傳遞 原始 UTF-8;若 subtree 已具現化或需要 sparse cell 序列化,仍會走訪必要內容。LoadOnly 與 LoadAndSaveUntouchedLazyDom 必須分別量測,不能把前者宣稱為 round-trip 成本。

此設計借鑑 Deflux 將工作表發現與內容讀取分離的 產品邏輯,但不引用其實作,也不加入 Checkpoint/DEFLATE 狀態引擎。安全與資源邊界依 Microsoft .NET ZIP 最佳實踐、 Microsoft XML Reader 安全設定 及 OASIS ODF 1.4 Package 標準 維持 entry 數量/大小、解壓總量、DTD、resolver、XML 字元數、巢狀深度與路徑防護。

相同核心路徑也服務 ODT、ODP 與 ODG:大型 text:p/text:list、內嵌表格及 styles/meta/settings subtree 都保留 UTF-8 lazy slice。StandardOdtBenchmarks 另以 LoadLargeParagraphAndEnumerateParagraphs 與 LoadLargeParagraphAndReadText 分離 ODT 外層載入和首次段落內容存取;ODP/ODG 的投影片/頁面索引只列舉外層 draw:page,不會 因建立集合而讀取其大型段落內容。四格式共同的快速 parser 必須拒絕 DTD/其他 XML markup declaration,並將 lazy payload 計入呼叫端指定的 MaxXmlCharactersInDocument;首次具現化 沿用原始 StrictXmlParsing 與字元上限,不可退回較寬鬆的預設值。

執行緒邊界維持「單一文件不保證無鎖並行讀寫」:不同文件可平行處理,同一 lazy subtree、 ODS worksheet facade 與 row/cell sparse cache 的首次發布具備同步保護;同一文件的多個儲存 操作會序列化,但 DOM Children/Attributes、投影片/繪圖頁面集合仍不得與修改或儲存並行。

判讀規則

  • 先看配置量,再看平均值;配置量突增通常比小幅平均值波動更值得追。
  • 同一台機器上比較同一個 filter、同一個量測設定檔、同一個電源模式。
  • 對微型基準測試結果保持保守;若 BenchmarkDotNet 提示 minimum iteration time 過短,請提高 -IterationTime 或資料量。
  • CI 適合跑煙霧測試與回歸關卡,不適合以單一本機毫秒數作為跨平台硬門檻。
  • 三格式標準工作負載與獨立子處理程序報告方法見 ODS、ODT、ODP 標準效能基準。

公式完整矩陣以穩定設定檔執行:

pwsh eng/Benchmark-Stable.ps1 -Filter "*FormulaEvaluationBenchmarks*"

BenchmarkDotNet 的 MemoryDiagnoser 同時記錄配置量與 Gen0/1/2;每公式配置量以 Allocated / FormulaCount 判讀。正式更新公式回歸基準前,必須在同一台穩定機器以 相同設定連跑三次並採中位數。時間容許 +40%、配置量容許 +15%;共享 CI runner 只執行固定小型工作負載,不把單次數值寫成公開效能宣稱。