計算ワーカーでのカスタム関数

supportCalcWorker() メソッドをオーバーライドして true を返すことで、カスタム関数を 計算ワーカー内で直接実行できます。これにより計算時の UIスレッドとの往復処理がなくなり、パフォーマンスを大きく向上できる場合があります。

既定では、UI スレッドで登録されたカスタム関数は、計算ワーカーと UI スレッド間の往復処理によって評価されます。supportCalcWorker() をオーバーライドして true を返すことで、関数の evaluate ロジックをシリアライズし、計算ワーカー内で直接実行して往復処理のオーバーヘッドを回避できます。 このデモでは、同一の SHA-256 ハッシュ関数を 2 つ比較します。1 つは既定のメインスレッド評価(HASH)を使用し、もう 1 つは Calc Worker 内で直接実行(HASHINWORKER)します。1,000 個の数式セルで実行して、パフォーマンスの違いを示します。 制限事項 関連するすべてのコードは evaluate 関数内に配置する必要があります。 SpreadJS は customFunction.evaluate.toString() を呼び出し、関数ソースを 計算ワーカーに送信します。そのため、関連するすべてのロジックは evaluate(または evaluateAsync)関数自体の内部で定義する必要があります。 計算ワーカーでは Workbook オブジェクトと Worksheet オブジェクトは使用できません。 計算ワーカー内では、GC.Spread.Sheets、spread.calculate、spread.suspendCalcService などのオブジェクトや API は公開されていません。カスタム関数で使用できるのは、評価コンテキストと Web Worker の組み込み API のみです。
window.onload = function () { var spreadLeft = new GC.Spread.Sheets.Workbook(document.getElementById("ssLeft"), { sheetCount: 1 }); var spreadRight = new GC.Spread.Sheets.Workbook(document.getElementById("ssRight"), { sheetCount: 1 }); spreadLeft.options.incrementalCalculation = true; spreadRight.options.incrementalCalculation = true; initSpread(spreadLeft, spreadRight); }; async function initSpread(spreadLeft, spreadRight) { function HashFunction() { } HashFunction.prototype = new GC.Spread.CalcEngine.Functions.AsyncFunction("HASH", 1, 1); HashFunction.prototype.defaultValue = function () { return "計算中..."; }; HashFunction.prototype.evaluateAsync = function (context, value) { var text = value == null ? "" : String(value); var encoder = new TextEncoder(); var data = encoder.encode(text); crypto.subtle.digest("SHA-256", data).then(function (hashBuffer) { var hashArray = Array.from(new Uint8Array(hashBuffer)); var hashHex = hashArray.map(function (b) { return b.toString(16).padStart(2, "0"); }).join(""); context.setAsyncResult(hashHex.substring(0, 6)); }); }; function HashInWorkerFunction() { } HashInWorkerFunction.prototype = new HashFunction(); HashInWorkerFunction.prototype.name = "HASHINWORKER"; HashInWorkerFunction.prototype.supportCalcWorker = function () { return true; }; GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASH", new HashFunction()); GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASHINWORKER", new HashInWorkerFunction()); var sheetLeft = spreadLeft.getActiveSheet(); var sheetRight = spreadRight.getActiveSheet(); // Calc Worker の読み込み完了を待機します await spreadLeft.waitForAllCalculations(); await spreadRight.waitForAllCalculations(); setupSheet(sheetLeft, "HASHINWORKER", true); setupSheet(sheetRight, "HASH", false); // 初回計算 var startLeft = performance.now(); sheetLeft.resumeCalcService(); await spreadLeft.waitForAllCalculations(); var timeLeft = (performance.now() - startLeft).toFixed(2); sheetLeft.setValue(1, 0, "計算時間: " + timeLeft + " ms"); var startRight = performance.now(); sheetRight.resumeCalcService(); await spreadRight.waitForAllCalculations(); var timeRight = (performance.now() - startRight).toFixed(2); sheetRight.setValue(1, 0, "計算時間: " + timeRight + " ms"); // 各 Spread の再計算ボタンを個別にバインドします function bindRecalc(spread, sheet) { var isRecalculating = false; spread.bind(GC.Spread.Sheets.Events.ButtonClicked, async function () { if (isRecalculating) { return; } isRecalculating = true; sheet.setValue(1, 0, "計算中..."); var start = performance.now(); spread.calculate(); await spread.waitForAllCalculations(); var time = (performance.now() - start).toFixed(2); sheet.setValue(1, 0, "計算時間: " + time + " ms"); isRecalculating = false; }); } bindRecalc(spreadLeft, sheetLeft); bindRecalc(spreadRight, sheetRight); } var names = [ "佐藤 太郎", "鈴木 花子", "田中 一郎", "高橋 美咲", "伊藤 健太", "渡辺 さくら", "山本 大輔", "中村 葵", "小林 翔", "加藤 優子", "吉田 陽菜", "山田 直樹", "佐々木 真央", "山口 拓也", "松本 結衣", "井上 亮", "木村 彩", "林 俊介", "清水 愛", "森 悠斗" ]; var emails = [ "alice@example.com", "bob@example.com", "charlie@example.com", "diana@example.com", "edward@example.com", "fiona@example.com", "george@example.com", "helen@example.com", "ivan@example.com", "julia@example.com", "kevin@example.com", "laura@example.com", "michael@example.com", "nancy@example.com", "oscar@example.com", "patricia@example.com", "quincy@example.com", "rachel@example.com", "steven@example.com", "tina@example.com" ]; function setupSheet(sheet, funcName, isWorker) { sheet.suspendPaint(); sheet.suspendCalcService(); sheet.options.allowCellOverflow = true; var headerColor = isWorker ? "#51cf66" : "#ff6b6b"; var headerForeColor = "#ffffff"; var lightBg = isWorker ? "#d3f9d8" : "#ffe0e0"; var dataRows = 1000; var headerRow = 0; var timeRow = 1; var tableHeaderRow = 2; var dataStartRow = 3; sheet.setRowCount(dataStartRow + dataRows); sheet.setColumnCount(4); sheet.setColumnWidth(0, 40); sheet.setColumnWidth(1, 150); sheet.setColumnWidth(2, 170); sheet.setColumnWidth(3, 120); // 0 行目: タイトル sheet.addSpan(headerRow, 0, 1, 4); var title = isWorker ? "supportCalcWorker() あり" : "supportCalcWorker() なし"; sheet.setValue(headerRow, 0, title); sheet.getCell(headerRow, 0).backColor(headerColor).foreColor(headerForeColor).font("bold 14px Arial") .hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center); sheet.setRowHeight(headerRow, 36); // 1 行目: 計算時間(0-2 列)と再計算ボタン(3 列) sheet.addSpan(timeRow, 0, 1, 3); sheet.setValue(timeRow, 0, "計算中..."); sheet.getCell(timeRow, 0).backColor(lightBg).font("bold 16px Consolas") .hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center); sheet.setRowHeight(timeRow, 30); // D2 の再計算ボタン var btnCellType = new GC.Spread.Sheets.CellTypes.Button(); btnCellType.text("再計算"); btnCellType.buttonBackColor(lightBg); sheet.setCellType(timeRow, 3, btnCellType); sheet.getCell(timeRow, 3).backColor(lightBg) .hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center); // 2 行目: テーブルヘッダー var headers = ["#", "名前", "メール", funcName + "()"]; for (var c = 0; c < headers.length; c++) { sheet.setValue(tableHeaderRow, c, headers[c]); sheet.getCell(tableHeaderRow, c).backColor("#f1f3f5").font("bold 12px Arial") .hAlign(GC.Spread.Sheets.HorizontalAlign.center); } sheet.setRowHeight(tableHeaderRow, 26); // データ行 for (var i = 0; i < dataRows; i++) { var row = dataStartRow + i; sheet.setValue(row, 0, i + 1); sheet.setValue(row, 1, names[i % names.length]); sheet.setValue(row, 2, emails[i % emails.length]); sheet.setFormula(row, 3, "=" + funcName + "(B" + (row + 1) + "&C" + (row + 1) + ")"); if (i % 2 === 1) { sheet.getRange(row, 0, 1, 4).backColor("#fafafa"); } } sheet.resumePaint(); }
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta name="spreadjs culture" content="ja-jp" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets-resources-ja/dist/gc.spread.sheets.resources.ja.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div class="sample-tutorial"> <div class="spread-panel"> <div id="ssLeft" class="sample-spreadsheets"></div> </div> <div class="spread-panel"> <div id="ssRight" class="sample-spreadsheets"></div> </div> </div> </body> </html>
body { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; } .sample-tutorial { position: relative; height: 100%; overflow: hidden; display: flex; } .spread-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .spread-panel:first-child { border-right: 2px solid #e0e0e0; } .sample-spreadsheets { flex: 1; overflow: hidden; }