【テクニカル・上級編】【上級プロフェッショナル向け】大量ファイル操作におけるFSOボトルネックを計測・特定するマイクロ秒単位の自作プロファイラー – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【上級】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の深部と直結しているがゆえに、書き手の設計思想がそのままパフォーマンスに直結するシビアな言語である。

「なぜこのバッチは終わらないのか?」

その疑問に感覚で答えるのではなく、今回紹介したマイクロ秒プロファイラーを用いて「どのメソッドが何回呼ばれ、何秒を消費しているか」を数値化し、エビデンスベースでリファクタリングを敢行せよ。それこそが、レガシーシステムの極限を極めたエンジニアの流儀である。

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