【Xcode15/GPUプロファイリング】Instrumentsで計測したGPUカウンタをテキスト出力する

2024/05/05に公開

開発環境

  • Unity 2022.3.21f1
  • Xcode 15.3
  • macOS Sonoma バージョン14.4.1
  • Python 3.9.6

はじめに

Instrumentsで計測したGPUカウンターの値を、スプレッドシートなど外部ツールで分析したいことがよくあります。
しかし、InstrumentsのGUI操作では、計測値をエクスポートすることができません。

本記事では、Instrumentsで生成されたtraceファイルから、
コマンドラインを用いてGPUカウンターのデータを抽出する方法について詳しく解説します。


GPU Counter

エクスポートの流れ

  1. xctraceコマンドを使用して、GPUカウンターの値をXMLファイルへ出力
  2. Pythonを利用して、必要な数値だけを抽出してテキスト形式へ整形

xctraceコマンドを使用して、GPUトレースをXML出力

xctraceコマンドを使用することで、traceデータから指定のデータを抽出することができます。

xctrace export [options]
コマンドの使い方の確認 (helpオプションの指定)

helpオプションを指定すると、コマンドの使い方が確認できます。

xctrace help export

usage: xctrace export [<options>] [--toc | --xpath expression]

description:
        Export given .trace using supplied query to the XML file format that can be later read and post-processed

options:
        --input <file>              Export data from the given .trace file
        --output <path>             Command output is written to the given path, if specified
        --toc                       Present entities to export in the table of contents form
        --xpath <expression>        Choose elements to export using specified XPath expression
        --har                       Export data as the HTTP Archive file

global options:
        --quiet        Make terminal ouput less verbose

notes:
        If output path is not specified, the export operation output will be written to the standard output.
        Table of Contents and XPath query are two separate modes and they cannot be specified together.

examples:
        xctrace export --input input.trace --toc
        xctrace export --input input.trace --toc --output table_of_contents.xml
        xctrace export --input input.trace --xpath '/trace-toc/run[@number="1"]/data/table[@schema="my-table-schema"]'

手順1. テーブル一覧の抽出

以下のコマンドを叩き、input.traceファイルからtableの一覧を抽出します。

xctrace export --input input.trace --toc --output table_of_contents.xml

テーブル一覧の確認

table_of_contents.xmlを開くと、さまざまなテーブルがずらっと並んでいるかと思います。
Instrumentsで計測したあらゆるデータがここに並んでいます。


table_of_contents.xml

今回欲しいのはGPUカウンタですので、ファイル内検索でgpu-counterで絞り込んでみましょう。

以下の二つのテーブルがヒットするかと思います。

  • schema="gpu-counter-info"
  • schema="gpu-counter-value"
table_of_contents.xml
<table shader-profiler="0" counter-profile="3" schema="gpu-counter-info" counter-device="xxx"/>
<table schema="gpu-counter-value"/>

gpu-counter-infoには、GPUカウンターの情報が格納されています。
gpu-counter-valueには、GPUカウンターの計測値が格納されています。

gpu-counter-infoの抽出

以下のコマンドを叩くと、gpu-counter-infoテーブルを取り出すことができます。
取り出したテーブルは gpu-counter-info.xml へ保存されます。

xctrace export --input input.trace --xpath '/trace-toc/run[@number="1"]/data/table[@schema="gpu-counter-info"]' > gpu-counter-info.xml

schemaの確認

schema要素を見ると、各列の情報が確認できます。
2番目の列に Counter ID が、 3番目の列には Name が入っています。
このschemaはどのrow要素に何の値が入るかを定義したものとなっています。

