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

スポンサーリンク

【上級者向け】大規模プロジェクトのサブプロジェクト一括更新におけるメモリリーク対策

Microsoft ProjectのVBA(Project VBA)において、数多のサブプロジェクトを統合した「マスタープロジェクト」の大規模バッチ処理は、多くのシニアエンジニアが一度は直面する悪夢の領域である。

数十、数百におよぶサブプロジェクトをループ内で順次開き、データを更新し、閉じる。この一連の処理を人間が手動で行うことは不可能であり、VBAによる自動化が不可欠だ。しかし、この日常的なバッチ処理を数時間走らせたとき、タスクマネージャーのメモリ使用量が右肩上がりに上昇し、や期せずして「メモリ不足(Out of Memory)」でプロセスがクラッシュする現象に直面した者は少なくないはずだ。

なぜProject VBAはこれほどまでにメモリリークを起こしやすいのか。そして、このレガシーなCOMコンポーネントの束縛から逃れ、数日間にわたる連続稼働をも耐え抜く堅牢なアーキテクチャをどう構築すべきか。

本稿では、Projectオブジェクトモデルの暗黒面と、Windows API、そしてガベージコレクションの挙動を完全に制御するための極限の知見を公開する。

1. Project VBAにおけるメモリリークの根因

真のエンジニアであれば、現象を嘆く前に「なぜそれが起きるのか」を物理レベルで理解しなければならない。

Project VBAの背後には、強大かつ複雑なCOM(Component Object Model)のツリーが存在する。`FileOpenEx`メソッドを呼び出し、サブプロジェクトをメモリ上にロードするたびに、COMランタイムは内部で参照カウンタ(Reference Counter)をインクリメントし、アンマネージドヒープ上にプロジェクト構造体を構築する。

ここで発生する典型的な罠は以下の通りだ:

1. 暗黙の参照保持: ループ内で `ActiveProject` や `Projects.Add` などを安易に使用すると、VBAランタイムがオブジェクトへの参照を隠蔽された変数内に保持し続ける。
2. COMラッパーの解放遅延: `Project.Close` メソッドを呼んでも、VBAの変数(`As Project`など)に参照が残っている場合、COMオブジェクトの背後にあるネイティブメモリは即座には解放されない。
3. VBA自身のガベージコレクションの怠慢: VBAのランタイムは、.NETのような高度な世代別GCを持たない。参照カウントが0になった瞬間に解放されるべきだが、循環参照や不適切な変数スコープによって、プロセス終了までメモリが解放されないゾンビオブジェクトが生成される。

これらを防ぐには、「変数のスコープを極限まで狭め、ループの各イテレーションの終わりで確実に参照を断ち切り、さらにWindows APIレベルでメモリの強制回収を促す」という徹底した防衛策が必要となる。

2. 対策のアーキテクチャ:堅牢な一括更新プロシージャ

以下に、数百規模のサブプロジェクトを安全に処理し、メモリリークを完全に封じ込めるための実用的なVBAコードを示す。

このコードでは、以下の設計思想を貫いている。

  • `Application.ScreenUpdating` や `DisplayAlerts` の完全制御によるオーバヘッド排除
  • オブジェクト変数の明示的な `Nothing` 代入による参照カウントの強制デクリメント
  • 大規模ループ内での定期的なWindows API(`SetProcessWorkingSetSize`)によるメモリワーキングセットのトリミング

実装コード

Option Explicit

‘ Windows APIの宣言:プロセスのワーキングセット(物理メモリ)を強制的に最小化する
If VBA7 Then
Private Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Private Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Private Declare Function GetCurrentProcess Lib “kernel32” () As Long
End If

‘ —————————————————————–
‘ メインエントリーポイント:サブプロジェクト一括更新バッチ
‘ —————————————————————–
Sub BatchUpdateSubProjects()
Dim fso As Object
Dim targetFolder As Object
Dim subFile As Object
Dim folderPath As String
Dim lngCount As Long

‘ 処理対象のフォルダパス(環境に合わせて変更)
folderPath = “C:\ProjectData\SubProjects\”

Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FolderExists(folderPath) Then
MsgBox “指定されたフォルダが存在しません: ” & folderPath, vbCritical
Exit Sub
End If

Set targetFolder = fso.GetFolder(folderPath)

‘ パフォーマンスと安定性のための設定
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Application.DisplayAlerts = False

lngCount = 0

‘ フォルダ内のサブプロジェクト(.mpp)を順次処理
For Each subFile In targetFolder.Files
If LCase(fso.GetExtensionName(subFile.Name)) = “mpp” Then
lngCount = lngCount + 1

‘ 1ファイルごとの処理を完全に独立したプロシージャに分離(スコープによるメモリ解放の徹底)
Call ProcessSingleProject(subFile.Path)

‘ 10ファイル処理するごとにWindows APIでメモリを強制回収
If lngCount Mod 10 = 0 Then
Call ForceGarbageCollection
End If
End If
Next subFile

MsgBox “すべてのサブプロジェクトの更新が完了しました。総処理数: ” & lngCount, vbInformation

CleanUp:
‘ 確実な環境復元
Application.ScreenUpdating = True
Application.DisplayAlerts = True
Set subFile = Nothing
Set targetFolder = Nothing
Set fso = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

