【実務・中級編】【上級者向け】大規模プロジェクトのサブプロジェクト一括更新におけるメモリリーク対策 – Project VBA解析バイブル

スポンサーリンク

【上級者】Project VBA 限界突破:大規模サブプロジェクト一括更新における「メモリリーク防衛網」の構築

開発現場のリーダーであるあなたなら、一度は経験があるはずだ。
数百件に及ぶマスター・サブプロジェクト構造を持つ巨大なスケジュール群。これをVBAで巡回し、進捗やベースラインを一括更新するバッチ処理を書いたはいいが、処理が進むにつれてタスクマネージャーのメモリ消費量が右肩上がりに膨れ上がり、中盤から極端に動作が重くなり、最終的に「メモリ不足(Error 7)」や得体の知れないCOM例外でクラッシュする――。

Microsoft Project(MS Project)のCOMオブジェクトモデルは、極めて強力であると同時に、メモリ管理において極めてデカダンな仕様を持っている。
「VBAだからガベージコレクションが勝手にやってくれるだろう」という甘い認識は、大規模プロジェクトの現場では即座にシステム停止という致命傷に繋がる。

今回は、数万タスク規模のサブプロジェクト群を何時間稼働させようとも、1バイトのメモリリークをも許さない、極限まで研ぎ澄ませたVBAアーキテクチャを伝授する。

なぜ、あなたの書いたループ処理はリークするのか?

多くの開発者は、次のようなコードを書く。

‘ 【アンチパターン】絶対にやってはいけないループ
Dim subPrj As Project
For Each subPrj In ActiveProject.Subprojects
subPrj.Open
‘ 何らかの更新処理
subPrj.Save
subPrj.Close
Next subPrj

一見、美しく簡潔に見えるこのコードこそが、メモリリークの温床だ。
何が起きているのか?

1. 参照の連鎖(Reference Chaining): `ActiveProject.Subprojects` や `subPrj` が保持するCOMオブジェクトの参照が、VBAの実行スタックや内部変数テーブルに残り続ける。
2. 暗黙のインスタンス生成: ループの各反復でMS Projectが裏で生成するドキュメントオブジェクトが、`Close`メソッドを叩いても完全には解放されず、セッション内に残骸(Zombie Object)を残す。
3. グローバルスコープの汚染: `Application` や `ActiveProject` などの暗黙の参照を多用すると、VBAのCOMマリオネットの糸が絡まり、プロセス終了までメモリが解放されない。

これを回避するためには、「オブジェクト変数の完全な明示的解放(`Nothing`代入)」「エラーハンドリングを伴うスコープの隔離」が不可欠となる。

堅牢なサブプロジェクト一括更新アーキテクチャ

大規模バッチ処理を安定稼働させるための設計原則は以下の3点だ。

1. 暗黙的参照の排除: `ActiveProject` や `ActiveWindow` をループ内で極力使わず、オブジェクト変数に明示的にバインドして操作する。
2. 参照の完全破棄(Kill Reference): オブジェクトの利用が終わったら、即座に `Set xxx = Nothing` を実行し、VBAの参照カウンタを確実にデクリメントする。
3. 画面描画とイベントの完全抑制: バッチ処理中の画面描画(ScreenUpdating)や警告表示(DisplayAlerts)をオフにし、COMのオーバヘッドを極限まで削る。

プロダクションコード例:メモリリークゼロ・一括更新エンジン

以下のコードは、実務の現場でそのまま利用できる、堅牢性と保守性を極限まで高めたマスタープロジェクト更新スクリプトである。エラーが発生しても確実にメモリとファイルを解放する構造(RAIIパターンをVBAで模した設計)を取り入れている。

Option Explicit

‘ —————————————————————–
‘ 処理名: 巨大サブプロジェクト群 一括ベースライン設定・更新バッチ
‘ 概要: メモリリーク対策を徹底し、数千タスク規模のサブプロジェクトを安全に巡回・更新する
‘ —————————————————————–
Public Sub ExecuteMassSubprojectUpdate()
Dim startTime As Double
startTime = Timer

‘ 1. パフォーマンスと安定性のための環境設定
With Application
.ScreenUpdating = False
.DisplayAlerts = False
.Calculation = pjCalculationManual ‘ 手動計算モードでCOM負荷を激減させる
End With

On Error GoTo ErrorHandler

Dim masterPrj As Project
Set masterPrj = ActiveProject

Dim sp As Subproject
Dim targetPrj As Project
Dim subProjPath As String
Dim processedCount As Long
processedCount = 0

MsgBox “大規模サブプロジェクトの一括更新を開始します。”, vbInformation, “バッチ処理開始”