gpu-counter-info.xml
<schema name="gpu-counter-info">
    <col>
        <mnemonic>timestamp</mnemonic>
        <name>Timestamp</name>
        <engineering-type>event-time</engineering-type>
    </col>
    <col>
        <mnemonic>counter-id</mnemonic>
        <name>Counter ID</name>
        <engineering-type>uint32</engineering-type>
    </col>
    <col>
        <mnemonic>name</mnemonic>
        <name>Name</name>
        <engineering-type>gpu-counter-name</engineering-type>
    </col>
    <col>
        <mnemonic>max-value</mnemonic>
        <name>Max Value</name>
        <engineering-type>uint64</engineering-type>
    </col>
    <col>
        <mnemonic>accelerator-id</mnemonic>
        <name>Accelerator ID</name>
        <engineering-type>uint64</engineering-type>
    </col>
    <col>
        <mnemonic>description</mnemonic>
        <name>Description</name>
        <engineering-type>string</engineering-type>
    </col>
    <col>
        <mnemonic>group-index</mnemonic>
        <name>Group Index</name>
        <engineering-type>uint32</engineering-type>
    </col>
    <col>
        <mnemonic>type</mnemonic>
        <name>Type</name>
        <engineering-type>string</engineering-type>
    </col>
    <col>
        <mnemonic>ring-buffer-count</mnemonic>
        <name>Ring Buffer Count</name>
        <engineering-type>uint32</engineering-type>
    </col>
    <col>
        <mnemonic>require-weighted-accumulation</mnemonic>
        <name>Require Weighted Accumulation</name>
        <engineering-type>boolean</engineering-type>
    </col>
    <col>
        <mnemonic>sample-interval</mnemonic>
        <name>Sample Interval</name>
        <engineering-type>uint32</engineering-type>
    </col>
</schema>

rowの確認

row要素は20個ほど並んでおり、それぞれがGPUカウンターを表しています。
例えば、以下のrow要素を見てみましょう。
これはBuffer Write LimiterというGPUカウンターを表しており、
2番目の要素にはCounter ID (7) が格納されていることがわかります。

<row>
    <event-time ref="1"/>
    <uint32 id="27" fmt="7">7</uint32>
    <gpu-counter-name id="28" fmt="Buffer Write Limiter">Buffer Write Limiter</gpu-counter-name>
    <uint64 ref="4"/>
    <uint64 ref="5"/>
    <string id="29" fmt="Measures the time during which buffer stores are attempted to execute as a percentage of peak buffer load performance.">Measures the time during which buffer stores are attempted to execute as a percentage of peak buffer load performance.</string>
    <uint32 ref="2"/>
    <string ref="7"/>
    <uint32 ref="8"/>
    <boolean ref="9"/>
    <uint32 ref="2"/>
</row>

カウンター名とカウンターIDは1:1に対応しており、これを表にまとめると以下のようになります。

Counter ID Counter Name
0 F32 Utilization
1 F16 Utilization
2 Texture Sample Limiter
3 Texture Filtering Limiter
4 Texture Cache Limiter
5 Texture Write Limiter
6 Buffer Read Limiter
7 Buffer Write Limiter
8 Threadgroup/Imageblock Load Limiter
9 Threadgroup/Imageblock Store Limiter
10 Fragment Input Interpolation Limiter
11 GPU Last Level Cache Limiter
12 Vertex Occupancy
13 Fragment Occupancy
14 Compute Occupancy
15 GPU Read Bandwidth
16 GPU Write Bandwidth
17 MMU Limiter
18 MMU Utilization
19 Partial Renders Count

手順2. gpu-counter-valueの抽出

以下のコマンドを叩くと、gpu-counter-valueテーブルを取り出すことができます。
取り出したテーブルは gpu-counter-value.xml へ保存されます。
(出力されるxmlの行数は数十万 ~ 数千万行と、とてつもなく長いです)

xctrace export --input input.trace --xpath '/trace-toc/run[@number="1"]/data/table[@schema="gpu-counter-value"]' > gpu-counter-value.xml

row要素の中の値は、以下のような値が格納されています。

