pipenvの起動を早くしたという話のimport timeの確認を3.7以前の頃はどうしてたかという話

pipenvの起動を早くしたという話です。良い話ですね。

https://dev.to/methane/how-to-speed-up-python-application-startup-time-nkf

ところでリンク先の記事はpython3.7で導入されるPYTHONPROFILEIMPORTTIMEの紹介なのですが。古の民(3.7以前の人々)はどのように解決していたのでしょうか?似たようなことを昔やっていたことがあり。その時はimport hookで無理矢理頑張る方法をとったりしていました。

modulegraph

完全には同じ事はできないのですが。import hookを可能な限り早めのタイミングで時刻を記録可能なものに置き換えて実行するみたいな感じにしていました。

以前書いたmoduleknifeというrepositoryのmodulegraphというコマンドで似たようなことをしています。

https://github.com/podhmo/moduleknife

もちろんpython(pure python)のレイヤーでのimport hookの置き換えなので、素のまっさらなpythonが立ち上がるまでの部分は計測不能なのですが。通常のライブラリやアプリを作る上で素のまっさらなpythonが立ち上がる時間自体はどうやっても早くできない固定コストという風に考えれば納得できなくもないということで無視しちゃってます。

pipenvのimport timeの高速化の部分について

同じ事ができるか実際に試してみると。

00import.py

import pipenv

modulegraphというコマンド経由でpythonスクリプトを実行すると、ここでgraphviz用のdotファイルが生成されます。

$ modulegraph --metadata=time --outfile=./00.dot 00import.py
$ graphviz -Tsvg 00.svg > 00.svg

ちなみに.dotファイルの中のコメントを見ると元の記事の通りにIpythonとpkg_resource関連が怪しいということがわかります(実行にかかった時間で降順でsort)。

// load ~/my/lib/python3.6/site-packages/pipenv/__init__.py ... 0.5924477577209473s
// load ~/my/lib/python3.6/site-packages/pipenv/cli.py ... 0.5918803215026855s
// load ~/my/lib/python3.6/site-packages/pipenv/patched/dotenv/__init__.py ... 0.2650315761566162s
// load ~/my/lib/python3.6/site-packages/pipenv/patched/dotenv/ipython.py ... 0.2640550136566162s
// load ~/my/lib/python3.6/site-packages/IPython/__init__.py ... 0.2633070945739746s
// load ~/my/lib/python3.6/site-packages/IPython/terminal/embed.py ... 0.23813724517822266s
// load ~/my/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py ... 0.1785898208618164s
// load ~/my/lib/python3.6/site-packages/pipenv/patched/pip/__init__.py ... 0.17690563201904297s
// load ~/my/lib/python3.6/site-packages/pipenv/patched/pip/utils/__init__.py ... 0.11338138580322266s
// load ~/my/lib/python3.6/site-packages/pipenv/patched/pip/_vendor/pkg_resources/__init__.py ... 0.10068082809448242s
// load ~/my/lib/python3.6/site-packages/pkg_resources/__init__.py ... 0.0988779067993164s
// load ~/my/lib/python3.6/site-packages/urllib3/__init__.py ... 0.045993804931640625s

補足するとこの時間は例えば foo パッケージから bar パッケージを読むという構造になっていた場合に以下の様な形で計測されます。

tf0 = import fooの前の時刻
  import foo
  tb0 = import barの前の時刻
    import bar
  tb1 = import barの後の時刻
tf1 = import fooの後の時刻

このとき
import fooの時間は tf1 - tf0
import barの時間は tb1 - tb0

これだけでも記事と同様にpkg_resourceの読み込みとdotenv経由のipythonの読み込みが遅いということはわかります。 ついでにgraphvizでグラフを生成してみましょう。

$ graphviz -Tsvg 00.svg > 00.svg

すごく大きくて何だかわからないグラフが表示されます(蛇足ですが画像ではなくsvgの場合検索が効くのが便利です)。

f:id:podhmo:20180124033811p:plain
生成されたgraphvizの巨大なグラフ

一応探してみるとpkg_resourceやipythonの部分が見つかります。

f:id:podhmo:20180124033928p:plainf:id:podhmo:20180124033923p:plain
探してみるとIpythonやpkg_resourceの時間も表示されている

個人的なpythonのformatter(yapf)のコマンドをpypiにuploadした。

個人的なpythonのformatter(yapf)のコマンドをpypiにuploadした。githubにだけおいておいてインストールするのが結構めんどくさくなったので。とは言え個人的なものであることは間違いないのでなるべく名前がかぶらないようにユニークなprefixなどを付けて公開した。

yayapf

インストールされるコマンド自体はyayapfという名前なのだけれど。元々利用しているyapf自体がyeat another python formatterの略なので"ya (yapf)"という感じ。

install

installはpipで。yayapfでもかぶらないとは思ったけれど。yapfと紛らわしいし。そもそも名前空間をあんまり汚染したくないという気持ちもあったので。poというprefixを付けた。

$ pip install po-yayapf

difference

yapfとの違いはほとんど以下の一点。

  • from foo import bar, baz 的なもののimportが気に入らなかった。

yapfの設定だと($HOME/.config/yapf/styleなどの設定によって変わるけれど)。上手くこのimportの形を死守できなかった。

from a_very_long_or_indented_module_name_yada_yad import (
    long_argument_1,
    long_argument_2,
    long_argument_3,
    long_argument_1,
    long_argument_2,
    long_argument_3,
)

時々こうなってしまう(後述)。

from a_very_long_or_indented_module_name_yada_yad import (
    long_argument_1, long_argument_2, long_argument_3, long_argument_1, long_argument_2,
    long_argument_3
)

なってほしかった挙動は

  • 末尾には必ずコンマがついている
  • importされたsymbol毎に改行されている

diffが発生しにくい感じなので。

yapfを利用したときのちょっとしたコードの書き方の使い分け

yapf自体はヒューリスティックなスコアで改行などをするかしないか決めていたはずなので。完全な整形結果の予想というのはできない(たぶん)のだけれど。 コンマが存在する部分に改行を加えたいか加えたくないかで末尾のコンマを打つか打たないかを決めている。

例えば、末尾にコンマのない関数呼びしの引数部分やdictは値毎に改行されない。

def f(x, y, z):
    return {"x": 1, "y": 2, "z": 3, "k": k}

これを以下の様にすると

-def f(x, y, z):
+def f(x, y, z,):
     return {"x": 1, "y": 2, "z": 3, "k": k}

引数部分が値毎にされる。

def f(
    x,
    y,
    z,
):
    return {"x": 1, "y": 2, "z": 3, "k": k}

dictも同様。

def f(
    x,
    y,
    z,
):
    return {
        "x": 1,
        "y": 2,
        "z": 3,
        "k": k,
    }

※ import文でもこのルールは使われるのだけれど。importに限っては元のコードがカンマで終わっていなくても常にカンマを付加してimportされて欲しいのでつけるみたいな処理が入っている。

style

ちなみに現在のyapfの設定は以下の様な感じ。

[style]
based_on_style = pep8
column_limit = 100
dedent_closing_brackets=true
spaces_around_power_operator = true
split_arguments_when_comma_terminated = true
join_multiple_lines = false