Python プロシージャハンドラーコードのプロファイリング

組み込みのコード・プロファイラーを使用することで、ハンドラーコードの実行に費やされた時間やメモリを調べることができます。プロファイラーは、プロシージャ・ハンドラーの各行の実行にどれだけの時間やメモリが費やされたかを示す情報を生成します。

プロファイラーを使用すると、以下のいずれかに焦点を当てたレポートを一度に作成できます。

  • 1行あたりの実行時間 には、このレポートでは、1行の実行回数、実行にかかった時間などが表示されます。

  • 1行あたりのメモリ使用量 には、このレポートでは1行あたりのメモリ使用量が表示されます。

プロファイラによって生成されたレポートは、指定したSnowflake 内部ユーザーステージ に保存されます。 GET_PYTHON_PROFILER_OUTPUT (SNOWFLAKE.CORE) システム関数を使用して、 プロファイラ出力 を読むことができます。

注釈

プロファイルはPythonの実行にパフォーマンスオーバーヘッドをもたらし、クエリのパフォーマンスに影響を与える可能性があります。これは開発、テスト、トラブルシューティングを目的としており、継続的な本番ワークロードでは有効にしないでください。

必要な権限

セッションレベルのパラメーターを設定しても権限チェックはトリガーされませんが、 ACTIVE_PYTHON_PROFILER セッションパラメーターを LINE または MEMORY のいずれかに設定してストアドプロシージャを実行すると、Snowflake は以下の権限をチェックします。

  • プロファイリング出力ステージの読み取り/書き込み権限が必要です。

  • プロファイルされたストアドプロシージャが 発呼者権限ストアドプロシージャ の場合、ストアドプロシージャで USAGE 権限を持つロールを使用する必要があります。

  • プロファイルされたストアドプロシージャが 所有者の権限のストアドプロシージャ の場合、そのストアドプロシージャで OWNERSHIP 権限のロールを使用する必要があります。

制限事項

  • ストアドプロシージャのみがサポートされています。UDFs のサポートはまだ可用性はありません。

  • 再帰プロファイリングは非対応です。指定したモジュールのトップレベル関数のみがプロファイルされます。関数の内部で定義された関数はそうではありません。

  • snowflake.snowpark API を介してクライアント側で作成されたストアドプロシージャのプロファイルはサポートされていません (たとえば、 Session.sproc.register から作成されたストアドプロシージャなど)。

  • joblib を通して並列に実行される Python 関数はプロファイルされません。

  • システム定義のストアドプロシージャはプロファイルできません。出力はゼロです。

使用状況

プロファイラーを使用するようにセットアップしたら、ストアドプロシージャをコールしてプロファイラー出力を生成するだけで使用できます。プロシージャの実行が終了すると、プロファイラーの出力は指定したステージのファイルに書き出されます。システム関数を使用してプロファイル出力を取得できます。

以下の手順に従って、プロファイルをセットアップし、使用してください.

  1. プロファイル出力を書き込む Snowflakeステージを指定します

    パラメーター PYTHON_PROFILER_TARGET_STAGE にステージの完全修飾名をセットします。

  2. プロファイラーを有効にし、プロファイルの対象を指定します。

    ACTIVE_PYTHON_PROFILER セッションパラメーターをセットします。

  3. ストアドプロシージャを呼び出します

    プロファイラーが有効になったら、ストアドプロシージャを呼び出します。

  4. プロファイリング出力の表示.

    実行終了後、プロファイリング出力は <query_id>_<sproc_name>.lprof または <query_id>_<sproc_name>.mprof というネーミングパターンで出力ステージにファイルとしてアップロードされます。

プロファイル出力を書き込むSnowflakeステージを指定します。

プロファイラーを実行する前に、レポートを保存するステージを指定する必要があります。ステージを指定するには、 PYTHON_PROFILER_TARGET_STAGE パラメーターにステージの完全修飾名をセットします。

  • セッションの間のみに出力を保存する仮ステージを使用します。

  • 永続ステージを使用して、セッションの範囲外でプロファイル出力を保存します。

以下の例のコードは、プロファイラー出力を受け取るための仮の profiler_output ステージ を作成します。

USE DATABASE my_database;
USE SCHEMA my_schema;

CREATE TEMPORARY STAGE profiler_output;
ALTER SESSION SET PYTHON_PROFILER_TARGET_STAGE = "my_database.my_schema.profiler_output";
Copy

プロファイラーを有効にし、プロファイルの対象を指定します。

ACTIVE_PYTHON_PROFILER セッション・パラメーターに、生成したいプロファイル・レポートの種類を指定する値をセットします。

  • プロファイルをライン使用アクティビティにフォーカスさせるには、以下のようにパラメーターを LINE (大文字と小文字を区別しない)値にセットします。

    ALTER SESSION SET ACTIVE_PYTHON_PROFILER = 'LINE';
    
    Copy
  • プロファイルでメモリ使用アクティビティに焦点を当てるには、以下のようにパラメーターを MEMORY の値(大文字と小文字は区別されない)にセットします。

    ALTER SESSION SET ACTIVE_PYTHON_PROFILER = 'MEMORY';
    
    Copy