‘ —————————————————————–
‘ 単一プロジェクトの処理(スコープを限定し、メモリ残留を防ぐ)
‘ —————————————————————–
Private Sub ProcessSingleProject(ByVal filePath As String)
Dim prj As Project

On Error GoTo ProjectErrorHandler

‘ サブプロジェクトを読込専用ではなく、書き込み用として開く
‘ ※大規模バッチでは ReadOnly:=False が前提となるケースが多い
FileOpenEx Name:=filePath, ReadOnly:=False

‘ アクティブなプロジェクトへの参照を取得
Set prj = ActiveProject

‘ — ここに実際の更新・ベースライン設定ロジックを記述 —
‘ 例:全タスクの再計算とベースラインの設定
CalculateAll
‘ prj.SetBaseline … 等の処理
‘ ——————————————————

‘ 変更を保存して閉じる
prj.Save
prj.Close pjSaveNo

‘ 参照の即時破棄
Set prj = Nothing
Exit Sub

ProjectErrorHandler:
‘ エラーが発生した場合でもファイルが開いたままにならないようにする
On Error Resume Next
If Not prj Is Nothing Then
prj.Close pjDoNotSave
Set prj = Nothing
End If
‘ ログ出力等の処理をここに挟むことが望ましい
On Error GoTo 0
End Sub

‘ —————————————————————–
‘ Windows APIを用いたメモリワーキングセットの強制トリミング
‘ —————————————————————–
Private Sub ForceGarbageCollection()
Dim hProc As LongPtr

‘ VBA自身のメモリ解放を促すためにDoEventsを挟む
DoEvents

‘ 現在のプロセスハンドルの取得
hProc = GetCurrentProcess()

‘ ワーキングセットを最小化(OSに不要なメモリページを返還させる)
‘ ※多用しすぎるとパフォーマンスに影響するため、一定間隔での実行が鉄則
Call SetProcessWorkingSetSize(hProc, &HFFFFFFFF, &HFFFFFFFF)

DoEvents
End Sub

3. コードの解説とシニアエンジニアの知見

A. プロシージャの分離(`ProcessSingleProject`の独立)

VBAにおいて、変数の寿命(ライフサイクル)はそれが宣言されたプロシージャの終了と共に終わる。もし、数百ファイルを処理する巨大な `For Each` ループの中に直接 `FileOpenEx` や `Set prj = …` を書くと、ループが回っている間はローカル変数のスコープが維持され、COMオブジェクトの参照がスタックやヒープに残り続ける。
処理を細分化し、1ファイル処理するごとにプロシージャを抜けさせることで、VBAのスコープ管理メカニズムに強制的にメモリを回収させることが可能になる。

B. 例外処理(Error Handling)の堅牢性

大規模バッチにおける最大の恐怖は、「途中でエラーダイアログがポップアップし、バッチが停止する」ことである。さらに悪いことに、エラー時にプロジェクトが開いたままになると、次回のファイルオープン時に排他制御(ロック)や読み取り専用の競合が発生する。
`ProcessSingleProject` 内の `ProjectErrorHandler` では、エラー発生時に確実に `prj.Close pjDoNotSave` を呼び出し、ゾンビプロセスの発生を防ぐ防壁を構築している。

C. Windows API (`SetProcessWorkingSetSize`) の活用

Project VBA単体の機能や `Set prj = Nothing` だけでは、OSが割り当てた物理メモリ(ワーキングセット)が即座に解放されないことが多々ある。これはWindowsのメモリ管理の仕様によるものだ。
`kernel32.dll` の `SetProcessWorkingSetSize` に対し `-1` (`&HFFFFFFFF`) を渡すことで、OSに対して「現在使用していないメモリページをただちにページファイルへスワップアウトし、物理メモリを解放せよ」と強制指示を出すことができる。これにより、数千ファイルの処理であっても、メモリ使用量を一定の閾値以下に押さえ込むことが可能となる。

4. 運用上の極意:インフラとバッチ設計のベストプラクティス

コードレベルの対策を施したとしても、インフラストラクチャ側の配慮が欠けていれば、エンタープライズ環境での安定稼働は勝ち取れない。

1. 仮想環境(RDP/仮想デスクトップ)でのheadless実行の回避
Microsoft Projectのオブジェクトモデルは、内部でGUI(ウィンドウやリボン)の描画エンジンと強く結合している。完全にバックグラウンド(画面なし)で動作するように設計されているわけではない。バッチ実行時は必ずセッションを維持し、`Application.ScreenUpdating = False` を徹底すること。
2. チャンク(分割)実行のすすめ
1回のバッチで1,000ファイルを処理するのではなく、リストを「1〜200」「201〜400」のように分割し、バッチプロセス自体をタスクスケジューラ等から完全に独立して多段起動・終了させる設計(プロセス単位のライフサイクル管理)に昇華させることこそが、真の意味での「落ちないシステム」のアーキテクチャである。

レガシーな技術と侮るなかれ。Project VBAの深淵を理解し、OSのメモリ管理までを視野に入れたコードを書くとき、VBAは単なるマクロの域を超え、堅牢なエンタープライズ・バッチエンジンへと生まれ変わるのだ。

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