計算エンジンの沈黙を制御せよ:大規模プロジェクトにおける計算負荷抑制の極致
数万行に及ぶタスク、複雑に絡み合う依存関係(Predecessors)、そして厳格な制約条件(Constraints)。Microsoft Projectという巨大な計算エンジンを扱う際、シニアエンジニアが直面する最大の壁は、コードの論理エラーではない。「計算エンジンの暴走」である。
タスクの制約条件を一つ書き換えるたびに、Projectはプロジェクト全体の依存関係グラフを再走査し、クリティカルパスを再計算しようとする。これを愚直にループ内で実行すれば、処理時間は数分、あるいはアプリケーションのフリーズ(応答なし)へと直結する。
本稿では、計算エンジンを「制御」下に置くための、`Calculation.ManualCalculation`を活用した高度な制御手法、およびメモリとリソースを極限まで最適化するアーキテクチャについて詳述する。
1. 依存関係グラフの計算コスト:なぜ「数分」かかるのか
MS Projectの計算エンジンは、タスク間の依存関係を「有向非巡回グラフ(DAG)」として保持している。
制約条件(ASAP/ALAP、Must Start On等)の変更は、グラフのノードにおける属性変更ではなく、グラフ全体のトポロジカルソートと、それに伴う日付の伝播(Propagation)を誘発する。
$N$個のタスクに対し、各タスクの属性を更新する際、デフォルトの `pjAutomatic` モードでは、更新のたびに $O(N + E)$ ($E$はエッジ数)の計算コストが、更新回数分だけ累積される。大規模プロジェクトでは、これが指数関数的な遅延を生む原因となる。
我々が目指すべきは、「すべての変更をメモリ上のグラフに蓄積し、最後に一度だけ計算エンジンを叩き起こす」という、バッチ処理的アプローチである。
2. 制御アーキテクチャ:State Guard パターン
単に `Application.Calculation = pjManual` と記述するだけでは、プロフェッショナルの仕事とは言えない。エラー発生時に計算モードが `Manual` のまま放置されれば、ユーザーは「なぜ日付が変わらないのか」という混乱に陥り、システムは死に至る。
堅牢な実装には、現在の状態を退避し、例外発生時にも必ず元の状態、あるいは適切な計算状態へ復帰させる「State Guard パターン」が不可欠である。
実装における黄金律
1. ScreenUpdatingの停止: UI描画によるオーバーヘッドを排除する。
2. CalculationのManual化: エンジンの自動再計算を封印する。
3. Windows APIによるUIロック: Windowsレベルでの描画更新を抑制し、プロセスの優先度を計算に集中させる。
4. 明示的なオブジェクト解放: COMオブジェクトの参照カウントを管理し、メモリリークを防ぐ。
3. 極限の最適化コード:実装例
以下に、大規模データの制約条件一括更新を行うための、実戦投入可能なクラスモジュール・レベルの設計思想を示す。
Option Explicit
' Windows APIの宣言: UIのフリッカー防止とプロセス制御
If VBA7 Then
Private Declare PtrSafe Function LockWindowUpdate Lib "user32" (ByVal hwndLock As LongPtr) As Long
Private Declare PtrSafe Function GetActiveWindow Lib "user32" () As LongPtr
Else
Private Declare Function LockWindowUpdate Lib "user32" (ByVal hwndLock As Long) As Long
Private Declare Function GetActiveWindow Lib "user32" () As Long
End If
''' <summary>
''' 大規模データ更新用プロセッサ
''' </summary>
Public Sub BulkUpdateConstraints(ByVal targetTaskIDs As Variant)
Dim projApp As MSProject.Application
Set projApp = MSProject.Application
Dim currentCalcMode As MSProject.Calculation
Dim currentScreenMode As Boolean
Dim hwnd As LongPtr
' 1. 現在の状態をキャプチャ(State Guard)
currentCalcMode = projApp.Calculation
currentScreenMode = projApp.ScreenUpdating
hwnd = GetActiveWindow()
On Error GoTo ErrorHandler
' 2. エンジンとUIの封印
' LockWindowUpdateにより、OSレベルでウィンドウの再描画を抑制する
LockWindowUpdate hwnd
projApp.ScreenUpdating = False
projApp.Calculation = pjManual
' 3. メイン処理(計算エンジンを眠らせたままの高速ループ)
Dim i As Long
Dim t As MSProject.Task
' 処理中、ユーザーに「計算中」であることを示すための最低限の制御
' DoEventsを多用すると計算速度が落ちるため、ループの閾値ごとに実行する
For i = LBound(targetTaskIDs) To UBound(targetTaskIDs)
' タスクオブジェクトへのアクセス
' ※大規模データの場合、Selectメソッド等は絶対に使用しないこと
Set t = projApp.ActiveProject.Tasks(targetTaskIDs(i))
If Not t Is Nothing Then
' 制約条件の更新(例: ASAPからMust Start Onへの変更)
' ここでの変更は、グラフのメモリ上の値を変えるだけで、再計算は走らない
t.ConstraintType = pjMustStartOn
t.ConstraintDate = DateAdd("d", i, Date)
End If
' 1000件ごとにOSに制御を戻し、メモリの断片化と応答性をケアする
If i Mod 1000 = 0 Then
DoEvents
End If
Next i
' 4. 変更の確定(ここで初めて計算エンジンを一気に動かす)
' 全ての変更が完了した後に、一度だけAutomaticに戻す、あるいはCalculateを呼ぶ
projApp.Calculation = pjAutomatic
CleanUp:
' 5. 状態の復元(State Restoration)
LockWindowUpdate 0 ' 解除
projApp.ScreenUpdating = currentScreenMode
projApp.Calculation = currentCalcMode
' COMオブジェクトの明示的解放
Set t = Nothing
Set projApp = Nothing
Exit Sub
ErrorHandler:
MsgBox "Critical Error in Engine Control: " & Err.Description, vbCritical
Resume CleanUp
End Sub
4. アーキテクトの視点:メモリとCOMの深淵
オブジェクトのライフサイクル管理
上記のコードで `Set t = Nothing` を明示しているのは、単なる作法ではない。MS ProjectのCOMインターフェースは、参照カウントが残っている限り、メモリ上にタスクのプロパティを保持し続ける。数万回のループ内でオブジェクトを適切に解放(あるいは再利用)しなければ、VBAのスタック領域ではなく、プロセス全体のヒープ領域を食いつぶし、最終的に `Out of Memory` を引き起こす。
レガシー環境への配慮
古いバージョンのWindowsや、レガシーなアドインが共存する環境では、`LockWindowUpdate` が他のアプリケーションの描画に干渉するリスクが稀にある。しかし、エンタープライズ用途の自動化においては、そのリスクを冒してでも「計算の原子性(Atomicity)」を確保することが、システム全体の整合性を保つ上で優先される。
システム間連携における注意点
もしこのVBAが、外部の .NET アプリケーションや Python スクリプトから COM 経由で制御されている場合、`Calculation = pjManual` の状態での呼び出しは、外部からの「データの不一致」を招く。
外部システムが値を読み取る際は、必ず `projApp.Calculate` が完了したことを確認するシグナル(またはプロパティの整合性チェック)を設計に組み込むべきである。
結論
大規模プロジェクトの制御において、エンジニアの資質は「いかに速く書くか」ではなく、「いかにエンジンの挙動を予測し、その暴走を抑え込むか」に現れる。
`Calculation.ManualCalculation` は、単なる高速化のスイッチではない。それは、複雑な依存関係の海を渡るための、エンジニアが手にする「舵」である。この舵を正確に操ることで、数分を要した苦痛の処理を、数秒の静謐なプロセスへと昇華させることができるのだ。
