再計算エンジンの檻を外せ:`Application.Calculation`によるスケジュール計算エンジンの非同期制御
レガシーなProject VBAやExcelベースの工程管理システムにおいて、数千行を超えるWBS(Work Breakdown Structure)や複雑な依存関係を持つスケジュールデータを扱うとき、開発者を絶望の淵に追い込むものがある。それが「過剰な自動再計算」である。
ASAP(できる限り早く)やALAP(できる限り遅く)、先行・後続タスクの制約条件をプログラムから一括流し込み、あるいは動的に書き換える際、標準設定のままでは1つのプロパティを変更するたびに計算エンジンが全タスクのトポロジカルソートとクリティカルパスの再評価を走らせる。
$O(N^2)$、あるいはそれに匹敵する重厚な演算処理がループの回数だけ発生する。この「無駄な同期的計算」を断ち切り、再計算エンジンを完全に手懐けることこそ、シニアエンジニアとアマチュアを分かつ境界線である。
今回は、Project VBAにおける計算エンジンのライフサイクルを完全に掌握し、極限のパフォーマンスを引き出すアーキテクチャを解説する。
—
1. 再計算エンジンの内部挙動とパフォーマンスの罠
Microsoft Project(およびExcel等の表計算エンジン)は、データモデルに対する変更検知と同時に、依存関係グラフを辿って再計算を実行する「イベント駆動型の同期エンジン」を内蔵している。
[VBAからプロパティ変更]
↓
[変更イベント発火]
↓
[依存グラフの全探索 & スケジュール再計算] ← ※これをループの都度行うため破滅的な重さになる
↓
[UIの再描画]
数千件のタスクに対し、開始日や制約タイプ(`pjConstraintStartAsSoonAsPossible`など)をループで書き換える場合、このサイクルが毎回実行される。結果として、CPUコアは100%張り付き、数分から数十分のフリーズ地獄を生み出す。
これを防ぐためのアプローチが、「計算エンジンの手動化(Manual Calculation)」および「イベント・画面描画の完全抑制」である。
—
2. 実装アーキテクチャ:計算エンジン制御クラスの設計
場当たり的に `Application.Calculation = False` を書くだけのコードは、例外発生時に計算が無効化されたまま残る「ゾンビ状態」を引き起こすため、プロのコードとは言えない。RAII(Resource Acquisition Is Initialization)の概念をVBAのクラスモジュール(`CAppController`等)に落とし込み、スコープを抜けた瞬間に確実かつ安全に元の状態へ復元する堅牢なアーキテクチャを構築する。
実装コード:`CAppController.cls`(クラスモジュール)
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “CAppController”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
‘ 状態退避用のプライベート変数
Private m_ScreenUpdating As Boolean
Private m_Calculation As Long
Private m_EventState As Boolean
Private m_IsInitialized As Boolean
Private Sub Class_Initialize()
On Error GoTo ErrorHandler
‘ 現在の環境状態を退避
m_ScreenUpdating = App.ScreenUpdating
m_Calculation = Application.Calculation
m_EventState = Application.EnableEvents
‘ 極限のパフォーマンスチューニングのための環境沈黙化
App.ScreenUpdating = False
Application.Calculation = pjManual ‘ 手動計算モードへ移行
Application.EnableEvents = False
m_IsInitialized = True
Exit Sub
ErrorHandler:
m_IsInitialized = False
Err.Raise Err.Number, “CAppController_Init”, “アプリケーション状態の退避に失敗しました: ” & Err.Description
End Sub
Private Sub Class_Terminate()
‘ クラス破棄時に必ず元の状態へ復元(例外安全の担保)
If m_IsInitialized Then
On Error Resume Next
Application.EnableEvents = m_EventState
Application.Calculation = m_Calculation
App.ScreenUpdating = m_ScreenUpdating
On Error GoTo 0
End If
End Sub
‘ 明示的な一括再計算の強制実行メソッド
Public Sub ForceCalculate()
Dim currentCalc As Long
currentCalc = Application.Calculation
‘ 一時的に自動計算に戻して計算を実行
Application.Calculation = pjAutomatic
Application.Calculate
‘ 再び手動に戻す(必要に応じて)
Application.Calculation = currentCalc
End Sub
—
3. 実践:大量制約変更バッチにおける非同期制御の適用
先ほど作成した制御クラスを実際のスケジュール更新バッチから呼び出す。ここでは、すべてのタスクの制約をASAPに一括変更し、最後に一度だけエンジンを走らせるユースケースを想定する。
実装コード:`ModuleBatch.bas`(標準モジュール)
Option Explicit
Sub ExecuteBatchScheduleOptimization()
Dim ctrl As CAppController
Dim t As Task
Dim startTime As Double
startTime = Timer
‘ 制御クラスのインスタンス化(ここで自動計算OFF、画面描画OFFが走る)
Set ctrl = New CAppController
On Error GoTo ErrorHandler
Debug.Print “— バッチ処理開始: ” & Format(Now, “hh:nn:ss”) & ” —”
‘ 大量データの高速ループ処理
‘ Projectではアクティブプロジェクトのタスク群を走査
For Each t Is ActiveProject.Tasks
If Not t Is Nothing Then
‘ サマリータスク以外を対象とする等のビジネスロジック
If Not t.Summary Then
‘ プロパティの書換(この時点では計算エンジンは一切走らない)
t.ConstraintType = pjConstraintStartAsSoonAsPossible
‘ 必要に応じカスタムフィールドの書き込み等
t.Text1 = “Optimized”
End If
End If
Next t
‘ ループ終了後、明示的に一度だけ再計算を実行
Debug.Print “制約条件の適用完了。一括再計算を実行します…”
ctrl.ForceCalculate
Debug.Print “— バッチ処理正常終了 処理時間: ” & Format(Timer – startTime, “0.00秒”) & ” —”
CleanUp:
‘ 明示的なオブジェクト解放
Set ctrl = Nothing
Exit Sub
ErrorHandler:
MsgBox “バッチ処理中に重大なエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
4. プロのための深層知見:メモリ最適化とレガシー環境の罠
シニアエンジニアとして、単にコードが動くだけではなく、以下のハードウェア・アーキテクチャ上の制約を考慮し尽くす必要がある。
1. COMオブジェクトの参照リークとガベージコレクション
VBAは裏でCOM(Component Object Model)と密に連携している。`For Each t Is ActiveProject.Tasks` のようなイテレーションにおいて、ラップされたタスクオブジェクトがループごとにヒープ領域に残り続けることがある。
数万件規模のエンタープライズプロジェクトを処理する場合、明示的な変数解放を行わないと、VBAランタイムのメモリリーク(メモリフットプリントの肥大化)を引き起こし、最悪の場合「メモリ不足 (Out of Memory)」でプロセスが強制終了する。
ループ内での重いオブジェクト生成を避け、不要になった参照は速やかに `Set t = Nothing` 等で解放する姿勢が不可欠である。
2. Windows API連携による描画スレッドの完全凍結
`App.ScreenUpdating = False` だけでは、Windowsのメッセージキューに溜まった描画イベントやCOMの割り込みを完全に抑えきれない場合がある。極限のパフォーマンスが求められる極大規模システムでは、親ウィンドウのハンドル(hwnd)に対して `LockWindowUpdate` APIを噛ませることで、OSレベルで描画スレッドを完全にロックダウンする手法も選択肢に入る。
If VBA7 Then
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Else
Private Declare Function LockWindowUpdate Lib “user32” (ByVal hwndLock As Long) As Long
End If
※ただし、Project VBAにおいては `Application.ScreenUpdating` と適切な `Calculation` 制御の組み合わせで十分な効果が得られるため、APIの過剰な介入は保守性を損ねる諸刃の剣であることを忘れてはならない。
3. マルチユーザー環境(Project Server / Project Online)との同期制御
スタンドアロンの `.mpp` ファイルではなく、Server環境に接続されたEnterprise Projectの場合、ローカルで計算エンジンを止めて一括変更を行ったとしても、チェックイン時やサーバー側での制約バリデーション(Global Enterprise Templatesの制約)とのコンフリクトが発生するリスクがある。
バッチ処理の設計においては、ローカルの計算エンジン制御と同時に、サーバー側のトランザクション境界を意識したエラーハンドリング(楽観的排他制御の考慮)が必須となる。
—
総括
`Application.Calculation` の非同期制御は、単なる「処理を速くする小技」ではない。データモデルの整合性を担保しながら、計算エンジンのライフサイクルをエンジニアが完全に支配下に置くための、極めて高度なアーキテクチャパターンである。
リソースの確実な管理、例外安全なクラス設計、そして計算コストの正確な見積もり。これらを網羅したコードベースこそが、レガシーの呪縛からシステムを解放し、真のエンタープライズグレードの安定性をもたらす。
