概要

項目内容
案件 IDMAS-233 (旧 N-57 / N-057)
案件名GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)
カテゴリDevOps・パフォーマンス・計測基盤
PhaseP1
優先度★★(MAS-231 最適化パス 1 の前提依存・並行実装可)
所要時間約 1.5-2 週間(週 10h 前提・Step 1-2 で計測基盤 + 主要関数埋込、Step 3-5 で集計・可視化・アラート)
対象ファイル(追加)000_infra/004_utils.jsUtils.perfStart / Utils.perfEnd / Utils.perfFlush 追加・約 80 行)/ 800_ops/810_perf_aggregator.js(日次集計・約 150 行)
対象ファイル(変更)400_domain/410_subledger_engine.jsAction A/B エントリ)/ 400_domain/407_rpa_orchestrator.jsRPA 起票)/ 600_report/601_datamart_ingest.js608_datamart_render.jsマート更新)/ 300_ui/301_ui_assist.js(サイドバー初期化)/ 500_import/502_receipt_reader.js(OCR 処理)/ 100_config/101_sys_config.jsDDLLOG_PERF 追加 + メニュー登録)
新規シート99_perf_log(ログ生シート・WORM・自動ローテーション 90 日)/ DDL 管理(LOG_PERF キー)
新規 03_sys_params キーMAS233_PERF_ENABLED(default true)/ MAS233_PERF_FLUSH_INTERVAL_SEC(default 60)/ MAS233_PERF_RETENTION_DAYS(default 90)/ MAS233_PERF_ALERT_P95_MS(default 10000)
前提案件なし(既存 Utils / CacheService / 99_error_log / 98_audit_log の運用パターンを踏襲)
後続連携MAS-231(GAS 最適化パス 1)— 本案件の Utils.perfStart / perfEnd API と 99_perf_log シートを利用して効果測定。MAS-233 完了後に MAS-231 着手、または並行実装で API シグネチャを spec で fix

目的

