【テクニカル・上級編】制約条件の「強制解除」によるスケジュール最適化:VBAを用いたボトルネック解消 – Project VBA解析バイブル

スポンサーリンク

制約条件の「強制解除」によるスケジュール最適化:VBAを用いたボトルネック解消

Microsoft Project(以下、MS Project)におけるスケジュール計算エンジンは、標準状態では極めて堅牢かつ保守的に動作する。ASAP(可能な限り早く)やALAP(可能な限り遅く)といったスケジューリング方向、そして「MsoConstraintType」に代表されるハード制約・ソフト制約の網の目は、プロジェクトの整合性を保つための防壁である。

しかし、大規模な工程表において、この防壁そのものが「無駄な工期遅延(ボトルネック)の元凶」と化すことは現場のエンジニアであれば誰もが知るところだ。人間が目視でクリティカルパス上の制約を見つけ出し、一つずつ手動で緩和していく作業は、非効率なだけでなくヒューマンエラーの温床となる。

本稿では、Project VBAのオブジェクトモデルの深部をハックし、メモリ消費とCOMコンポーネントのライフサイクルを極限までコントロールしながら、「ボトルネックとなっている制約条件を動的に特定し、自動解除・緩和してシミュレーションを行う高速エンジン」の実装手法を解説する。

—

1. 現代のProject VBA開発におけるアーキテクチャ的懸念とメモリ管理

レガシーなVBA環境において、MS ProjectのCOMオブジェクト操作は「遅い」「不安定」の代名詞として扱われがちだ。その原因の多くは、VBE(Visual Basic Environment)の暗黙的な参照解決と、COMラッパーの不適切な解放タイミングにある。

特にスケジュール計算エンジンは、タスクやリソースのプロパティ(例:`Task.ConstraintType`や`Task.Start`)にアクセスするたびに、裏側でマーシャリングが発生する。数千件規模のタスクを持つプロジェクトに対し、最適化ループの中で不適切なオブジェクト参照を残し続けると、ExcelやProjectのプロセス空間でメモリリークが発生し、最悪の場合はCOM例外(エラー 80010108: オブジェクトでなくなりつつあるオブジェクトのメソッド呼び出しです)を引き起こす。

これを防ぐための鉄則は以下の2点だ。
1. ループ内でのドットつなぎ(例:`ActiveProject.Tasks(i).Constraint.Type`)の排除:必要なオブジェクトは明示的な変数に一度キャッシュし、処理後は即座に `Nothing` を代入する。
2. 計算エンジンのバッチ制御:タスクのプロパティを変更する都度プロジェクト全体が再計算されるのを防ぐため、必要に応じて計算モードを制御する。

—

2. ボトルネック特定と制約「強制解除」シミュレーションの設計

今回構築するシミュレータのアルゴリズムは以下のステップで動作する。

1. ベースラインの工期取得:現在のプロジェクト完了日(`Project.Finish`)を記録する。
2. 制約の評価とスコアリング:各タスクの `ConstraintType` を走査し、それがクリティカルパス上の「不可侵な制約(例:MSOT_MUST_FINISH_ON 等)」であるかを判定する。
3. 仮想解除(What-If分析):一時的に制約を `ASAP` または `MSO_AS_SOON_AS_POSSIBLE` に緩め、COM上で一時的な再計算を実行。
4. 工期短縮効果の測定:解除前後の完了日の差分を算出し、最も工期短縮インパクトが大きい制約を特定する。
5. 最適化の適用(またはロールバック):シミュレーション結果に基づき、ボトルネックとなっている制約を自動的に緩和する。

—

3. 実装コード:制約自動緩和・最適化エンジン

以下のコードは、Project VBAの限界性能を引き出すために最適化された実用モジュールである。エラーハンドリングと、COMオブジェクトの確実な解放(`Set … = Nothing`)を徹底している。

Option Explicit

‘ Windows API: 処理の合間にUIスレッドをブロックせずCPUを適切に解放するためのスリープ(必要に応じた拡張用)
If VBA7 Then
Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If

Public Sub SimulateAndReleaseBottlenecks()
Dim prj As Project
Set prj = ActiveProject

Dim originalFinish As Date
originalFinish = prj.Finish

Dim tsk As Task
Dim targetTask As Task
Dim bestTaskID As Long
Dim minFinish As Date
Dim currentFinish As Date
Dim originalConstraint As Long
Dim originalConstraintDate As Date

Dim maxReduction As Double
Dim reductionDays As Double

maxReduction = 0
bestTaskID = 0
minFinish = originalFinish

MsgBox “制約条件のボトルネック解析シミュレーションを開始します。” & vbCrLf & _
“ベースライン完了日: ” & originalFinish, vbInformation, “ASAP/ALAP Optimizer”

‘ 自動計算を一時オフにして速度を最大化(Project 2010以降)
On Error Resume Next
Application.Calculation = pjManual
On Error GoTo 0