ストアドプロシージャを呼び出します。

プロファイラーが有効になったら、ストアドプロシージャを呼び出します。

CALL YOUR_STORED_PROCEDURE();
Copy

デフォルトでは、プロファイラーがプロファイルするのはユーザーモジュールで定義されているメソッドです。プロファイルには他のモジュールも登録できます。詳細情報については、 プロファイル追加モジュール を参照してください。

プロファイル出力表示

実行終了後、プロファイリング出力は <query_id>_<sproc_name>.lprof または <query_id>_<sproc_name>.mprof というネーミングパターンで出力ステージにファイルとしてアップロードされます。

出力は、 SNOWFLAKE データベース のシステム関数 GET_PYTHON_PROFILER_OUTPUT からアクセスできます。

システム関数の署名の形式は以下の通りです。

SELECT SNOWFLAKE.CORE.GET_PYTHON_PROFILER_OUTPUT(<query_id>);
Copy

<query_id> を、プロファイリングを有効にしたストアドプロシージャ・クエリのクエリ ID に置き換えてください。

出力ステージの出力ファイルに直接アクセスすることもできます。詳細情報については、 ステージングされたファイルの表示 を参照してください。

注釈

システム関数は、 PYTHON_PROFILER_TARGET_STAGE パラメーターで指定されたステージからプロファイリング出力ファイルを探します。

子ストアドプロシージャのプロファイル出力は親プロシージャの出力に追加されません。子ストアドプロシージャの出力を表示するには、子プロシージャのクエリ ID のシステム関数を明示的に呼び出します。

プロファイル用追加モジュールの追加

デフォルトではインクルードされていないモジュールを、プロファイルのためにインクルードすることができます。追加モジュールをプロファイルに含めるには、 PYTHON_PROFILER_MODULES パラメーターに含めたいモジュール名をセットします。

デフォルトでは、モジュールで定義されたメソッドがプロファイルされます。これらの方法には以下のようなものがあります。

  • ハンドラーメソッド

  • モジュールで定義されているメソッド

  • パッケージや他のモジュールからインポートされたメソッド。

以下の例では、 handlerhelpersome_method がデフォルトでプロファイルされます。

CREATE OR REPLACE PROCEDURE my_sproc()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.10
PACKAGES = ('snowflake-snowpark-python', 'other_package')
HANDLER='handler'
AS $$
from other_package import some_method

def helper():
...

def handler(session):
...
$$;
Copy

PYTHON_PROFILER_MODULES パラメーターを持つモジュールのインクルード

PYTHON_PROFILER_MODULES パラメーターを使って、デフォルトでは含まれないようなプロファイルモジュールを含めることができます。この方法でモジュールをインクルードすると、そのモジュールから使用されるすべての関数がプロファイラの出力に含まれます。デフォルト値では、 PYTHON_PROFILER_MODULES パラメーターの値は空文字列 ('') になります。この場合、プロファイルはインラインハンドラーコード (もしあれば) のみをプロファイルします。

モジュールをプロファイリングに含めるには、パラメーターの値としてモジュール名をカンマ区切りのリストで指定します。

ALTER SESSION SET PYTHON_PROFILER_MODULES = 'module_a, my_module';
Copy

ステージングハンドラーコードのプロファイル

インラインではなくステージングされたハンドラーコード (ヘルパー関数を含む) をプロファイルするには、 PYTHON_PROFILER_MODULES パラメーターを使用して、ステージングされたハンドラーを明示的に指定する必要があります。

デフォルトでは、 インラインではなく、 ステージングされたハンドラーコードはプロファイルされません --- つまり、ハンドラーモジュールが IMPORTS 句で指定されている場合です。

例えば、デフォルトでは、このプロシージャは詳細なプロファイリング出力を生成しません。

CREATE OR REPLACE PROCEDURE test_udf_1()
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.8'
PACKAGES=('snowflake-snowpark-python')
HANDLER = 'test_python_import_main.my_udf'
IMPORTS = ('@stage1/test_python_import_main.py', '@stage2/test_python_import_module.py');
Copy

ステージング・コードをプロファイリングに含めるには、 PYTHON_PROFILER_MODULES パラメーターの値として、ステージング・モジュール名をカンマ区切りのリストで指定します。

ALTER SESSION SET PYTHON_PROFILER_MODULES = 'test_python_import_main, test_python_import_module';
Copy

この例のコードは、プロファイラーを使用して行の使用状況のレポートを生成し、取得する方法を示しています。

CREATE OR REPLACE PROCEDURE last_n_query_duration(last_n number, total number)
RETURNS string
LANGUAGE PYTHON
RUNTIME_VERSION=3.8
PACKAGES=('snowflake-snowpark-python')
HANDLER='main'
AS
$$
import snowflake.snowpark.functions as funcs

