みんなが知らないPythonプロファイラScaleneを紹介する

# はじめに
この記事は朝日新聞社 Advent Calendar 2023の15日目の記事です。
題名の通り、みんなが（知っているのかどうかを筆者が）知らないPythonプロファイラ **Scalene** について紹介します。

# プロファイラとは
プロファイラはコードのパフォーマンスの計測・分析のためのツールで、パフォーマンス改善やバグの発見などに役立ちます。関数レベルで計測できるもの、行レベルで計測できるものなど様々ありますが、筆者がよく使っていたのは下記のツールでした。

### line_profiler
行レベルで実行時間や実行回数を計測できるツールです。

https://github.com/pyutils/line_profiler

### memory_profiler
こちらはメモリ使用量の計測のためのツールです。line_profiler同様、行レベルで計測できます。

https://github.com/pythonprofilers/memory_profiler

上記については下記などが参考になります。

https://qiita.com/aratana_tamutomo/items/aa3b723a3dd7a44e45d6


# Scaleneでできること
それでは本題のScaleneについて紹介します。

https://github.com/plasma-umass/scalene

ScaleneはPython用の高性能なCPU/GPU/メモリプロファイラで、他のPythonプロファイラよりも高機能で高速なツールです。
メモリに加えてGPUの計測、システム時間（スリープやI/O）の計測などにも対応しており、オンリーワンのツールであることがわかります。
（なのですが、日本語の記事は見たことがない...）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/2819916e-11e8-03b9-c606-72314750bc6e.png)
https://github.com/plasma-umass/scalene?tab=readme-ov-file#performance-and-features

後で紹介しますが、出力はGUIで確認できたり、JSON/HTMLファイルに書き出したりと便利に使えます。
また、OpenAIのGPT-3.5/GPT-4を使ったコードの最適化提案機能もあります。要チェックですね。


# 使ってみる
pipでインストールします。
```bash
pip install scalene
```
※バージョンは検証時最新の1.5.31.1です。


今回は簡単に下記のコードで検証します。
```python
# test.py
def sample_function():
    total = []
    for i in range(10 ** 8):
        total.append(i)
    return total

sample_function()
```

## 実行方法
### コマンドラインで実行
```
scalene [options] your_program.py
```
オプションとして出力方法・項目などを指定します。
<details><summary>オプション一覧はこちら</summary>

```
-h, --help            show this help message and exit
--version             prints the version number for this release of Scalene and exits
--column-width COLUMN_WIDTH
                      Column width for profile output (default: 132)
--outfile OUTFILE     file to hold profiler output (default: stdout)
--html                output as HTML (default: web)
--json                output as JSON (default: web)
--cli                 forces use of the command-line
--stacks              collect stack traces
--web                 opens a web tab to view the profile (saved as 'profile.html')
--no-browser          doesn't open a web tab; just saves the profile ('profile.html')
--viewer              opens the Scalene web UI and exits.
--reduced-profile     generate a reduced profile, with non-zero lines only (default: False)
--profile-interval PROFILE_INTERVAL
                      output profiles every so many seconds (default: inf)
--cpu                 profile CPU time (default:  True )
--cpu-only            profile CPU time (deprecated: use --cpu )
--gpu                 profile GPU time and memory (default: False )
--memory              profile memory (default: True )
--profile-all         profile all executed code, not just the target program (default: only the target program)
--profile-only PROFILE_ONLY
                      profile only code in filenames that contain the given strings, separated by commas (default: no restrictions)
--profile-exclude PROFILE_EXCLUDE
                      do not profile code in filenames that contain the given strings, separated by commas (default: no restrictions)
--use-virtual-time    measure only CPU time, not time spent in I/O or blocking (default: False)
--cpu-percent-threshold CPU_PERCENT_THRESHOLD
                      only report profiles with at least this percent of CPU time (default: 1%)
--cpu-sampling-rate CPU_SAMPLING_RATE
                      CPU sampling rate (default: every 0.01s)
--allocation-sampling-window ALLOCATION_SAMPLING_WINDOW
                      Allocation sampling window size, in bytes (default: 10485767 bytes)
--malloc-threshold MALLOC_THRESHOLD
                      only report profiles with at least this many allocations (default: 100)
--program-path PROGRAM_PATH
                      The directory containing the code to profile (default: the path to the profiled program)
--memory-leak-detector
                      EXPERIMENTAL: report likely memory leaks (default: True)
--on                  start with profiling on (default)
--off                 start with profiling off
```
</details>

デフォルトでは実行時にブラウザが開き、GUIで結果が表示されます。
※ssh接続先で実行する場合、ブラウザに遷移することなく標準出力に出力されました。

:::note warn
コードの実行時間が短すぎるとScaleneによるプロファイリングは動かず、下記のエラーが表示されます。
Scalene: Program did not run for long enough to profile.
:::

### Jupyterで実行
Jupyterではマジックコマンドを使って実行します。

```python
%load_ext scalene

# Line mode
%scrun [options] statement

# Cell mode
%%scalene [options]
your code here
```