‘ 2. サブプロジェクトコレクションの巡回
‘ ※For Eachではなく、コレクションのインデックスを逆順または順次安全に取得する
Dim i As Long
For i = 1 To masterPrj.Subprojects.Count
Set sp = masterPrj.Subprojects(i)

‘ サブプロジェクトが外部ファイルとして存在する場合のみ処理
If Not sp.SourceProject Is Nothing Then
subProjPath = sp.Path

‘ 【重要】すでに開いている場合は一旦閉じる等の安全策を講じる
‘ ここでは確実に個別に開いて閉じるライフサイクルを強制する

Debug.Print “処理中 (” & i & “/” & masterPrj.Subprojects.Count & “): ” & subProjPath

‘ サブプロジェクトを開く
‘ 引数: (Readonly, Merge, TaskFilter, MissingFileAction)
sp.Open
Set targetPrj = sp.SourceProject

‘ ————————————————-
‘ 3. 【実務ロジック】ここに対象プロジェクトに対する処理を記述
‘ 例:全タスクのベースライン設定(例としてB1を設定)
Call ApplyBaselineToProject(targetPrj)
‘ ————————————————-

‘ 変更を保存して閉じる
targetPrj.Save
targetPrj.Close pjSave

processedCount = processedCount + 1
End If

‘ 4. ループごとの参照解放(極めて重要)
Set targetPrj = Nothing
Set sp = Nothing

‘ ガベージコレクションを促すための小休止(必要に応じてDoEvents)
DoEvents
Next i

‘ 計算モードを戻して再計算
Application.Calculation = pjCalculationAutomatic
masterPrj.Calculate

MsgBox “一括更新が正常に完了しました。” & vbCrLf & _
“処理件数: ” & processedCount & ” 件” & vbCrLf & _
“所要時間: ” & Format((Timer – startTime) / 60, “0.0”) & ” 分”, _
vbInformation, “処理完了”

CleanUp:
‘ 5. 確実な環境復元
With Application
.ScreenUpdating = True
.DisplayAlerts = True
.Calculation = pjCalculationAutomatic
End With

‘ ルートオブジェクトの解放
Set masterPrj = Nothing
Exit Sub

ErrorHandler:
MsgBox “致命的なエラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “バッチ処理中断”

‘ エラー時も確実にオブジェクトと環境を復元
Set targetPrj = Nothing
Set sp = Nothing
Set masterPrj = Nothing
Resume CleanUp
End Sub

‘ —————————————————————–
‘ サブルーチン: 個別プロジェクトへのベースライン適用ロジック
‘ —————————————————————–
Private Sub ApplyBaselineToProject(ByRef prj As Project)
On Error GoTo LocalError

‘ 例:ActiveBaselineを設定して保存
prj.SetBaseline Baseline:=pjBaseline

Exit Sub
LocalError:
‘ 個別ファイルのエラーでバッチ全体を止めないためのログ出力等の処理をここに書く
Debug.Print “Error in project: ” & prj.Name & ” – ” & Err.Description
Err.Clear
End Sub

チーフアーキテクトからの実務アドバイス

このコードをそのまま導入するにあたり、以下のデータベース連携やファイルサーバー運用における「現場の罠」についても言及しておこう。

1. ネットワーク越し(SharePoint / OneDrive / 企業内LAN)のファイルロック:
大規模プロジェクトにおいて、サブプロジェクトがネットワークドライブ上にある場合、COM経由の `Open` / `Close` は高確率でタイムアウトやファイル競合を引き起こす。大規模バッチを回す際は、事前にRobocopy等でローカルディスク(例: `C:\Temp\MSProject_Work\`)に全ファイルを同期させ、ローカル環境上でこのVBAを実行した上で、最後にサーバーへアップロードし直すアーキテクチャをとるべきだ。
2. `DoEvents` の効用と諸刃の剣:
ループ内に挟んだ `DoEvents` は、OSに制御を戻すことでメモリのフラグメンテーションを防ぎ、フリーズを防ぐ効果がある。しかし、ユーザーが意図せずバッチ実行中にMS Projectのウィンドウを操作してしまうリスクも生まれる。完全な無人バッチにする場合は、ウィンドウの最小化 (`AppWindow.WindowState = pjMinimized`) を併用することを強く推奨する。

プログラミングとは、動くものを作ることではない。「何時間、何万回実行しても、リソースを完全にクリーンに保ち続けられる美しき構造」を作ることだ。
この知見をあなたのプロジェクトに組み込み、安定稼働の果実を勝ち取ってほしい。

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