row 概要 備考
1番目 タイムスタンプ 時間の単位はns
fmtにはInstruments上で確認できる値が入っている
2番目 Counter ID Counterの名前はgpu-counter-valueに格納されている
3番目 GPUカウンターの計測値
gpu-counter-value.xml
<?xml version="1.0"?>
<trace-query-result>
<node xpath='//trace-toc[1]/run[1]/data[1]/table[94]'><schema name="gpu-counter-value"><col><mnemonic>timestamp</mnemonic><name>Timestamp</name><engineering-type>event-time</engineering-type></col><col><mnemonic>counter-id</mnemonic><name>Counter ID</name><engineering-type>uint32</engineering-type></col><col><mnemonic>value</mnemonic><name>Value</name><engineering-type>fixed-decimal</engineering-type></col><col><mnemonic>accelerator-id</mnemonic><name>Accelerator ID</name><engineering-type>uint64</engineering-type></col><col><mnemonic>sample-index</mnemonic><name>Sample ID</name><engineering-type>uint32</engineering-type></col><col><mnemonic>ring-buffer-index</mnemonic><name>Ring Buffer Index</name><engineering-type>uint32</engineering-type></col></schema>
<row><event-time id="1" fmt="00:00.091.975">91975333</event-time><uint32 id="2" fmt="0">0</uint32><fixed-decimal id="3" fmt="0.0000">0.000000000</fixed-decimal><uint64 id="4" fmt="4,294,967,919">4294967919</uint64><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="5" fmt="1">1</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="6" fmt="2">2</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="7" fmt="3">3</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="8" fmt="4">4</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="9" fmt="5">5</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="10" fmt="6">6</uint32><fixed-decimal id="11" fmt="0.2058">0.205807956</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="12" fmt="7">7</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="13" fmt="8">8</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="14" fmt="9">9</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="15" fmt="10">10</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="16" fmt="11">11</uint32><fixed-decimal id="17" fmt="29.4304">29.430373509</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="18" fmt="12">12</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="19" fmt="13">13</uint32><fixed-decimal id="20" fmt="0.1162">0.116167340</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="21" fmt="14">14</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="22" fmt="15">15</uint32><fixed-decimal id="23" fmt="0.0034">0.003368421</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="24" fmt="16">16</uint32><fixed-decimal id="25" fmt="1.2061">1.206063157</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="26" fmt="17">17</uint32><fixed-decimal id="27" fmt="30.7648">30.764758056</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="28" fmt="18">18</uint32><fixed-decimal id="29" fmt="24.7452">24.745244899</fixed-decimal><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time ref="1"/><uint32 id="30" fmt="19">19</uint32><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="2"/><uint32 ref="2"/></row>
<row><event-time id="31" fmt="00:00.092.131">92131833</event-time><uint32 ref="2"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="5"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="6"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="7"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="8"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="9"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="10"/><fixed-decimal id="32" fmt="0.2085">0.208497375</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="12"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="13"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="14"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="15"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="16"/><fixed-decimal id="33" fmt="20.0945">20.094488188</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="18"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="19"/><fixed-decimal id="34" fmt="0.1172">0.117197752</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="21"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="22"/><fixed-decimal id="35" fmt="0.0283">0.028330392</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="24"/><fixed-decimal id="36" fmt="8.3427">8.342684843</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="26"/><fixed-decimal id="37" fmt="20.0472">20.047244094</fixed-decimal><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="28"/><fixed-decimal ref="37"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>
<row><event-time ref="31"/><uint32 ref="30"/><fixed-decimal ref="3"/><uint64 ref="4"/><uint32 ref="5"/><uint32 ref="2"/></row>

手順3. GPUカウンターの値をテキストファイルへ出力する

ここからが本題になりますが、GPUカウンターの値を外部へ持っていくためにはXMLを自分で解析して出力してあげる必要があります。
(GPUカウンターの値をXML以外の形式で出力する方法を見つけることができませんでした)

今回は、Python3を用いてXMLを解析してテキストファイルとして出力することにしました。
以下のpyファイルを、二つのxmlファイル(gpu-counter-info.xml, gpu-counter-value.xml)と同じ階層に配置します。

collect_counters.py
import xml.etree.ElementTree as ET

counters = {}
counterInfoRoot = ET.parse('gpu-counter-info.xml').getroot()

# GPUカウンタの名前を取得
rows = counterInfoRoot.findall(".//row")
counter_names={}
for i in range(0, len(rows)):
    row = rows[i]
    row_children = list(row)
    counter_id = i
    counter_name = row_children[2].text
    counter_names[counter_id]=counter_name