:::note warn
Jupyterでの実行時にはメモリ計測はできません...。
Scalene extension successfully loaded. Note: Scalene currently only supports CPU+GPU profiling inside Jupyter notebooks. For full Scalene profiling, use the command line version.
:::


ちなみにデコレータを使う方法やコード内に直接定義する方法もあります（詳しくは[こちら](https://github.com/plasma-umass/scalene?tab=readme-ov-file#using-scalene)）。


## GUIで確認
下記を実行すると...
```bash
scalene test.py
```
結果はこんな感じ👇
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/e7079b46-2c43-992a-b145-54415d963d36.png)
（結果は`profile.html`として保存されており、そのHTMLが開かれているようです。）

各項目にカーソルを合わせると詳細な数字が確認でき、各列名をクリックすることで自由にソートもできます。
またここでは示されていませんが、動作環境にNVIDIA GPUがインストールされている場合、GPUの実行時間も計測できます。

さらに、上部の`advanced options`からLLMによる最適化提案機能を設定できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/e614d43d-7946-bcc4-ccee-1dec20a10309.png)
OpenAIのAPIキーを入力し、モデルをGPT-3.5とGPT-4から選択します（残高がないとエラーするので注意）。
そしてコードに表示されている💥や⚡をクリックすると、こんな感じで書き換え案が提示されます🎉
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/801d6773-9232-dc8a-4642-e2bf62789c01.png)

また、Jupyterでもこのように結果が表示されますが、メモリ使用量などは取得できないのでコマンドラインから実行するのが吉です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/74241c20-0421-56a4-7f34-dc9950c086d3.png)


## CLIで確認
下記を実行すると...
```bash
scalene --cli test.py 
```
結果はこんな感じ👇
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1809679/53bcecb3-ab34-a85b-a054-3f1eae0abf22.png)
手軽に見たい場合はこれで十分ですね。

## ファイルに出力

### HTML
```bash
scalene --cli --html --outfile output.html test.py
```
`--cli`オプションを指定しないとブラウザで開かれてしまいます。
HTMLファイルはデフォルトでも生成されているので、出力ファイル名を指定したいときに使うとよさそうです。

### JSON

```bash
scalene --cli --json --outfile output.json test.py
```
データをこねこねしたいときはJSONファイルに出力しましょう。
`--cli`オプションを指定しないとブラウザで開かれてしまうだけでなく、JSON形式で保存されなくなってしまうのでMUSTです。


<details><summary>出力されるJSON（一部省略）</summary>

```json
{
    "alloc_samples": 938,
    "args": [
        "test.py"
    ],
    "elapsed_time_sec": 9.31862497329712,
    "filename": "hogehoge",
    "files": {
        "hogehoge/test.py": {
            "functions": [
                {
                    "line": "Scalene.sample_function",
                    "lineno": 1,
                    "memory_samples": [...],
                    "n_avg_mb": 2890.0064544677734,
                    "n_copy_mb_s": 0.0,
                    "n_core_utilization": 0.10783966435646258,
                    "n_cpu_percent_c": 28.423653243865136,
                    "n_cpu_percent_python": 57.8480782413048,
                    "n_gpu_avg_memory_mb": 0.0,
                    "n_gpu_peak_memory_mb": 0.0,
                    "n_gpu_percent": 0,
                    "n_growth_mb": 2890.0064544677734,
                    "n_malloc_mb": 3813.505753517151,
                    "n_mallocs": 1,
                    "n_peak_mb": 2890.0064544677734,
                    "n_python_fraction": 0.9999966878476239,
                    "n_sys_percent": 13.728268514829912,
                    "n_usage_fraction": 1.0
                }
            ],
            "imports": [],
            "leaks": {},
            "lines": [
                {
                    "end_outermost_loop": 5,
                    "end_region_line": 5,
                    "line": "def sample_function():\n",
                    "lineno": 1,
                    "memory_samples": [],
                    "n_avg_mb": 0.0,
                    "n_copy_mb_s": 0.0,
                    "n_core_utilization": 0.0,
                    "n_cpu_percent_c": 0.0,
                    "n_cpu_percent_python": 0.0,
                    "n_gpu_avg_memory_mb": 0.0,
                    "n_gpu_peak_memory_mb": 0.0,
                    "n_gpu_percent": 0,
                    "n_growth_mb": 0.0,
                    "n_malloc_mb": 0.0,
                    "n_mallocs": 0,
                    "n_peak_mb": 0.0,
                    "n_python_fraction": 0,
                    "n_sys_percent": 0.0,
                    "n_usage_fraction": 0.0,
                    "start_outermost_loop": 1,
                    "start_region_line": 1
                },
                ...
            ],
            "percent_cpu_time": 100.0
        }
    },
    "gpu": false,
    "growth_rate": 0.08109565504538821,
    "max_footprint_fname": "hogehoge/test.py",
    "max_footprint_lineno": 3,
    "max_footprint_mb": 3843.3808002471924,
    "memory": true,
    "program": "hogehoge/test.py",
    "samples": [...],
    "stacks": []
}
```
</details>


# おわりに
以上Scaleneの紹介でした。
本記事が少しでも皆さんの目に留まれば、みんなが（知っているのかどうかを筆者が）知らないことはなくなりそうです。