【テクニカル・上級編】【上級プロフェッショナル向け】大量図面の一括変換における「メモリリーク」対策:一定数処理ごとのAcadApplication再起動とセッション復元 – AutoCAD VBA解析バイブル

スポンサーリンク
スポンサーリンク

破滅的なメモリ枯渇を回避せよ:大量図面一括変換における「プロセスの断続的再起動」アーキテクチャ

数千、あるいは数万という単位のDWGファイルを、バージョン変換(例:2018形式から2013形式へのダウングレード)や一括属性更新のためにループ処理する。これは、業務自動化における「日常」でありながら、同時に「最も危険な戦場」でもある。

多くのエンジニアは、単純な `For Each` ループの中に `Documents.Open` と `SaveAs` を詰め込み、`Set obj = Nothing` を記述して「完璧な解放」を謳う。しかし、彼らは決定的な真実を見落としている。AutoCADという巨大なC++アプリケーションが、COMインターフェースを通じてメモリを消費し続けるプロセスにおいて、VBAのガベージコレクション(参照カウントのゼロ化)だけでは、ホストプロセスのメモリ肥大化(メモリリーク)を止めることは不可能だ。

本稿では、単なるコードの書き方ではなく、プロセスレベルでの生存戦略——「Controller(制御側)」と「Worker(実行側)」を分離し、一定数ごとにAutoCADのプロセスそのものを物理的に破棄・再起動する、極限の堅牢性を備えたアーキテクチャを解説する。

1. 宿命的な問題:なぜ `Set obj = Nothing` では足りないのか

VBAにおけるオブジェクト解放は、あくまで「COM参照カウント」を減らす行為に過ぎない。AutoCADの内部エンジン(C++層)は、図面を開くたびに以下のリソースを蓄積していく。

  • Undo Stack(元に戻す履歴)のメモリ占有
  • 辞書(Dictionaries)やシンボルテーブルのキャッシュ
  • フォント、画層、スタイル情報の内部レジストリ
  • COMインターフェースのオーバーヘッドによるヒープ領域の断片化

これらは、VBA側でオブジェクトを `Nothing` にしたとしても、AutoCADのプロセス(`acad.exe`)が生存している限り、OSから見れば「使用中」として扱われ続け、やがて仮想メモリを食いつぶし、システム全体のパフォーマンスを著しく低下させるか、最悪の場合 `Out of Memory` でクラッシュを引き起こす。

大量処理における正解は、「プロセスを使い捨てにする」ことだ。

2. 解決策:Controller-Worker モデルの構築

単一のVBAマクロで全てを完結させてはならない。以下の二層構造を構築する。

1. Controller (外部制御層): VBScript またはスタンドアロンの実行ファイル。図面リストを管理し、AutoCADのプロセスを起動・監視し、一定数処理が終わるごとにプロセスを強制終了(Kill)して再起動する。
2. Worker (AutoCAD内部層): AutoCAD内で実行されるVBA。Controllerから渡された「特定のバッチ分」の図面のみを処理し、完了したら自ら終了(またはControllerに制御を戻す)する。

これにより、メモリリークの源泉である `acad.exe` を定期的にリフレッシュし、常にクリーンなメモリ状態で処理を継続できる。

3. 実装:極限の堅牢性を備えたコード・アーキテクチャ

ここでは、最も導入が容易でありながら強力な VBScriptによるController と、AutoCAD VBAによるWorker の実装例を示す。

A. Controller: VBScriptによるプロセス・オーケストレーション

このスクリプトは、AutoCADを「道具」として扱い、その寿命を完全に支配する。

' =============================================================================
' Controller: DWG Batch Processor Orchestrator
' 機能: 図面リストを分割し、AutoCADプロセスを一定数ごとに再起動する
' =============================================================================
Option Explicit

Dim objFSO, objFile, strFileList, strCurrentFile
Dim intBatchSize, intProcessedCount, intTotalFiles
Dim objShell, strAcadPath, strCommand

