sixty

pour les services Python

Était-ce votre code, ou était-ce de l’attente ?

Tous les autres agents peuvent vous dire qu’une fonction a ralenti. Celui-ci vous dit laquelle des deux moitiés — le code qui calculait, ou le temps passé à attendre un verrou, une place dans le pool ou une extension C qui tenait le GIL. Elles appellent des correctifs opposés, et tous les autres chiffres par appel restent corrects pendant que la seconde se produit, ce qui explique que rien d’autre ne l’attrape.

Ce qu’il mesure

Le CPU par appel
time.thread_time() est par thread et coûte environ 100 ns à lire, et un span synchrone possède son thread pendant toute sa durée — c’est donc un chiffre exact plutôt qu’une part d’une horloge à l’échelle du processus répartie entre tout ce qui tournait par ailleurs.
L’attente par appel
Le reste du temps propre d’une fonction : un verrou tenu sur plus de travail qu’avant, un pool de connexions sans place libre, une extension C qui a pris le GIL et ne l’a pas rendu. C’est invisible pour toutes les autres mesures ici, parce que le code n’est pas plus lent — il fait la queue.
Chaque requête psycopg
Versions 2 et 3, patchées au curseur plutôt qu’à la connexion, si bien que SQLAlchemy posé sur psycopg est mesuré aussi. Lignes renvoyées, colonnes, taille de la réponse, et la requête normalisée.
Quelle fonction a émis quelle requête
Une requête est créditée à la fonction au-dessus d’elle, donc un N+1 atterrit sur la fonction qui contient la boucle plutôt que d’être réparti sur une requête qui, seule, a toujours eu l’air correcte.
Vos routes HTTP, telles qu’elles sont écrites
Flask par l’url_rule correspondante, Django par la route telle qu’elle apparaît dans urls.py, et tout ce qui est ASGI ou WSGI par ce que vous enveloppez. Le fil dit donc GET /users/<int:user_id>/orders au lieu d’une opération par identifiant d’utilisateur.

Ce qu’il trouve

Chacune de ces choses est comparée à la version précédente : le constat nomme donc le changement plutôt que le jour.

Votre propre code qui ralentit, coupé en deux

Le constat qui dit vers quel correctif se tourner. Le temps de calcul a augmenté et pas l’attente : regardez dans la fonction. L’attente a augmenté et le CPU n’a pas bougé : regardez le pool, le verrou, ou ce avec quoi le processus partage désormais un thread.

Un N+1 qui n’était pas là à la version précédente

Une fonction qui faisait une requête par appel en fait maintenant vingt. Chacune est rapide et correctement indexée, donc aucune mesure de temps ne bouge assez pour se remarquer — seul le nombre.

Une requête qui s’est mise à lire toute la table

Les lignes par appel sont passées de 30 à 30 000. Sur une base chaude avec des données de taille développement cela bouge à peine l’horloge, et cela met la production à terre quand il y a un an de lignes derrière.

Quelque chose désormais appelé dans une boucle

Inchangé par appel — même vitesse, mêmes données — et appelé des centaines de fois là où il l’était une seule.

Une requête qui ne renvoie plus rien, en silence

Réussie, rien de levé, pas lente, et revenue vide là où elle revenait avec des lignes. La plupart des façons dont un service comme celui-ci casse font baisser les chiffres plutôt que monter.

Les erreurs, groupées par leur provenance

Par emplacement dans le code plutôt que par message, donc un bug fait une entrée quel que soit le nombre de chaînes différentes qu’il formate.

Ce qui arrive dans votre fil

Pas un tableau de bord à lire. Une carte par constat, avec les chiffres, la version qui les a changés, et les preuves dont votre agent de code a besoin pour écrire le correctif.

Ce sont les deux régressions qu’apps/demo-python livre exprès, contre le jeu de données que crée son script de seed — environ 1 000 utilisateurs et 30 000 commandes. Vous pouvez le lancer et les regarder arriver.

lignesorders.get_user_orders
30 lignes30 000 lignes1000×

Une refonte a laissé tomber la clause WHERE, donc chaque appel récupère toute la table et la filtre en Python. L’horloge a à peine bougé ; le nombre de lignes est tout le signal.

depuis la version v2, contre la v1

N+1orders.enrich_orders
1 requête20 requêtes20×

Une recherche groupée est devenue une recherche par commande. Chaque requête est individuellement rapide et correctement indexée, ce qui est exactement ce qui lui permet de survivre à la relecture et au staging.

depuis la version v2, contre la v1

Comment ça s’installe

Remettez-le à l’agent de code que vous avez déjà ouvert. Il détermine si ce projet est Flask, Django, FastAPI ou un simple callable WSGI, met le middleware au bon endroit, et marque la couche service — puis corrige ce qui est trouvé, dans la même conversation.

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):
    ...

L’identifiant de version est récupéré tout seul chez Vercel, Render, Railway, Fly, Heroku ou GitHub Actions, et SIXTY_RELEASE le fixe partout ailleurs. La première comparaison arrive après votre prochain déploiement, parce qu’il n’y a rien à quoi comparer une version isolée.

Ce qu’il ne fait pas

  • Un span qui s’étend au-delà d’un await partage son thread avec tout ce que la boucle a exécuté par ailleurs, donc il ne remonte aucun CPU plutôt qu’un CPU gonflé. Un service asyncio obtient du CPU par fonction synchrone et par requête, mais pas par requête HTTP — la réponse honnête plutôt qu’une réponse plausible.
  • Les plans de requête ne sont pas capturés. Postgres est là et l’EXPLAIN fonctionnerait ; cet agent n’en émet pas encore, donc « elle a cessé d’utiliser son index » arrive sur Node et Ruby et pas ici.
  • psycopg est le seul pilote instrumenté. SQLAlchemy sur psycopg est mesuré, parce que le curseur en dessous l’est — asyncpg et les pilotes MySQL ne le sont pas du tout.
  • Sous gunicorn ou uwsgi, chaque worker remonte indépendamment et le collecteur les fusionne. C’est correct, et bon à savoir quand vous lisez un chiffre qui décrit un seul processus.
  • Rien ici ne marque vos fonctions à votre place. Node obtient cela d’une transformation au build et Python n’a pas d’équivalent, donc le décorateur ou instrument_module est une étape que vous faites — et la sauter laisse le fil avec des routes et des requêtes et rien entre les deux.

Si vous êtes ici pour l’une de ces raisons

Découvrez ce qu’a fait votre dernier changement.

Essayez !

Vous construisez autre chose ? Voyez services Node, services Go ou applications Ruby et Rails — ces pages sont différentes, et ce que nous voyons aussi.

Surveillance des performances pour Python — Django, Flask, FastAPI et Postgres — Sixty