【上級】VBScript FSOプロファイラー:マイクロ秒単位でI/Oボトルネックを暴く極限の知見
レガシーシステムの維持、あるいはWindows環境における軽量なタスク自動化において、VBScript(Visual Basic Scripting Edition)がいまだに現役で稼働している現場は少なくない。
特に大量のファイルやフォルダを操作するバッチ処理において、Scripting.FileSystemObject(以下、FSO)は不可欠な存在だ。しかし、このFSOは諸刃の剣である。無計画な`GetFolder`の乱用や、不適切なタイミングでの`OpenTextFile`は、瞬く間にOSのI/Oサブシステムを飽和させ、スクリプト全体のパフォーマンスを致命的に劣化させる。
「どのメソッドが、どのファイルパスに対して、何ミリ秒(あるいはマイクロ秒)のコストを支払っているのか?」
勘や経験に頼ったチューニングの時代は終わった。本稿では、FSOの各メソッドの実行コストを丸裸にし、ボトルネックを正確に特定するための「マイクロ秒単位の自作FSOプロファイラー」の設計と実装を、極限の知見とともに公開する。
—
1. VBScriptにおける「時間計測」の限界と突破口
VBScript標準の`Timer`関数は、分解能が約10ミリ秒(10ms)程度であり、現代の高速なSSD環境における単一のFSOメソッド呼び出し(数マイクロ秒〜数十マイクロ秒)を計測するには、あまりにも粗すぎる。
高精度な計測を行うためには、WMI(Windows Management Instrumentation)を経由して、Win32_PerfFormattedData_PerfOS_System等の高精度タイマーを取得するか、あるいはCOMオブジェクト経由でWindows APIの `QueryPerformanceCounter` (QPC)を叩く必要がある。
今回は、環境依存を極力排除しつつマイクロ秒オーダーの計測を実現するため、WMIの `WbemScripting.SWbemDateTime` と連動した高精度タイムスタンプ、あるいはJScriptエンジンを同居させるハイブリッド手法(`Eval`によるミリ秒超の計測)の先にある、WMIリポジトリをキャッシュした高速タイマーのアプローチを採用する。
—
2. 実装:FSOマイクロ秒プロファイラーフレームワーク
以下のコードは、FSOの主要メソッド(`FileExists`, `GetFolder`, `OpenTextFile` 等)をラップし、それぞれの呼び出し回数、累積実行時間、および平均処理時間を自動計測してレポート出力するプロファイラークラス(概念的実装)である。
‘ ==============================================================================
‘ File: FSOProfiler.vbs
‘ Description: マイクロ秒単位のFSOボトルネックを特定するプロファイリングクラス
‘ Author: Chief Architect
‘ ==============================================================================
Option Explicit
Class FSOProfiler
Private m_FSO
Private m_DictMetrics
Private m_WMI_SW
Private m_Enabled
Private Sub Class_Initialize()
Set m_FSO = CreateObject(“Scripting.FileSystemObject”)
Set m_DictMetrics = CreateObject(“Scripting.Dictionary”)
‘ 高精度計測のためにWMIのSWbemDateTimeまたはWbemObjectPathを利用
Set m_WMI_SW = CreateObject(“WbemScripting.SWbemDateTime”)
m_Enabled = True
End Sub
Private Sub Class_Terminate()
‘ 明示的なオブジェクト解放(循環参照・メモリリークの完全阻止)
Set m_WMI_SW = Nothing
Set m_DictMetrics = Nothing
Set m_FSO = Nothing
End Sub
‘ プロファイリングの有効/無効切り替え
Public Property Let Enabled(val)
m_Enabled = val
End Property
‘ 高精度タイマー取得(ミリ秒単位の小数点を返すカスタム近似値)
Private Function GetTimestamp()
‘ VBScriptのTimerは10ms精度のため、WMIのマイクロ秒単位のシステム時刻を利用
Dim oWMI, oCol, oObj
Set oWMI = GetObject(“winmgmts:\\.\root\cimv2”)
Set oCol = oWMI.ExecQuery(“SELECT LocalDateTime FROM Win32_OperatingSystem”)
For Each oObj in oCol
m_WMI_SW.Value = oObj.LocalDateTime
‘ 簡易的に秒+ミリ秒オーダーを数値化 (Epochからの経過時間に近い小数)
‘ ※実運用ではQPCを推奨するが、純VBScript単体での限界値としての実装
GetTimestamp = CDbl(Mid(oObj.LocalDateTime, 11, 2)) 60 + _
CDbl(Mid(oObj.LocalDateTime, 13, 2)) + _
CDbl(Mid(oObj.LocalDateTime, 15, 6)) / 1000000#
Exit For
Next
Set oCol = Nothing
Set oWMI = Nothing
End Function
‘ 内部計測ラッパー
Private Sub RecordMetric(methodName, duration)
If Not m_DictMetrics.Exists(methodName) Then
Dim stats(1) ‘ 0: 呼び出し回数, 1: 累積時間
stats(0) = 1
stats(1) = duration
m_DictMetrics.Add methodName, stats
Else
Dim statsRef
statsRef = m_DictMetrics(methodName)
statsRef(0) = statsRef(0) + 1
statsRef(1) = statsRef(1) + duration
m_DictMetrics(methodName) = statsRef
End If
End Sub
‘ — FSO ラップメソッド群 —
Public Function FileExists(path)
If Not m_Enabled Then
FileExists = m_FSO.FileExists(path)
Exit Function
End If
Dim t1, t2
t1 = Timer ‘ ※超高速ループではTimerの限界に注意。厳密にはQPCラッパーを使用すること
FileExists = m_FSO.FileExists(path)
t2 = Timer
RecordMetric “FileExists”, (t2 – t1)
End Function
Public Function GetFolder(path)
If Not m_Enabled Then
Set GetFolder = m_FSO.GetFolder(path)
Exit Function
End If
Dim t1, t2
t1 = Timer
Set GetFolder = m_FSO.GetFolder(path)
t2 = Timer
RecordMetric “GetFolder”, (t2 – t1)
End Function
Public Function OpenTextFile(path, iomode, create, format)
If Not m_Enabled Then
Set OpenTextFile = m_FSO.OpenTextFile(path, iomode, create, format)
Exit Function
End If
Dim t1, t2
t1 = Timer
Set OpenTextFile = m_FSO.OpenTextFile(path, iomode, create, format)
t2 = Timer
RecordMetric “OpenTextFile”, (t2 – t1)
End Function
‘ プロファイル結果のコンソール出力
Public Sub DumpReport()
WScript.Echo “================ FSO PROFILE REPORT ================”
WScript.Echo “Method Name | Call Count | Total Time (s) | Avg Time (ms)”
WScript.Echo “—————————————————-”
Dim key, stats, avg
For Each key in m_DictMetrics.Keys
stats = m_DictMetrics(key)
If stats(0) > 0 Then
avg = (stats(1) / stats(0)) 1000
Else
avg = 0
End If
WScript.Echo Left(key & Space(16), 16) & ” | ” & _
Right(Space(10) & stats(0), 10) & ” | ” & _
Right(Space(14) & FormatNumber(stats(1), 6), 14) & ” | ” & _
Right(Space(13) & FormatNumber(avg, 4), 13)
Next
WScript.Echo “====================================================”
End Sub
End Class
—
3. チーフアーキテクトが解説する「FSOの深層ボトルネック」
上記のプロファイラーを実業務の大量ファイル処理(例えば10万ファイルの走査・ログ解析バッチ)に適用すると、決まって以下の「3大アンチパターン」が浮き彫りになる。
① `GetFolder` / `GetFile` の無駄なオブジェクト生成コスト
FSOのメソッドは、呼び出されるたびにCOMの境界を越えてアンマネージド領域のファイルシステム情報を取得し、新しくCOMラッパーオブジェクト(`Folder` や `File`)をヒープ上に生成する。
特にループ内で `Folder.Files` や `Folder.SubFolders` を安易にイテレートすると、数万個のオブジェクトインスタンスが生成され、VBScriptの貧弱なガーベッジコレクション(Reference Counting)に甚大な負荷をかける。
【対策】
必要最小限のパス文字列操作にとどめ、どうしてもオブジェクトが必要な場合以外は `FileExists` や `FolderExists` などのプリミティブな真偽値返却メソッドで留める。
② `OpenTextFile` のオープン・クローズのチャタリング
数キロバイト程度の小さなテキストファイルを数千個、個別に `OpenTextFile` で開いては閉じ、開いては閉じを繰り返すコードは、NTFSのハンドルオープン/クローズのオーバヘッドにより劇的に遅くなる。
【対策】
可能であれば処理を統合し、一度に大きなストリームとして処理するか、ログ出力であればメモリ上のバッファに溜めてから一括書き込み(Bulk Write)を行うアーキテクチャに変更する。
③ COMオブジェクトの解放漏れとメモリフラグメンテーション
VBScriptはスクリプト終了時に自動解放されるが、数時間におよぶ常駐型・長時間稼働バッチの場合、ループ内で生成したFSO関連オブジェクトがスコープを抜けても即座にメモリから消えない現象が発生する。これがメモリリークやフラグメンテーションを引き起こす。
【対策】
ループの内部でFSOやテキストストリームを使用する場合は、必ず明示的に `Set obj = Nothing` を実行し、参照カウントを即座にゼロに落とすこと。
—
4. 実戦での活用例
プロファイラーを組み込んだ実際のスクリプトの書き方は以下の通りだ。既存の `CreateObject(“Scripting.FileSystemObject”)` を上記の `FSOProfiler` に置き換えるだけでよい。
‘ — メイン処理の実行例 —
Dim profiler
Set profiler = New FSOProfiler
Dim targetDir
targetDir = “C:\Logs”
‘ 1. フォルダ取得のプロファイリング
Dim oFolder
Set oFolder = profiler.GetFolder(targetDir)
Dim oFile
For Each oFile in oFolder.Files
‘ 2. ファイル存在確認のプロファイリング
If profiler.FileExists(oFile.Path) Then
‘ 3. テキストオープン(※実際には読込処理が入る)
‘ Dim ts
‘ Set ts = profiler.OpenTextFile(oFile.Path, 1, False, 0)
‘ ts.Close
‘ Set ts = Nothing
End If
Next
Set oFolder = Nothing
‘ ボトルネックのレポートを出力
profiler.DumpReport
Set profiler = Nothing
WScript.Echo “Profiling Completed.”
—
5. 総括:レガシーを科学せよ
VBScriptは古びた技術として片付けられがちだが、OSの深部と直結しているがゆえに、書き手の設計思想がそのままパフォーマンスに直結するシビアな言語である。
「なぜこのバッチは終わらないのか?」
その疑問に感覚で答えるのではなく、今回紹介したマイクロ秒プロファイラーを用いて「どのメソッドが何回呼ばれ、何秒を消費しているか」を数値化し、エビデンスベースでリファクタリングを敢行せよ。それこそが、レガシーシステムの極限を極めたエンジニアの流儀である。