何が遅いのか」を数値で把握するための計測基盤を整備し、MAS-231 最適化パス 1 着手前のボトルネック特定と、最適化後の効果検証を可能にする。

  • ラベル別の実行時間累積を CacheService に保管し、関数の呼出回数・総経過時間・P50・P95 を 99_perf_log に日次集計
  • Action A/B、マート更新、RPA 起票、サイドバー初期化、OCR 処理の主要 5 系統に計測埋込(並列実装の対称性確保・failure_patterns #25)
  • 週平均 P95 > 10 秒のホットスポット自動検知 → 上位 5 ボトルネックを MAS-231 で対策する 計測 → 改善 ループ を確立
  • SpreadsheetApp.flush() 呼出箇所の所在地 grep + google.script.run ラウンドトリップ件数の自動計測で MAS-231 の Step 3 / Step 6 を裏付けるエビデンスを取得

現在のコード

利用する既存 API(実在確認済・全て Read で裏取り)

API定義場所シグネチャ用途
Utils.logInfo(funcName, message)000_infra/004_utils.js:494(string, string) → void計測ラベル開始/終了の info ログ(任意)
Utils.logError(funcName, error, context)000_infra/004_utils.js:504(string, Error, string) → voidperfFlush 失敗時のエラーログ
Utils.persistLog(level, funcName, message, detail)000_infra/004_utils.js:577(string, string, string, *) → voidアラート発火時の WARN 永続記録
Utils.auditLog(operation, ...)000_infra/004_utils.js:539DDL 適用 / メニュー実行を 98_audit_log に残す既存パターン参考
Constants.getParam(key, defaultVal)000_infra/002_constants.js:205(string, *) → string|numberMAS233_PERF_* 4 キー取得
Constants.CONFIG_SHEET000_infra/002_constants.js:7'01_sys_config'キー登録先(参考)
CacheService.getScriptCache()GAS 標準perfStart / perfEnd の累積記録
LockService.getDocumentLock()GAS 標準perfFlush 時の競合回避(並行実行ガード)
Utils.getSheetByKey(key, fallback)000_infra/004_utils.js:302(string, string) → Sheet99_perf_log 取得
getWebSpreadsheet_()000_infra/004_utils.js:9() → SpreadsheetWeb App / コンテナバインド両対応

計測埋込対象のエントリポイント(実在確認済)

系統エントリ関数定義場所ラベル例
Action A (INV 起票)processInvoiceApprovals()400_domain/410_subledger_engine.js:103actionA.processInvoiceApprovals
Action B (消込確定)processSettlementClearings()400_domain/410_subledger_engine.js:361actionB.processSettlementClearings
RPA 起票RPAService.*400_domain/407_rpa_orchestrator.jsrpa.<service>
マート更新runDataMart 系(600_report/601_datamart_ingest.js608_datamart_render.jsdatamart.<step>
サイドバー初期化doGet(e)100_config/101_sys_config.js:400)/ getInitialStateForSpa(300_ui 系)sidebar.bootstrap
OCR 処理502_receipt_reader.js のエントリocr.process
DDLsetupAllSchemas(isFull)100_config/101_sys_config.js:840ddl.setupAllSchemas

既存 DDL 登録パターン(LOG_PERF 追加先)

100_config/101_sys_config.js:884LOG_AUDIT 登録パターン(existKeys.includes('LOG_AUDIT') ガード + confSheet.appendRow(...))と完全対称に LOG_PERF を追加する。schemas ハッシュ('LOG_PERF': { headers: [...], color: '#434343' })も同様に LOG_AUDIT(同 L1047)と並列に定義する。

既存 _paramsCache パターン(再利用可能)

Constants.getParam (000_infra/002_constants.js:205) は 起動時 1 回だけ 03_sys_params を読み込み、以降キャッシュ する設計。MAS233_PERF_* 4 キーは本パターンをそのまま利用できるため、追加実装は Constants.PERF_DEFAULTS のような新規定数のみ。

修正方針

Step 1: Utils.perfStart / Utils.perfEnd / Utils.perfFlush004_utils.js に追加

// 000_infra/004_utils.js (Utils 名前空間に追加)
Utils.perfStart = function (label) {
  if (!_perfEnabled_()) return null;
  return { label: label, t0: Date.now() };
};

Utils.perfEnd = function (token) {
  if (!token) return;
  var elapsed = Date.now() - token.t0;
  _perfBuffer_().push({ label: token.label, ms: elapsed, ts: new Date() });
  // バッファサイズ閾値超過 or 60 秒経過で flush
  if (_shouldFlush_()) Utils.perfFlush();
};

Utils.perfFlush = function () {
  // CacheService から累積バッファを読出 → 99_perf_log に append → CacheService クリア
  // LockService.getDocumentLock() で並行 flush をシリアライズ
};

設計判断:

  • 計測ラベルの粒度: 関数単位 (actionA.processInvoiceApprovals) を基本とし、Step 1 では「処理ブロック単位」(例: actionA.fetchInvoices / actionA.buildJournals / actionA.writeJournals)の細粒度計装は対象外(v2 で検討)。粒度を絞ることで CacheService 100KB 制限と Cache 書込頻度のオーバーヘッドを最小化する。
  • CacheService 書込頻度: バッファリング方式を採用。perfEnd の都度 cache.put すると 1 計測あたり 50-100ms のオーバーヘッドが付くため、Utils._perfBuffer_ (in-memory array) に蓄積し、MAS233_PERF_FLUSH_INTERVAL_SEC 経過 or バッファサイズ 200 件超で Utils.perfFlush() を呼出。毎回 cache.put は実装しない(計測自体が遅いと無意味)。
  • シリアライズ: バッファは JSON.stringifycache.put。値が 100KB を超える場合は WARN ログ + 旧バッファを 99_perf_log に直接 append してクリア(failure_patterns #29 の Infinity 検査も実施)。

Step 2: 主要関数への計測埋込(5 系統対称適用・failure_patterns #25 遵守)

Action A (410_subledger_engine.js:103 processInvoiceApprovals 先頭・末尾):

function processInvoiceApprovals() {
  var perf = Utils.perfStart('actionA.processInvoiceApprovals');
  try {
    // 既存ロジック
  } finally {
    Utils.perfEnd(perf);  // try/finally で必ず計測終了
  }
}

Action B (410_subledger_engine.js:361 processSettlementClearings) — Action A と完全対称(failure_patterns #25 / 銀行・CC 並列実装の教訓)。

RPA 起票 (407_rpa_orchestrator.js RPAService.* 各エントリ) — rpa.<funcName> ラベルで一括計装。

マート更新 (600_report/60[1-8]_datamart_*.js 各エントリ) — datamart.<step> ラベル。

サイドバー初期化 (101_sys_config.js:400 doGet(e) 先頭 + 300_uigetInitialStateForSpa 等の public エンドポイント) — sidebar.bootstrap / sidebar.<endpoint> ラベル。

OCR 処理 (502_receipt_reader.js エントリ + callDocumentAiInvoiceParser_ / callGeminiForReasoningOnVertex_ 内部呼出) — ocr.docai / ocr.gemini ラベルで API レイテンシも分離計測。

Step 3: 99_perf_log シート定義 + DDL 登録

// 100_config/101_sys_config.js schemas に追加
'LOG_PERF': {
  headers: ['日時', 'ラベル', '処理件数', '所要時間(ms)', 'P50(ms)', 'P95(ms)', '関数名', '備考'],
  color: '#434343'
},

existKeys.includes('LOG_PERF') ガード + confSheet.appendRow(['LOG_PERF', '', '99_perf_log', 'パフォーマンス計測ログ(操作追跡・90日ローテーション)'])LOG_AUDIT 登録(L884)の直後に対称配置。

Step 4: 日次集計 (800_ops/810_perf_aggregator.js 新設)

function aggregatePerfLogDaily() {
  // 99_perf_log から過去 1 日分を読込 → ラベル別に件数 / 総 ms / P50 / P95 を集計
  // 集計結果を新しい行として 99_perf_log に追記 (種別='AGG_DAILY')
  // 90 日超の生ログ行は別シート 99_perf_log_archive_YYYYMM に移送 (98_audit_log と同パターン)
}

時間トリガー: ScriptApp.newTrigger('aggregatePerfLogDaily').timeBased().atHour(2).everyDays(1).create() (02:00 JST 実行)。ローテーションは 98_audit_logarchiveAuditLogMonthly_ (101_sys_config.js:1749 付近) と同一パターン。

Step 5: 可視化 + ボトルネック検知

可視化: シート内スパークライン + 条件付き書式(赤: P95 > 5 秒 / 黄: P95 > 1 秒 / 緑: P95 < 1 秒)。Looker Studio 連携は v1 では実装せず、人間検討事項に残す(Looker Studio はスプレッドシート直結が GAS 経由不要 + ライセンス無料のため将来オプションとして容易に追加可能)。

ボトルネック検知ルール:

  • (a) ラベル別週平均 P95 > MAS233_PERF_ALERT_P95_MS(default 10000ms)→ Utils.persistLog('WARN', 'aggregatePerfLogDaily', 'P95 alert: ' + label, ...) でアラート + Session.getEffectiveUser().getEmail()MailApp.sendEmail 通知(オプション・スパム防止のため日次 1 回上限)
  • (b) SpreadsheetApp.flush() 呼出箇所の特定: scripts/grep-flush.sh(既存 scripts/ パターン踏襲)で grep -nE "SpreadsheetApp\.flush\(\)" 400_domain/*.js 600_report/*.js 100_config/*.js を CI 実行 + 件数を 99_perf_log に静的記録(実行時計測ではなく静的解析)
  • (c) google.script.run ラウンドトリップ件数: クライアント側タイマーで withSuccessHandler 受信時刻を記録し、サーバー側 perfEnd(token) の所要時間と差分を取れば「クライアント送信 → サーバー受信 → クライアント受信」の往復レイテンシを推定可能(v1 は GAS 側のみ実装し、クライアント側計測は SPA 化と並行検討)

Step 6: メニュー登録 + 動作確認

Constants.MENU_DEFINITION に「📊 パフォーマンス診断」カテゴリを新設し、aggregatePerfLogDaily / clearPerfLog(90 日超を強制クリア)/ openPerfLogSheet の 3 機能を登録。category: '🔧 開発・設定' 配下に配置 (既存 🔧 マイグレーション と並列・002_constants.js:310-370 付近のパターン踏襲)。

影響範囲

対象種別変更内容リスク
000_infra/004_utils.js変更Utils.perfStart / perfEnd / perfFlush + 内部ヘルパー _perfEnabled_ / _perfBuffer_ / _shouldFlush_ を追加(約 80 行)既存 Utils.* への影響なし。token が null の場合は no-op(計測 OFF 時)
100_config/101_sys_config.js変更schemas.LOG_PERF 追加 + existKeys.includes('LOG_PERF') ガード + メニュー登録(約 30 行)DDL 整合性は setupAllSchemas 既存テストでカバー
400_domain/410_subledger_engine.js変更processInvoiceApprovals / processSettlementClearings 各先頭・末尾に perfStart / perfEnd ラップ(4 行追加)try/finally で必ず perfEnd 呼出。例外伝播は不変
400_domain/407_rpa_orchestrator.js変更RPAService.* 各エントリに同等ラップ(10-15 行追加)同上
600_report/601-608_datamart_*.js変更runXxx エントリに同等ラップ(合計 16 行追加)同上
300_ui/301_ui_assist.js変更サイドバー初期化エントリに同等ラップ(4 行追加)同上
500_import/502_receipt_reader.js変更OCR エントリ + DocAI / Gemini API 呼出に同等ラップ(10 行追加)同上
800_ops/810_perf_aggregator.js追加日次集計 + ローテーション(約 150 行)新規ファイル・既存ロジックに影響なし
99_perf_log新規シートDDL 管理 (LOG_PERF キー)既存シートに影響なし
99_perf_log_archive_YYYYMM動的生成90 日超ログの月次アーカイブ動的生成タブ(98_audit_log と同パターン)
03_sys_params変更MAS233_PERF_* 4 キー追加既存キーには影響なし
appsscript.json変更なしCacheService / LockService / MailApp / ScriptApp.newTrigger は既存スコープ内failure_patterns #26 遵守(部分宣言禁止)
docs/_config.json変更nav 1 行追加(§E.1 基盤)PR 競合に注意

注意事項

  1. #18-#20(命名造語禁止): Utils.perfStart / Utils.perfEnd / Utils.perfFlush のみを定義し、それ以外の架空関数(Utils.measureExecution / PerfMonitor.start 等)は spec も実装も禁止。Constants.PERF_* 定数も Constants.getParam('MAS233_PERF_*') 経由のみで参照(独立定数化しない)。
  2. #25(並列実装対称性): Action A / Action B は 完全対称的に計測埋込する(片方だけが try/finally でラップされる状態を禁止)。RPA 系・マート系も同パターンを徹底。grep -nE "Utils.perfStart|Utils.perfEnd" 400_domain/ 600_report/ で件数の偶数性(start と end が等数)を確認。
  3. #26(oauthScopes 部分宣言禁止): appsscript.json を一切編集しない。CacheService / LockService / MailApp / ScriptApp.newTrigger / SpreadsheetApp は既存 cloud-platform 系スコープでカバー。
  4. #27(Admin SDK API 変動): 該当なし(本案件は GAS 標準 API のみ使用)。
  5. #28(末尾 _ の private 化): aggregatePerfLogDaily / clearPerfLog / openPerfLogSheetMENU_DEFINITION 経由で呼ばれるため末尾 _ 禁止。一方、内部ヘルパー _perfEnabled_ / _perfBuffer_ / _shouldFlush_ / _perfRotateOldLogs_ は同一ファイル内のみから呼ばれるため末尾 _ を必ず付ける
  6. #29(V8→Java Infinity null): P50 / P95 計算で Math.max の空配列が -Infinity を返すため、Number.isFinite() でガード + 0 にフォールバック。99_perf_log 書込前に必ず _scrubInfinityForJSON_ 相当の処理を通す(300_ui/302_spa_bridge.js 参照)。
  7. 計測自体のオーバーヘッド: Utils.perfStart / perfEnd 1 セットあたり <1ms を必須とする。バッファリング方式で cache.put を 60 秒に 1 回に集約するため、計測 ON でも実用上影響なし(回帰テスト: MAS233_PERF_ENABLED=false 時と比較して < 5% 差)。
  8. CacheService クォータ枯渇: 単一値 100KB / 全体 100MB / 1 時間 1000 calls の制約あり。バッファサイズ閾値(200 件)+ 60 秒 flush で 1 時間あたり最大 60 calls に抑制。サイズ超過時は WARN ログ + 直接 append 経由の fallback で記録漏れゼロを保証。
  9. 99_perf_log 保持期間: 90 日を default(MAS233_PERF_RETENTION_DAYS)。90 日超は月次アーカイブシート 99_perf_log_archive_YYYYMM に移送(98_audit_logarchiveAuditLogMonthly_ パターン踏襲)。無期限保持は採用しない(Spreadsheet の 1000 万セル上限・編集時の重さ回避のため)。
  10. Looker Studio 連携の要否: v1 では実装しない。Sheets 直結で接続できるため、必要時に Looker 側でビューを構築すれば GAS 側変更不要。可視化は v1 ではシート内スパークライン + 条件付き書式(赤/黄/緑)で十分。
  11. ユーザー操作計測(クライアント側タイマー): v1 では GAS 側のみ実装。サイドバー側 google.script.run.withSuccessHandler 受信時刻記録は MAS-232 SPA 化と並行検討(Date.now() 差分のシリアライズに注意・failure_patterns #29 と同パターンで Number.isFinite ガード)。
  12. トリガー登録の冪等性: aggregatePerfLogDaily の時間トリガーは ScriptApp.getProjectTriggers() で既存確認 → 重複登録回避。MAS-201 バックアップトリガー(801_backup_tool.js 等)の同パターン踏襲。
  13. アラート発火の頻度制御: MAS233_PERF_ALERT_P95_MS 超過のメール通知は 日次 1 回を上限(CacheServiceperf_alert_last_sent キーで 24 時間ロック)。スパム化を回避。
  14. マイグレーション運用ガイドライン整合: 800 番台は CLAUDE.md「マイグレーションスクリプト運用ガイドライン」配下だが、810_perf_aggregator.js は計測基盤の常駐スクリプトであり、冪等性・ログ出力・メニュー登録の 3 ルールは遵守する(マイグレーション番号 810 は本案件で消費・次回マイグレーションは 811 から)。

エッジケース

実装時に必ず以下 12 件を 900_test/901_test_runner.js の単体テストでカバーする。

#条件検知方法期待される挙動ログ出力
1MAS233_PERF_ENABLED=false 時の no-opperfStartnull を返し、perfEnd(null) が即時 return計測完全無効化・既存ロジックへの影響ゼロ(回帰テスト < 5% 差)
2perfStart から perfEnd の間で例外発生try/finally の finally 分岐で perfEnd が呼ばれる例外発生時も計測が記録される(catch して swallow しない)finally で perfEnd
3同一ラベルの multiple start without end (ネスト誤用)perfStart の戻り値 token が独立しているため、ネスト呼出は問題なし各 token 独立に t0 を持ち、perfEnd で正しく差分計測
4バッファサイズ 100KB 超過JSON.stringify(buffer).length >= 100000バッファをそのまま 99_perf_log に直接 append + Cache クリア + WARN ログUtils.persistLog('WARN', 'Utils.perfFlush', 'Buffer > 100KB direct flush', ...)
5CacheService クォータ枯渇cache.putCache service quota exceeded 例外例外捕捉 + バッファを 99_perf_log に直接 append + 当日のみ計測一時停止Utils.persistLog('WARN', 'Utils.perfFlush', 'Cache quota exceeded', ...)
6並行 flush(複数の onEdit / トリガーが同時実行)LockService.getDocumentLock().tryLock(5000)ロック取得失敗時は次回 flush に持ち越し(バッファに追記継続)Utils.persistLog('INFO', 'Utils.perfFlush', 'Lock contention, retry next', ...)
799_perf_log シート未作成DDL 未適用環境で Utils.getSheetByKey('LOG_PERF', '99_perf_log') が null自動作成(98_audit_log の auditLog 既存パターン踏襲・004_utils.js:546-554)+ INFO ログUtils.logInfo('Utils.perfFlush', 'auto-created 99_perf_log')
8P50 / P95 計算で空配列samples.length === 00 を返す(Math.max(...[])-Infinity を回避・failure_patterns #29)
990 日ローテーションで月跨ぎ集計実行時刻が月初の場合、前月分はアーカイブ済99_perf_log_archive_YYYYMM を当月分とは別シートとして冪等作成Utils.auditLog('MIGRATE', '99_perf_log', '', '', 'aggregatePerfLogDaily', ...)
10アラートのスパム化MAS233_PERF_ALERT_P95_MS 超過が連日継続CacheServiceperf_alert_last_sent_<label> キーで 24 時間ロックUtils.persistLog('WARN', 'aggregatePerfLogDaily', 'P95 alert: <label>', ...)
11DDL 再適用時のラベル名衝突LOG_PERF キーが既に登録済existKeys.includes('LOG_PERF') ガードで重複追加回避(LOG_AUDIT 同パターン)
12環境(dev / prod)切替時のキャッシュ混在CacheService はスクリプト ID スコープのため環境間で混在しないEnv.name() をキーに含める(例: perf_buffer_dev / perf_buffer_prod不要(実装で物理分離)

実データ検証

実装完了後に以下を MCP / GAS エディタ + dev で実行し、合格基準と一致することを spec 完成の必須条件とする。

1. Utils.perfStart / perfEnd 1 セットのオーバーヘッド計測

計測指標計測 OFF計測 ON合格基準
processInvoiceApprovals 100 件実行(基準値)+5% 以内< 5%
runDataMart 5 年分(基準値)+5% 以内< 5%

2. 99_perf_log 集計結果の妥当性

計測項目期待値
ラベル数5 系統 × 各 3-5 関数 = 15-25 ラベル
1 日あたりログ行数100-500 行(通常運用)
P50 / P95 計算精度サンプル数 ≥ 10 で誤差 < 5%、< 10 は 備考 に "サンプル不足" と記録

3. アラート発火検証

シナリオ期待される挙動
意図的に Utilities.sleep(15000)processInvoiceApprovals に挿入翌日 aggregatePerfLogDailyMAS233_PERF_ALERT_P95_MS=10000 超過を検知 → WARN + メール送信
連日 sleep を維持2 日目以降は perf_alert_last_sent_<label> で 24h ロックされメール送信されない

4. ローテーション(90 日経過)動作

dev で MAS233_PERF_RETENTION_DAYS=1 に設定 → 翌日 aggregatePerfLogDaily 実行 → 99_perf_log_archive_YYYYMM に前日分が移送されることを確認。

5. 並行実行ロック検証

dev で同時に processInvoiceApprovals を 3 並列実行(手動・複数タブで RPAService.processApprovals() 連打)→ 全実行が 99_perf_log に記録され、1 つも欠落していないことを確認。

6. failure_patterns 既知パターン回帰

パターン検証方法
#25 並列対称性grep -nE "Utils\.perfStart" 400_domain/ 600_report/ の件数 = perfEnd の件数
#28 末尾 _ privategrep -nE "google\.script\.run\.\w+_" webapp_client/ で 0 件
#29 Infinity 漏れP50 / P95 計算後の値が全て Number.isFinite() を通る

関連ドキュメント

仕様書 / ファイル関連箇所・参照理由
MAS-231 GAS パフォーマンス最適化パス 1本案件の 直接の後続Utils.perfStart / perfEnd API + 99_perf_log を MAS-231 が利用して効果測定。MAS-233 計測 ↔ MAS-231 改善 のループ運用
MAS-179 監査証跡 (98_audit_log)DDL 登録パターン (LOG_AUDIT) + シート保護 (applyAuditLogProtection_) + 月次アーカイブ (archiveAuditLogMonthly_) のテンプレート参照。99_perf_log も同パターンで実装
MAS-201 シートバックアップ800 番台のメンテナンススクリプト構成・トリガー登録冪等性パターン (ScriptApp.getProjectTriggers() 既存確認) の参考
MAS-178 エラーハンドリング (99_error_log)Utils.persistLog の利用パターン参照。99_perf_log99_error_log の責務分離(前者は性能、後者は障害)
MAS-232 サイドバー SPA 化google.script.run ラウンドトリップ件数のクライアント側計測の協調点。本案件 v1 はサーバー側のみ・クライアント側は MAS-232 と並行検討
failure_patterns.md#18-#20 (命名造語) / #25 (並列対称性) / #26 (oauthScopes) / #28 (末尾 _ private) / #29 (Infinity null) を直接参照
CLAUDE.md800 番台「マイグレーションスクリプト運用ガイドライン」+ Utils 名前空間ルール
docs/_internal/dev_spec_prompt_template.md14 セクション必須・分割 Step 実行原則
docs/prd.mdHuman-in-the-Loop ポリシー(アラート確認 → 人間対策の流れ)

人間が検討すべき事項

  1. 計測ラベルの粒度: v1 は 関数単位 で実装。処理ブロック単位(例: actionA.fetchInvoices / actionA.buildJournals / actionA.writeJournals の 3 段階)への細粒度化は MAS-231 着手後の 2 週間データで上位 5 ボトルネックが特定できなかった場合の v2 で再検討。
  2. CacheService 書込頻度: バッファリング(60 秒に 1 回)が default。毎回書込は採用しない(オーバーヘッド > 計測価値)。flush 間隔の最適値(30 秒 vs 60 秒 vs 120 秒)は実運用 2 週間後にチューニング。
  3. 99_perf_log 保持期間: 90 日で確定。無期限保持は採用しない(Sheets 1000 万セル制約 + 編集時の重さ)。長期トレンド分析が必要なら BigQuery エクスポート(MAS-237 GCP 移行と連動)を v2 で検討。
  4. Looker Studio 連携: v1 では未実装。シート内スパークライン + 条件付き書式で十分。Looker Studio が必要になれば Sheets 直結(GAS 不要)で接続可能。
  5. ユーザー操作計測(クライアント側タイマー): v1 ではサーバー側のみ。google.script.run ラウンドトリップ計測は MAS-232 SPA 化と並行検討(クライアント側 performance.now() で送信時刻 → サーバー側 perfEnd の差分でレイテンシ推定)。
  6. アラート閾値の妥当性: MAS233_PERF_ALERT_P95_MS=10000(10 秒)が default。実運用 2 週間で false positive が多ければ調整。閾値ゼロでも 24h ロックでスパムは防止される。
  7. メール通知 vs Slack 通知: v1 はメール(MailApp.sendEmail + Session.getEffectiveUser)。Slack Webhook は MAS-186 監視通知と統合検討。
  8. ボトルネック上位 5 → MAS-231 対策のループ運用: 2 週間データ蓄積 → aggregatePerfLogDaily の P95 ranking で上位 5 を特定 → MAS-231 Step ごとに対策 → 再計測。運用フロー (docs/ops/perf_loop.md) のドキュメント化が必要か 検討。
  9. SpreadsheetApp.flush() 静的検出の自動化: scripts/grep-flush.sh を CI(.github/workflows/ci.yml)に組込むか、scripts/pre-push-check.sh に追加するか。MAS-224 GitHub Actions と連動。
  10. Jr 採用後のハンズオン教材: 本計測ツールは「最適化が効いたかを数値で確認できる」ため Jr ハンズオン適合性が高い。docs/ops/perf_handbook.md への組み入れ要否を検討(MAS-230 Jr 採用要件と連動)。
  11. テストランナー (901_test_runner.js) への組込: 計測 ON/OFF の切替テスト(オーバーヘッド < 5% の自動回帰)を追加するか。CLAUDE.md「変更時の動作確認テスト」表に 400_domain/410_subledger_engine.js 変更時の必須テストとして追加候補。
  12. prod デプロイ時の計測 OFF 運用: v1 は prod でも default ONMAS233_PERF_ENABLED=true)。オーバーヘッド < 5% で実運用問題なし想定。問題があれば 03_sys_params で OFF に即切替可能(コード変更不要)。
  13. 計測対象の Constants.getParam('TAX_RATES') 等の高頻度呼出関数: マスタ取得系は MAS-231 のキャッシュ層で最適化対象のため、本案件で計測埋込すれば Before/After の数値が直接エビデンスになる。優先計装候補。
  14. Phase 別の段階リリース: Step 1 (Utils.perfStart/End) → Step 2 (Action A/B 埋込) → Step 3 (99_perf_log DDL) → Step 4 (集計) → Step 5 (可視化 + アラート) → Step 6 (メニュー) を 独立 PR 化 するか、まとめて 1 PR か。spec 推奨は段階 PR(rollback 容易性)。

実装プロンプト(Claude Code 用)

Claude Sonnet 4.6 推奨(Step 1 計測ヘルパー実装は Opus 4.7 推奨・名前空間設計判断あり)。

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-233「GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)」を実装してください。

## 実行前タスク

必ず以下を Read し、固有名詞を実在のものに揃えてから着手すること:

1. `000_infra/004_utils.js` — `Utils` 名前空間 (`logInfo` L494 / `logError` L504 / `persistLog` L577 / `auditLog` L539 / `getSheetByKey` L302) のシグネチャを確認
2. `000_infra/002_constants.js` — `Constants.getParam` (L205) の `_paramsCache` 機構と `MENU_DEFINITION` のカテゴリ命名規則 (L266-398)
3. `100_config/101_sys_config.js` — `LOG_AUDIT` の DDL 登録 (L884) + `schemas.LOG_AUDIT` 定義 (L1047) + `archiveAuditLogMonthly_` 月次ローテーション (L1749 付近)
4. `400_domain/410_subledger_engine.js` — `processInvoiceApprovals` (L103) / `processSettlementClearings` (L361) のエントリ
5. `400_domain/407_rpa_orchestrator.js` — `RPAService.*` のエントリ
6. `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js` — マート更新のエントリ
7. `300_ui/301_ui_assist.js` — サイドバー初期化エントリ
8. `500_import/502_receipt_reader.js` — OCR 処理エントリ
9. `docs/_internal/failure_patterns.md` — #18-#20 / #25 / #26 / #28 / #29 を本実装で必ず遵守
10. `CLAUDE.md` — 800 番台「マイグレーションスクリプト運用ガイドライン」 + `Utils` 名前空間ルール

## 修正対象ファイル

- `000_infra/004_utils.js`(`Utils.perfStart` / `perfEnd` / `perfFlush` を追記・約 80 行)
- `000_infra/002_constants.js`(`MENU_DEFINITION` に「📊 パフォーマンス診断」カテゴリ追加)
- `100_config/101_sys_config.js`(`schemas.LOG_PERF` 追加 + `existKeys.includes('LOG_PERF')` ガード)
- `400_domain/410_subledger_engine.js`(Action A/B 各エントリに `try/finally` で `perfStart` / `perfEnd` ラップ)
- `400_domain/407_rpa_orchestrator.js`(同上)
- `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js`(同上)
- `300_ui/301_ui_assist.js`(同上)
- `500_import/502_receipt_reader.js`(同上)
- `800_ops/810_perf_aggregator.js`(**新規**・約 150 行・`aggregatePerfLogDaily` / `clearPerfLog` / `openPerfLogSheet` / `_perfRotateOldLogs_`)

## 実装内容(Step 1-6)

### Step 1: `Utils.perfStart` / `perfEnd` / `perfFlush` 追加(004_utils.js)

- `_perfEnabled_()` ヘルパー: `Constants.getParam('MAS233_PERF_ENABLED', 'true')` → boolean
- `_perfBuffer_()` ヘルパー: スクリプトプロパティとは別の in-memory array (関数内 closure)
- `_shouldFlush_()` ヘルパー: バッファ件数 > 200 or 前回 flush から 60 秒経過
- `Utils.perfStart(label)`: 計測 OFF 時 null を返す。token は `{ label, t0: Date.now() }`
- `Utils.perfEnd(token)`: token が null なら no-op。`Date.now() - token.t0` をバッファに append
- `Utils.perfFlush()`: `LockService.getDocumentLock().tryLock(5000)` で並行 flush ガード → バッファを `99_perf_log` に append → CacheService クリア。シート未作成時は自動作成(`auditLog` の自動作成パターン踏襲)

### Step 2: 主要関数への計測埋込(5 系統対称適用)

各エントリ関数を以下の **try/finally パターン**でラップする。**Action A と Action B は完全対称的**に編集する(failure_patterns #25・片方だけ try/finally になる状態を禁止):

    function processInvoiceApprovals() {
      var perf = Utils.perfStart('actionA.processInvoiceApprovals');
      try {
        // 既存ロジック (変更なし)
      } finally {
        Utils.perfEnd(perf);
      }
    }

対象: `processInvoiceApprovals` / `processSettlementClearings` / `RPAService.*` / `runDataMart` 系 / `doGet(e)` / OCR エントリ / `setupAllSchemas`。

### Step 3: DDL に `LOG_PERF` 追加(101_sys_config.js)

- `schemas.LOG_PERF`: `{ headers: ['日時', 'ラベル', '処理件数', '所要時間(ms)', 'P50(ms)', 'P95(ms)', '関数名', '備考'], color: '#434343' }`(`LOG_AUDIT` と並列に配置)
- `existKeys.includes('LOG_PERF')` ガード + `confSheet.appendRow(['LOG_PERF', '', '99_perf_log', 'パフォーマンス計測ログ(操作追跡・90日ローテーション)'])`(`LOG_AUDIT` 直後に対称配置)

### Step 4: 日次集計(800_ops/810_perf_aggregator.js 新規)

- `aggregatePerfLogDaily()`: `99_perf_log` の生ログから当日分をラベル別に集計 → P50 / P95 / 件数 / 総 ms を新行として追記(種別='AGG_DAILY')
- `_perfRotateOldLogs_()`: `MAS233_PERF_RETENTION_DAYS=90` 超のログを `99_perf_log_archive_YYYYMM` に移送(`archiveAuditLogMonthly_` パターン踏襲)
- `clearPerfLog()`: 手動全削除メニュー(confirm ダイアログ必須)
- `openPerfLogSheet()`: `99_perf_log` をアクティブ化
- 時間トリガー登録: `ScriptApp.newTrigger('aggregatePerfLogDaily').timeBased().atHour(2).everyDays(1).create()`(既存トリガー有無を `ScriptApp.getProjectTriggers()` でチェック)

### Step 5: 可視化 + アラート

- `99_perf_log` に条件付き書式: 「赤: P95 > 5000ms / 黄: P95 > 1000ms / 緑: P95 < 1000ms」(`addConditionalFormatRule` で setupAllSchemas 内に登録)
- `MAS233_PERF_ALERT_P95_MS=10000` 超過時: `Utils.persistLog('WARN', ...)` + `MailApp.sendEmail` 通知(`CacheService` の `perf_alert_last_sent_<label>` キーで 24h ロック)

### Step 6: メニュー登録(002_constants.js)

`Constants.MENU_DEFINITION` に以下を追記:

    {
      category: '📋 サイドバー: 📊 パフォーマンス診断',
      items: [
        { label: '📊 99_perf_log を開く', funcName: 'openPerfLogSheet', description: 'パフォーマンス計測ログシートをアクティブ化' },
        { label: '🔄 日次集計を実行', funcName: 'aggregatePerfLogDaily', description: 'P50/P95/件数を集計し 99_perf_log に追記' },
        { label: '🗑️ ログ全削除', funcName: 'clearPerfLog', description: '99_perf_log の全行を削除(要確認)' },
      ]
    }

## 制約

- **`appsscript.json` を一切編集しない**(failure_patterns #26 oauthScopes 部分宣言禁止)
- **末尾 `_` の付与ルール**: `MENU_DEFINITION` 経由で呼ばれる `aggregatePerfLogDaily` / `clearPerfLog` / `openPerfLogSheet` は末尾 `_` 禁止。内部ヘルパー `_perfEnabled_` / `_perfBuffer_` / `_shouldFlush_` / `_perfRotateOldLogs_` は末尾 `_` 必須(failure_patterns #28)
- **新規 namespace 禁止**: `Utils.*` のみに追加(`PerfMonitor` / `Profiler` 等の新規 namespace を勝手に作らない)
- **`Constants.getParam` 経由でパラメータ取得**: `PropertiesService.getScriptProperties()` を直接呼ばない
- **計算結果の値域変更なし**: 既存ロジックの戻り値・I/O は不変(try/finally は例外伝播も保持)
- **回帰テスト < 5%**: 計測 ON/OFF の `processInvoiceApprovals` 100 件実行で実行時間差 < 5% を保証

## 動作確認

1. `npm run push:dev` で dev に push
2. `🔧 開発・設定 → 全シートのスキーマと UI を最新化(DDL)` を実行 → `99_perf_log` シートが新規作成され、ヘッダー 8 列 (`日時 / ラベル / 処理件数 / 所要時間(ms) / P50(ms) / P95(ms) / 関数名 / 備考`) が登録されることを確認
3. `📋 サイドバー: 📒 経理業務 → Action A 実行` → `99_perf_log` に `actionA.processInvoiceApprovals` 行が記録されることを確認
4. Action B 実行 → 対称的に `actionB.processSettlementClearings` 行が記録されることを確認(failure_patterns #25 対称性確認)
5. 60 秒後(または手動で `Utils.perfFlush()` 実行)→ バッファが `99_perf_log` に flush されることを確認
6. `🔄 日次集計を実行` → 当日分の集計行(種別='AGG_DAILY')が追加され、P50/P95 が計算されることを確認
7. `MAS233_PERF_ALERT_P95_MS=100` (低閾値) で実行 → メール通知発火を確認 / 翌実行で 24h ロックで再発火しないことを確認
8. `MAS233_PERF_ENABLED=false` に切替 → `Utils.perfStart` が null を返し、計測が完全停止することを確認
9. `grep -nE "Utils\\.perfStart" 400_domain/ 600_report/` の件数 = `perfEnd` の件数 を確認(並列対称性)
10. dev で 1 週間運用 → `99_perf_log_archive_YYYYMM` への移送(`MAS233_PERF_RETENTION_DAYS=1` で実機確認)

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| Step 1 計測ヘルパー設計 | あり | `_perfBuffer_` の closure / Lock / Cache 書込頻度の選定 |
| Step 2 5 系統への対称埋込 | なし | パターン適用のみ・各エントリで try/finally コピー |
| Step 3 DDL 登録 | なし | `LOG_AUDIT` パターン踏襲のみ |
| Step 4 集計 + ローテーション | あり | P50/P95 計算・空配列ガード・月次アーカイブ |
| Step 5 可視化 + アラート | なし | `addConditionalFormatRule` パターン適用 |
| Step 6 メニュー登録 | なし | `MENU_DEFINITION` 追記のみ |

推奨実行モデル

Phase推奨モデル根拠
Step 1 計測ヘルパー実装Claude Opus 4.7 (1M context)Utils 名前空間設計判断 + Lock / Cache / Buffer の整合性確保
Step 2 主要関数への対称埋込Claude Sonnet 4.6パターン適用・5 系統 × 各 1-3 関数の機械的書換
Step 3 DDL 登録Claude Haiku 4.5LOG_AUDIT パターンのコピー + 文字列置換のみ
Step 4 集計 + ローテーションClaude Sonnet 4.6P50/P95 計算 + 月次アーカイブ・既存 archiveAuditLogMonthly_ パターン適用
Step 5 可視化 + アラートClaude Sonnet 4.6条件付き書式 + メール通知の組合せ・スパムロック設計
Step 6 メニュー登録Claude Haiku 4.5MENU_DEFINITION への追記のみ
仕様書レビューGemini 3 Pro Preview + Deep Think計測ツールの設計妥当性を第三者検証

変更履歴

日時バージョン変更内容
2026-04-30v0.1(仕様書初版)初版作成。MAS-233 (旧 N-57) の TODO_future.md 案件定義 + MAS-231 仕様書 (対案件) を統合し、Claude Opus 4.7 (1M context) で 14 セクション全網羅起草。主要設計判断: (a) Utils.perfStart / Utils.perfEnd / Utils.perfFlush004_utils.js に追加 (新規 namespace 禁止) / (b) ラベル粒度は関数単位 (v2 で処理ブロック単位検討) / (c) CacheService 書込はバッファリング(60 秒・200 件)/ (d) 保持期間 90 日 (98_audit_log の月次アーカイブパターン踏襲) / (e) Looker Studio v1 未連携 (Sheets 直結で十分) / (f) クライアント側計測は MAS-232 SPA 化と並行検討。failure_patterns 反映: #18-#20 (命名造語禁止・Utils.perfStart のみ実装) / #25 (Action A/B 完全対称的に try/finally ラップ) / #26 (oauthScopes 編集禁止) / #28 (末尾 _ private 規約) / #29 (P50/P95 で -Infinity 回避)。実装規模: 004_utils.js +80 行 / 810_perf_aggregator.js 新規 150 行 / 5 系統 × 各エントリに 4 行 try/finally / 工数 1.5-2 週間。実装着手可能状態 (MAS-231 と並行実装で OK・API シグネチャは spec で fix)。

仕様書作成プロンプト

本仕様書を生成する際に Claude Opus 4.7 に投入したプロンプト全文(再現性・監査性のため記録)
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者兼仕様書ライターです。

## タスク

案件 ID **MAS-233**「GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)」の開発仕様書を作成し、`/Users/ts_kuma/projects/my-gas-project-doc/docs/dev/dev_mas-233_perf_diagnostic.md` に保存してください。

## 案件概要 (todo_master_tables.md)

- **Phase**: P1 / **★**: ★★ / **Layer**: 💾 Data
- **目的**: 「何が遅いのか」を数値で把握するための計測基盤。MAS-231 最適化パス 1 の着手前に必須
- **実装スコープ**:
  - **計測ヘルパー関数**: `Utils.perfStart(label)` / `Utils.perfEnd(label)` を `004_utils.js` に追加。ラベル別に `console.time` + `CacheService` に累積記録 (1 実行の呼出回数と総経過時間を保存)
  - **主要関数への埋込**: Action A/B、マート更新、RPA 起票、サイドバー初期化、OCR 処理の各エントリに自動挿入
  - **レポートシート**: `99_perf_log` を DDL に追加し、`関数名 / 実行日時 / 処理件数 / 所要時間(ms) / P50 / P95` を日次で集計
  - **可視化**: Looker Studio or シート内スパークライン / 条件付き書式 「赤:>5秒 / 黄:>1秒 / 緑:<1秒」
  - **ボトルネック検知ルール**: (a) 同一関数が週平均 P95 > 10 秒なら自動アラート、(b) `SpreadsheetApp.flush()` 呼出箇所の特定、(c) `google.script.run` ラウンドトリップ件数計測
  - **着手順序**: 計測埋込 → 2 週間データ蓄積 → 上位 5 ボトルネック特定 → MAS-231 で対策実装のループ
- **MAS-231 (パフォーマンス最適化) との関係**: MAS-233 が計測 → MAS-231 が対策のループ運用

## 検討事項

1. 計測ラベルの粒度 (関数単位 / 処理ブロック単位)
2. CacheService 書込頻度 (毎回 vs バッファリング)
3. 99_perf_log 保持期間 (90 日 / 無期限)
4. Looker Studio 連携の要否
5. ユーザー操作計測 (サイドバーボタン押下〜応答) の捕捉方法 (クライアント側タイマー埋込)

## 必読ファイル (調査して固有名詞を確定)

実装の前に以下のファイルを `Read` で参照し、関数名・列名・定数名を確実に存在するものに揃えてください:

1. `/Users/ts_kuma/projects/my-gas-project-doc/docs/_internal/dev_spec_prompt_template.md` — 仕様書 14 セクションテンプレート
2. `/Users/ts_kuma/projects/my-gas-project-doc/docs/_internal/failure_patterns.md` — #18-#31 (失敗パターン)
3. `/Users/ts_kuma/projects/my-gas-project-doc/000_infra/004_utils.js` — `Utils` 名前空間の現行関数群 (logInfo / logError 等の周辺ロジック)
4. `/Users/ts_kuma/projects/my-gas-project-doc/000_infra/002_constants.js` — `Constants` の `_paramsCache` 機構と CacheService 利用パターン
5. `/Users/ts_kuma/projects/my-gas-project-doc/100_config/101_sys_config.js` — DDL 定義パターン (新シート `99_perf_log` 追加用の参考)
6. `/Users/ts_kuma/projects/my-gas-project-doc/400_domain/410_subledger_engine.js` — Action A/B のエントリポイント特定
7. `/Users/ts_kuma/projects/my-gas-project-doc/CLAUDE.md` — コーディング規約・名前空間ルール
8. 参考: `/Users/ts_kuma/projects/my-gas-project-doc/docs/dev/dev_mas-231_gas_perf_optimization.md` — 対の仕様書 (同フォーマット・MAS-231 と相互参照)

## 仕様書フォーマット要件 (14 セクション)

`dev_spec_prompt_template.md` 準拠。以下を必ず含める:
- フロントマター (id: MAS-233, aliases: ["N-58", "N-58a"] が候補 — id_mapping_table.csv で確認、type: Task, status: Draft)
- 概要テーブル (案件 ID / カテゴリ / Phase / 優先度 / 所要時間 / 前提案件 / 後続案件 [MAS-231])
- 目的 / 修正方針 / 実装スコープ / Step 分割
- failure_patterns 参照
  - #18-#20 命名造語: `Utils.perfStart` / `Utils.perfEnd` 以外の架空関数を作らない
  - #25 並列実装対称性: 計測埋込は Action A と Action B 両方に対称的に追加
  - #26 oauthScopes: 部分宣言禁止
- エッジケース表 (10 件以上)
- 人間が検討すべき事項 (10 件以上)
- 推奨実行モデル (Step ごとに Haiku/Sonnet/Opus)
- 関連ドキュメント (MAS-231 / MAS-179 監査証跡 / MAS-201 バックアップとのクロスリンク)
- 開発プロンプト (Claude Code 向け実装指示)
- 変更履歴
- 末尾に `<details>` プロンプト全文記録

## 必ず守る規約

- 名前空間: `Utils` (既存) への関数追加。新規 namespace を勝手に作らない
- DDL シート名: `99_perf_log` (DDL 管理外も検討 — 動的生成タブとして扱う方が安全か検討)
- CacheService 利用: `CacheService.getScriptCache()` パターンを既存コード (`Constants.getParam` 等) から踏襲
- failure_patterns #18-#20 — 推測で関数名を作らず Read で実在確認

## アウトプット

ファイル: `docs/dev/dev_mas-233_perf_diagnostic.md`

Step 分割で順次 `Write` → `Edit`:
1. Step 2-1: 骨格 Write (フロントマター + 14 H2 見出し)
2. Step 2-2: 概要〜注意事項 Edit
3. Step 2-3a: エッジケース〜人間検討事項 Edit
4. Step 2-3b: 開発プロンプト〜変更履歴 Edit
5. Step 2-4: `<details>` プロンプト全文記録 Edit

最終確認として `wc -l` で行数を測定し報告してください (期待: 350-500 行)。

起票時の補足:

  • aliases は id_mapping_table.csv で実在確認した結果 ["N-057", "N-57"] を採用(プロンプト原文の N-58 は誤り。N-58 は MAS-234「セキュリティ基準準拠ロードマップ」として既使用)。

  • DDL シート 99_perf_logDDL 管理対象LOG_PERF キー)として実装。98_audit_log の運用パターン(保護なし・月次アーカイブあり)を踏襲。動的生成タブ案は採用せず(MAS-179 監査ログとの責務分離・整合性の観点から DDL 管理を選択)。

  • 関連コマンド:

    # Gemini Deep Think で代替案再生成する場合
    TARGET_ID=MAS-233 node scripts/gen_gemini_alt_oneshot.js
    
    # Gemini レビュー実行
    SPEC_FILES=docs/dev/dev_mas-233_perf_diagnostic.md node scripts/4_review_specs_by_gemini.js