# 集計用のデータを初期化
row_count=0
for i in range(0, len(counter_names)):
    counters[i] = {
        "name":counter_names[i],
        "id":i,
        "sum":0.0,        
        "max":0.0,
        "count":0,
        "values":[]
    }
counterValueRoot = ET.parse('gpu-counter-value.xml').getroot()

# schemeからCOUNTER ID を取り出す
timestamp_value=None
counter_value=None
counter_id_refs = {} 
counter_id_values = {} # Counter ID の値

for row in counterValueRoot.findall(".//row"):
    row_children = list(row)
    timestamp_cell = row_children[0] # 時間
    counter_id_cell = row_children[1] # GPUカウンター ID
    value_cell = row_children[2] # GPUカウンターの値

    if 'fmt' in counter_id_cell.attrib:        
        counter_id_ref = int(counter_id_cell.attrib['id']) # 0 ~ 30
        counter_id_value = int(counter_id_cell.attrib['fmt']) # 0 ~ 19
        counter_id_values[counter_id_value] = counter_id_value
        counter_id_refs[counter_id_ref] = int(counter_id_value)
    else:
        # Timestamp
        if 'fmt' in timestamp_cell.attrib:
            timestamp_value = timestamp_cell.attrib['fmt']
            row_count+=1

        # Counter ID
        if 'ref' in counter_id_cell.attrib:
            counter_id_ref = int(counter_id_cell.attrib['ref'])
            counter_id_value = counter_id_refs[counter_id_ref] 

        # Counter Value
        if 'fmt' in value_cell.attrib:
            counter_value = float(value_cell.attrib['fmt'])
            counters[counter_id_value]["values"].append(counter_value)
            counters[counter_id_value]["sum"] += counter_value
            counters[counter_id_value]["count"] += 1

            if ("max" in counters[counter_id_value]):
                counters[counter_id_value]["max"] = max(counter_value, counters[counter_id_value]["max"])
            else:
                counters[counter_id_value]["max"] = counter_value

# 出力
print(f'\tAVG\tMAX') # ヘッダー
for i in counter_id_values:
    counter = counters[i]
    counter_name = counter["name"]
    sum = counter["sum"]
    count = counter["count"]
    values = counter["values"]
    max = counter["max"]

    if (count == 0):
        avg = 0
    else:
        avg = sum / row_count

    print(f'{counter_name}\t{avg}\t{max}')

コマンドラインからpythonファイルを実行し、txtファイルへ出力します。

python3 collect_counters.py > result.txt

出力結果

コピー&ペーストでGoogleスプレッドシートに貼り付けられることができます。

result.txt
	AVG	MAX
F32 Utilization	14.15747796208535	22.5599
F16 Utilization	0.026617969984202204	1.9656
Texture Sample Limiter	78.23507310426538	99.8415
Texture Filtering Limiter	28.303441232227502	45.0571
Texture Cache Limiter	79.08311180884662	99.995
Texture Write Limiter	11.465882424960522	25.7937
Buffer Read Limiter	0.02923882306477092	0.2086
Buffer Write Limiter	0.0016479462875197458	0.0379
Threadgroup/Imageblock Load Limiter	0.013851658767772513	1.1633
Threadgroup/Imageblock Store Limiter	5.018091311216418	9.0984
Fragment Input Interpolation Limiter	7.076718404423376	11.1025
GPU Last Level Cache Limiter	68.80119206161135	88.168
Vertex Occupancy	0.0004071484992101104	0.0135
Fragment Occupancy	79.75370173775647	99.9522
Compute Occupancy	4.739336492890995e-07	0.0005
GPU Read Bandwidth	13.1058484992101	35.6089
GPU Write Bandwidth	2.6124018562401297	12.5913
MMU Limiter	84.83115106635047	99.9209
MMU Utilization	38.195075947867245	58.0024
Partial Renders Count	0	0.0

Instrument上で確認できる数値と比較すると、値が同じであることが確認できます。

関連記事

https://swet.dena.com/entry/2020/10/26/112627

Discussion