sixty

Python のサービス向け

あなたのコードだったのか、待っていただけなのか。

他のどのエージェントも「関数が遅くなった」とは言えます。これは、そのどちら半分かを言います — 計算していたコードなのか、ロックやプールの空きや GIL を握った C 拡張を待っていた時間なのか。この2つは正反対の対処を必要とし、後者が起きているあいだ他の呼び出し単位の数値はすべて正しいままです。だから他の何もこれを捕まえられません。

何を測るか

呼び出しあたりの CPU
time.thread_time() はスレッド単位で、読み取りに約100ナノ秒しかかかりません。そして同期のスパンは、その全区間にわたって自分のスレッドを占有します — つまりこれは、プロセス全体の時計を「同時に動いていた何か」と按分した割合ではなく、正確な数値です。
呼び出しあたりの待ち時間
関数の自分時間の残りです。以前より長い処理をまたいで保持されたロック、空きのないコネクションプール、GIL を取って返さなかった C 拡張。ここにある他のどの計測からも見えません。コードが遅くなったのではなく、順番待ちをしているからです。
psycopg のすべてのクエリ
バージョン2と3を、接続ではなくカーソルにパッチします。だから psycopg の上に乗った SQLAlchemy も測れます。返却行数、カラム数、レスポンスサイズ、そして正規化された文。
どの関数がどのクエリを出したか
クエリはその上にある関数に帰属するので、N+1 はループを持つ関数の上に着地します。単体ではいつも問題なく見えていたクエリの側に散らばりません。
あなたの HTTP ルートを、書かれたままの形で
Flask は一致した url_rule で、Django は urls.py に書かれているとおりのルートで、ASGI や WSGI のものは包んだ対象で。だからフィードには、ユーザーIDごとに別々の操作ではなく GET /users/<int:user_id>/orders と出ます。

何を見つけるか

どれも1つ前のリリースと比較されます。だから検出結果は、日付ではなく変更を名指しします。

自分のコードが遅くなった、その内訳

どちらの対処に手を伸ばすべきかを名指しする検出です。計算時間が増えて待ち時間が増えていない:関数の中を見る。待ち時間が増えて CPU が動いていない:プールか、ロックか、いまそのプロセスがスレッドを共有している相手を見る。

前のリリースにはなかった N+1

1リクエストにつき1本だったクエリが、いまは20本。どれも速く、正しくインデックスも効いているので、気づくほど時間は動きません — 動くのは回数だけです。

テーブル全体を読み始めたクエリ

1回あたりの行数が30から30,000へ。開発規模のデータと温まったデータベースが相手ならほとんど時計は動かず、1年分の行が後ろについたときに本番を落とします。

いまはループの中で呼ばれているもの

1回あたりは変わらず — 同じ速さ、同じデータ — それでも、以前は1回だった呼び出しが数百回になっています。

静かに何も返さなくなったクエリ

成功し、例外も出さず、遅くもなく、以前は行が返っていたのに空で返ってきた。この種のサービスが壊れ方の多くは、数字を大きくではなく小さくします。

エラーを、出どころでまとめて

メッセージではなくコード上の位置でまとめます。だから1つのバグは、どれだけ違う文字列に整形されようと1件です。

フィードに何が届くか

読むためのダッシュボードではありません。検出1件につきカード1枚。数値と、それを変えたリリースと、コーディングエージェントが修正を書くのに必要な証拠が付いています。

これは apps/demo-python がわざと出荷している2つの回帰で、そのシードスクリプトが作るデータ — およそ1,000ユーザーと30,000件の注文 — が相手です。実際に走らせて、届くところを見られます。

行数orders.get_user_orders
30行30,000行1000×

リファクタリングで WHERE 句が落ち、呼び出しのたびにテーブル全体を取ってきて Python で絞り込んでいます。時計はほとんど動かず、行数がシグナルのすべてです。

リリース v2 以降、v1 との比較

N+1orders.enrich_orders
1本20本20×

まとめて引いていたものが、注文ごとの取得になりました。どのクエリも単体では速く、正しくインデックスも効いています。まさにそれが、レビューとステージングを通り抜けさせる理由です。

リリース v2 以降、v1 との比較

導入方法

すでに開いているコーディングエージェントに渡してください。このプロジェクトが Flask なのか Django なのか FastAPI なのか、素の WSGI callable なのかを判断し、ミドルウェアを正しい位置に置き、サービス層に印を付けます — そして同じ会話の中で、見つかったものを直します。

claude mcp add sixty \
  -e SIXTY_API_KEY=sixty_sk_… \
  -e SIXTY_ENDPOINT=https://ingest.sixty.sh \
  -- npx -y @sixty-sh/mcp

# or by hand:
pip install sixty-sh

# wsgi.py, asgi.py or main.py — before any connection is opened
import sixty
sixty.init()

# then one of these, whichever this project is:
#   Flask   instrument_flask(app)
#   Django  SixtyMiddleware, first in MIDDLEWARE
#   ASGI    SixtyASGIMiddleware
#   WSGI    SixtyMiddleware around the callable

# and the code between the view and the database
@sixty.trace
def get_user_orders(user_id):
    ...

リリース識別子は Vercel、Render、Railway、Fly、Heroku、GitHub Actions から自動で拾われ、それ以外の場所では SIXTY_RELEASE で指定します。最初の比較は次のデプロイのあとに届きます。1つのリリースには比べる相手がないからです。

できないこと

  • await をまたぐスパンは、その間ループが動かした他のものとスレッドを共有します。だから水増しした CPU ではなく、CPU なしとして報告します。asyncio のサービスでは、同期関数ごと・クエリごとの CPU は得られますが、リクエストごとには得られません — もっともらしい答えではなく、正直な答えです。
  • クエリプランは取得しません。Postgres はそこにあり、EXPLAIN も動くはずですが、このエージェントはまだ発行していません。だから「インデックスを使わなくなった」は Node と Ruby には届き、ここには届きません。
  • 計測しているドライバは psycopg だけです。psycopg の上の SQLAlchemy は、下のカーソルが計測されているので測れます — asyncpg と MySQL のドライバはまったく測れません。
  • gunicorn や uwsgi の下では各ワーカーが個別に報告し、コレクタがそれをまとめます。これは正しい動作ですが、1プロセスを表す数値を読むときには知っておく価値があります。
  • ここではあなたの関数に印を付けてくれるものはありません。Node はビルド時の変換でそれを得ますが、Python には同等のフックがないので、デコレータか instrument_module はあなたが踏む一歩です — そこを飛ばすと、フィードにはルートとクエリだけが残り、その間に何もなくなります。

これが理由でここに来たのなら

最後の変更が何をしたのか、確かめてください。

試してみる

別のものを作っていますか? Node のサービス、Go のサービス、Ruby と Rails のアプリ のページもあります — 見えるものが違うので、内容も違います。

Python 向けパフォーマンス監視 — Django・Flask・FastAPI・Postgres — Sixty