‘ 1. 全タスクを走査し、どの制約を解除すれば最も工期が短縮されるかをシミュレート
Dim i As Long
Dim totalTasks As Long
totalTasks = prj.Tasks.Count

For i = 1 to totalTasks
Set tsk = prj.Tasks(i)

‘ 有効なタスクかつ、サマリータスク・マイルストーンを除外する場合の制御(ここでは実作業タスクを対象)
If Not tsk Is Nothing Then
If Not tsk.Summary And tsk.Type <> pjTaskMilestone Then

‘ ハード制約(Must Finish On, Must Start On など)が設定されているものをターゲット候補とする
If tsk.ConstraintType = pjConstraintMustFinishOn Or _
tsk.ConstraintType = pjConstraintMustStartOn Or _
tsk.ConstraintType = pjConstraintStartNoEarlierThan Then

‘ 現在の状態を退避
originalConstraint = tsk.ConstraintType
originalConstraintDate = tsk.ConstraintDate

‘ 【What-If】一時的に制約を「できる限り早く(ASAP)」に変更
tsk.ConstraintType = pjConstraintAsSoonAsPossible

‘ プロジェクト全体の再計算を強制実行
Calculate

‘ 短縮された完了日を取得
currentFinish = prj.Finish

‘ 工期短縮効果(日単位)を計算
reductionDays = CDbl(originalFinish – currentFinish)

If reductionDays > maxReduction Then
maxReduction = reductionDays
bestTaskID = tsk.ID
minFinish = currentFinish
End If

‘ 状態を元のハード制約にロールバック(シミュレーションのため)
tsk.ConstraintType = originalConstraint
tsk.ConstraintDate = originalConstraintDate
Calculate
End If
End If
End If

‘ メモリ肥大化を防ぐため、ループごとに参照を確実に解放
Set tsk = Nothing
Next i

‘ 計算モードを元に戻す
On Error Resume Next
Application.Calculation = pjAutomatic
Calculate
On Error GoTo 0

‘ 2. 最も効果の高いボトルネックタスクの制約を実際に解除する
If bestTaskID > 0 Then
Set targetTask = prj.Tasks.ItemByID(bestTaskID)
If Not targetTask Is Nothing Then
Dim msg As String
msg = “最大のボトルネックを特定しました。” & vbCrLf & _
“タスク名: ” & targetTask.Name & ” (ID: ” & targetTask.ID & “)” & vbCrLf & _
“予想工期短縮日数: ” & maxReduction & ” 日” & vbCrLf & _
“新・完了日予測: ” & minFinish & vbCrLf & vbCrLf & _
“この制約を自動的に解除(ASAP化)しますか?”

If MsgBox(msg, vbYesNo + vbQuestion, “ボトルネック解消の確認”) = vbYes Then
targetTask.ConstraintType = pjConstraintAsSoonAsPossible
Calculate
MsgBox “制約の解除が完了しました。プロジェクトのスケジュールが最適化されました。”, vbInformation, “最適化完了”
Else
MsgBox “シミュレーションを終了しました。制約は変更されていません。”, vbInformation, “キャンセル”
End If
Set targetTask = Nothing
End If
Else
MsgBox “工期を短縮可能な有効な制約ボトルネックは見つかりませんでした。”, vbInformation, “解析結果”
End If

Set prj = Nothing
End Sub

—

4. エンジニアリング上の解説と運用時の注意点

メモリフットプリントの最小化

上記のコードでは、`For Each` ではなくインデックスベースの `For i = 1 to prj.Tasks.Count` を採用している。大規模なMSPファイルにおいて `For Each` は内部列挙子の生成と破棄コストが高く、メモリリークの温床になりやすい。また、ループの終端で `Set tsk = Nothing` を明示的に実行し、VBAのガベージコレクションに頼らずCOM参照を即座に解放している。

計算エンジン(Calculation)の制御

`Application.Calculation = pjManual` による手動計算モードへの切り替えは、タスクプロパティの書き換えに伴う無駄なスケジュール再計算の連鎖を防ぐために不可欠である。What-Ifシミュレーションのループ内では意図的に `Calculate` を呼び出して差分を測定しているが、このバッチ制御を行わない場合と比較して、処理速度が 最大で数十倍以上 向上する。

実運用におけるリスクヘッジ

ハード制約(Must Finish On等)を機械的にASAPへ変更することは、発注者や上位工程との契約上の納期合意を勝手に破壊するリスクを孕む。そのため、本ツールは単に自動適用するだけでなく、「どのタスクのどの制約がクリティカルパスを圧迫しているか」を定量的に可視化するレポートツールとしても機能する設計にしている。

真のシニアエンジニアであれば、このコードをベースにログ出力機能(CSVへのボトルネック履歴書き出し)や、複数の制約を同時に緩和した複合シミュレーションへの拡張へと昇華させるべきだろう。限界を知る者だけが、レガシーなツールを思い通りに操ることができる。

タイトルとURLをコピーしました