def main(session, last_n, total):
  # create sample dataset to emulate id + elapsed time
  session.sql('''
  CREATE OR REPLACE TABLE sample_query_history (query_id INT, elapsed_time FLOAT)
  ''').collect()
  session.sql('''
  INSERT INTO sample_query_history
  SELECT
  seq8() AS query_id,
  uniform(0::float, 100::float, random()) as elapsed_time
  FROM table(generator(rowCount => {0}));'''.format(total)).collect()

  # get the mean of the last n query elapsed time
  df = session.table('sample_query_history').select(
    funcs.col('query_id'),
    funcs.col('elapsed_time')).limit(last_n)

  pandas_df = df.to_pandas()
  mean_time = pandas_df.loc[:, 'ELAPSED_TIME'].mean()
  del pandas_df
  return mean_time
$$;

CREATE TEMPORARY STAGE profiler_output;
ALTER SESSION SET PYTHON_PROFILER_TARGET_STAGE = "my_database.my_schema.profiler_output";
ALTER SESSION SET ACTIVE_PYTHON_PROFILER = 'LINE';

-- Sample 1 million from 10 million records
CALL last_n_query_duration(1000000, 10000000);

SELECT SNOWFLAKE.CORE.GET_PYTHON_PROFILER_OUTPUT(last_query_id());
Copy

ライン・プロファイラーの出力は次のようになります。

Handler Name: main
Python Runtime Version: 3.8
Modules Profiled: ['main_module']
Timer Unit: 0.001 s

Total Time: 8.96127 s
File: _udf_code.py
Function: main at line 4

Line #      Hits        Time  Per Hit   % Time  Line Contents
==============================================================
    4                                           def main(session, last_n, total):
    5                                               # create sample dataset to emulate id + elapsed time
    6         1        122.3    122.3      1.4      session.sql('''
    7                                                   CREATE OR REPLACE TABLE sample_query_history (query_id INT, elapsed_time FLOAT)''').collect()
    8         2       7248.4   3624.2     80.9      session.sql('''
    9                                               INSERT INTO sample_query_history
    10                                               SELECT
    11                                               seq8() AS query_id,
    12                                               uniform(0::float, 100::float, random()) as elapsed_time
    13         1          0.0      0.0      0.0      FROM table(generator(rowCount => {0}));'''.format(total)).collect()
    14
    15                                               # get the mean of the last n query elapsed time
    16         3         58.6     19.5      0.7      df = session.table('sample_query_history').select(
    17         1          0.0      0.0      0.0          funcs.col('query_id'),
    18         2          0.0      0.0      0.0          funcs.col('elapsed_time')).limit(last_n)
    19
    20         1       1528.4   1528.4     17.1      pandas_df = df.to_pandas()
    21         1          3.2      3.2      0.0      mean_time = pandas_df.loc[:, 'ELAPSED_TIME'].mean()
    22         1          0.3      0.3      0.0      del pandas_df
    23         1          0.0      0.0      0.0      return mean_time

メモリプロファイラーの出力は次のようになります。

ALTER SESSION SET ACTIVE_PYTHON_PROFILER = 'MEMORY';

Handler Name: main
Python Runtime Version: 3.8
Modules Profiled: ['main_module']
File: _udf_code.py
Function: main at line 4

Line #   Mem usage    Increment  Occurrences  Line Contents
=============================================================
    4    245.3 MiB    245.3 MiB           1   def main(session, last_n, total):
    5                                             # create sample dataset to emulate id + elapsed time
    6    245.8 MiB      0.5 MiB           1       session.sql('''
    7                                                 CREATE OR REPLACE TABLE sample_query_history (query_id INT, elapsed_time FLOAT)''').collect()
    8    245.8 MiB      0.0 MiB           2       session.sql('''
    9                                             INSERT INTO sample_query_history
    10                                             SELECT
    11                                             seq8() AS query_id,
    12                                             uniform(0::float, 100::float, random()) as elapsed_time
    13    245.8 MiB      0.0 MiB           1       FROM table(generator(rowCount => {0}));'''.format(total)).collect()
    14
    15                                             # get the mean of the last n query elapsed time
    16    245.8 MiB      0.0 MiB           3       df = session.table('sample_query_history').select(
    17    245.8 MiB      0.0 MiB           1           funcs.col('query_id'),
    18    245.8 MiB      0.0 MiB           2           funcs.col('elapsed_time')).limit(last_n)
    19
    20    327.9 MiB     82.1 MiB           1       pandas_df = df.to_pandas()
    21    328.9 MiB      1.0 MiB           1       mean_time = pandas_df.loc[:, 'ELAPSED_TIME'].mean()
    22    320.9 MiB     -8.0 MiB           1       del pandas_df
    23    320.9 MiB      0.0 MiB           1       return mean_time