' --- 設定領域 ---
strFileList = "C:\Work\DWG_List.txt" ' 処理対象のフルパスが並んだテキストファイル
intBatchSize = 50                   ' 何枚ごとにAutoCADを再起動するか(メモリ状況に応じて調整)
strAcadPath = "C:\Program Files\Autodesk\AutoCAD 20xx\acad.exe"
' ----------------

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")

If Not objFSO.FileExists(strFileList) Then
    WScript.Echo "Error: File list not found."
    WScript.Quit
End If

' ファイルリストの読み込みと総数のカウント
Dim arrFiles()
Dim count : count = 0
Set objFile = objFSO.OpenTextFile(strFileList, 1)
Do Until objFile.AtEndOfStream
    count = count + 1
    ReDim Preserve arrFiles(count - 1)
    arrFiles(count - 1) = objFile.ReadLine
Loop
objFile.Close

intTotalFiles = count
intProcessedCount = 0

WScript.Echo "Starting Batch Process. Total: " & intTotalFiles & " files."

' --- メインループ ---
Do While intProcessedCount < intTotalFiles
    ' 新しいAutoCADインスタンスの起動
    ' /b はバックグラウンド実行、/c はコマンドライン引数(ここではWorkerを起動するスクリプトを指定)
    ' ※実際には、Worker用のVBAを起動する仕組みが必要
    WScript.Echo "Launching new AutoCAD instance for batch starting at " & (intProcessedCount + 1)
    
    ' AutoCADを起動し、Workerマクロを実行するコマンドを構築
    ' ここでは、Workerに「開始インデックス」と「終了インデックス」を渡す設計とする
    Dim startIdx, endIdx
    startIdx = intProcessedCount
    endIdx = intProcessedCount + intBatchSize - 1
    If endIdx >= intTotalFiles Then endIdx = intTotalFiles - 1

    ' AutoCADを起動(コマンドライン引数を利用してVBAをトリガー)
    ' 注意: AutoCADの仕様により、直接VBA引数を渡すには特定のスイッチや
    ' 事前に作成した起動用スクリプトが必要。ここでは概念的な実行を示す。
    strCommand = """" & strAcadPath & """ /p ""C:\Work\WorkerLauncher.scr"""
    objShell.Run strCommand, 1, True ' Trueで終了を待機

    ' 進捗更新
    intProcessedCount = endIdx + 1
    
    ' プロセスが残っている場合の強制終了(ゾンビプロセス対策)
    ' Windows APIに近い挙動をShell経由で実行
    objShell.Run "taskkill /F /IM acad.exe /T", 0, True
Next

WScript.Echo "All tasks completed successfully."

' =============================================================================
' 補足: WorkerLauncher.scr (AutoCADスクリプトファイル)
' 内容例:
' (command "_load" "C:/Work/WorkerModule.dvb")
' (command "_RunBatch" "0" "50") 
' =============================================================================

B. Worker: AutoCAD VBAによる高効率処理

Workerは、渡された範囲の図面を処理することだけに集中する。

' =============================================================================
' Worker: AutoCAD VBA Module
' 機能: 指定された範囲の図面を確実に処理し、エラーをログに記録する
' =============================================================================
Option Explicit

' 外部からインデックスを受け取るためのグローバル変数
Public g_StartIndex As Long
Public g_EndIndex As Long

Sub RunBatch()
    ' ※実際には、スクリプトや外部から引数を渡す仕組み(Commandline等)を利用
    ' ここではデモとして固定値を代入
    g_StartIndex = 0 
    g_EndIndex = 49
    
    ExecuteProcessing
End Sub

Sub ExecuteProcessing()
    Dim i As Long
    Dim doc As AcadDocument
    Dim filePath As String
    Dim logFile As String
    Dim fso As Object
    
    logFile = "C:\Work\process_log.txt"
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    ' 図面リストの読み込み(Controllerと同じリストを使用)
    Dim arrFiles As Variant
    arrFiles = LoadFileList("C:\Work\DWG_List.txt")
    
    On Error Resume Next
    
    For i = g_StartIndex To g_EndIndex
        If i >= UBound(arrFiles) Then Exit For
        
        filePath = arrFiles(i)
        
        ' 1. 図面のオープン
        Set doc = Documents.Open(filePath)
        
        If Err.Number <> 0 Then
            WriteLog logFile, "ERROR: Cannot open " & filePath & " - " & Err.Description
            Err.Clear
        Else
            ' 2. 変換・更新ロジック(ここが心臓部)
            PerformTransformation doc
            
            ' 3. 保存とクローズ
            ' SaveAs を使用してバージョン指定を行う
            ' acR2013Drawing は定数。環境に合わせて選択。
            doc.SaveAs filePath, acR2013Drawing 
            doc.Close True
            
            WriteLog logFile, "SUCCESS: " & filePath
        End If
        
        ' 4. オブジェクトの明示的解放(重要だが、これだけでは不十分であることを理解しておくこと)
        Set doc = Nothing
        
        ' 5. CPU負荷軽減のための僅かな待機
        DoEvents
    Next i
    
    ' 全処理終了後、AutoCADを閉じる(Controllerがtaskkillするが、自ら閉じるのが紳士的)
    ThisDrawing.Close False
End Sub

Private Sub PerformTransformation(ByRef doc As AcadDocument)
    ' ここに具体的な図面操作ロジックを記述
    ' 例: 全レイヤーの画層名変更、ブロック属性の更新など
    Dim ent As AcadEntity
    For Each ent In doc.ModelSpace
        ' 処理...
    Next ent
End Sub

Private Function LoadFileList(path As String) As Variant
    Dim fso As Object, ts As Object
    Dim arr() As String, line As String, count As Long
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set ts = fso.OpenTextFile(path, 1)
    count = 0
    Do Until ts.AtEndOfStream
        ReDim Preserve arr(count)
        arr(count) = ts.ReadLine
        count = count + 1
    Loop
    ts.Close
    LoadFileList = arr
End Function

Private Sub WriteLog(path As String, msg As String)
    Dim fso As Object, ts As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set ts = fso.OpenTextFile(path, 8, True) ' 8 = ForAppending
    ts.WriteLine Now & " : " & msg
    ts.Close
End Sub

4. アーキテクトの視点:運用における極意

このアーキテクチャを実戦投入する際、以下の3点に留意せよ。

① Checkpoint(再開地点)の実装

大規模なバッチ処理は、夜間に実行されることが多い。途中で停電やネットワーク遮断が発生した場合に、最初からやり直すのは時間の浪費だ。
`process_log.txt` をスキャンし、「最後に成功したインデックス」を Controller が把握するロジックを組み込め。これにより、失敗しても「中断した箇所から再開」する、真にプロフェッショナルなバッチ処理が完成する。

② Windows APIによる「ゾンビプロセス」の根絶

AutoCADは、稀にダイアログボックスを表示したまま、あるいは致命的なエラーで応答なし(Not Responding)の状態になる。
Controller側では、`taskkill /F` を使用して、確実にプロセスを物理的に抹殺せよ。これは「行儀が悪い」ことではなく、「決定論的な制御」を実現するために不可欠な手段である。

③ メモリ使用量の監視

`intBatchSize`(再起動間隔)は、環境によって最適値が異なる。
Windowsのパフォーマンスモニター(PerfMon)を用いて、`acad.exe` の `Private Bytes` がどのように推移するかを計測せよ。メモリが線形に増加し、一定の閾値(例えば物理メモリの50%)に達する前にプロセスをリフレッシュするように設定するのが、最も効率的な戦略となる。

結論

VBAは強力だが、それはあくまで「AutoCADという宇宙の中の、小さな一画」を操作する手段に過ぎない。宇宙そのものが膨張し、重力(メモリ)が歪むとき、エンジニアに求められるのは、その宇宙を外側からコントロールする「設計思想」である。

「プロセスを使い捨て、状態を外部に持たせる」

この原則を胸に刻めば、君の自動化システムは、いかなる膨大なデータ群に対しても、揺るぎない堅牢性を発揮するだろう。

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