Vous pouvez modifier le format de fichier pour générer des profils speedscope ou des données brutes avec le paramètre --format
. Voir py-spy record --help
pour plus d'informations sur d'autres options, notamment la modification du taux d'échantillonnage, le filtrage pour inclure uniquement les threads qui contiennent le GIL, le profilage des extensions C natives, l'affichage des identifiants de thread, le profilage des sous-processus et plus encore.
Top affiche une vue en direct des fonctions qui prennent le plus de temps dans votre programme Python, similaire à la commande top d'Unix. Exécuter py-spy avec :
py-spy top --pid 12345
# OR
py-spy top -- python myprogram.py
fera apparaître une vue de haut niveau de mise à jour en direct de votre programme python :
py-spy peut également afficher la pile d'appels actuelle pour chaque thread python avec la commande dump
:
py-spy dump --pid 12345
Cela videra les piles d'appels pour chaque thread et quelques autres informations de base sur le processus vers la console :
Ceci est utile dans le cas où vous avez juste besoin d'une seule pile d'appels pour déterminer où votre programme Python est bloqué. Cette commande a également la possibilité d'imprimer les variables locales associées à chaque cadre de pile en définissant l'indicateur --locals
.
Ce projet vise à vous permettre de profiler et de déboguer n'importe quel programme Python en cours d'exécution, même si le programme dessert le trafic de production.
Bien qu'il existe de nombreux autres projets de profilage Python, presque tous nécessitent une modification du programme profilé d'une manière ou d'une autre. Habituellement, le code de profilage s'exécute à l'intérieur du processus Python cible, ce qui ralentira et modifiera le fonctionnement du programme. Cela signifie qu'il n'est généralement pas sûr d'utiliser ces profileurs pour résoudre des problèmes de débogage dans les services de production, car ils auront généralement un impact notable sur les performances.
py-spy fonctionne en lisant directement la mémoire du programme python à l'aide de l'appel système process_vm_readv sous Linux, de l'appel vm_read sous OSX ou de l'appel ReadProcessMemory sous Windows.
La détermination de la pile d'appels du programme Python se fait en examinant la variable globale PyInterpreterState pour obtenir tous les threads Python exécutés dans l'interpréteur, puis en itérant sur chaque PyFrameObject dans chaque thread pour obtenir la pile d'appels. Étant donné que l'ABI Python change entre les versions, nous utilisons le bindgen de Rust pour générer différentes structures Rust pour chaque classe d'interpréteur Python qui nous intéresse et utilisons ces structures générées pour déterminer la disposition de la mémoire dans le programme Python.
Obtenir l'adresse mémoire de l'interpréteur Python peut être un peu délicat en raison de la randomisation de la disposition de l'espace d'adressage. Si l'interpréteur Python cible est livré avec des symboles, il est assez facile de déterminer l'adresse mémoire de l'interpréteur en déréférençant les variables interp_head
ou _PyRuntime
selon la version de Python. Cependant, de nombreuses versions de Python sont livrées avec des binaires supprimés ou sans les fichiers de symboles PDB correspondants sous Windows. Dans ces cas, nous parcourons la section BSS à la recherche d'adresses qui semblent pointer vers un PyInterpreterState valide et vérifions si la disposition de cette adresse correspond à ce que nous attendons.
Oui! py-spy prend en charge le profilage des extensions Python natives écrites dans des langages comme C/C++ ou Cython, sur Linux et Windows x86_64. Vous pouvez activer ce mode en passant --native
sur la ligne de commande. Pour de meilleurs résultats, vous devez compiler votre extension Python avec des symboles. Il convient également de noter que pour les programmes Cython, py-spy a besoin du fichier C ou C++ généré afin de renvoyer les numéros de ligne du fichier .pyx d'origine. Lisez l'article de blog pour plus d'informations.
En passant l'indicateur --subprocesses
à l'enregistrement ou à la vue supérieure, py-spy inclura également la sortie de tout processus python qui est un processus enfant du programme cible. Ceci est utile pour profiler les applications qui utilisent des pools de tâches multitraitements ou gunicorn. py-spy surveillera les nouveaux processus en cours de création, s'y attachera automatiquement et inclura des échantillons de ceux-ci dans la sortie. La vue d'enregistrement inclura le PID et la ligne de commande de chaque programme dans la pile d'appels, les sous-processus apparaissant comme enfants de leurs processus parents.
py-spy fonctionne en lisant la mémoire d'un processus Python différent, et cela peut ne pas être autorisé pour des raisons de sécurité en fonction de votre système d'exploitation et des paramètres de votre système. Dans de nombreux cas, l'exécution en tant qu'utilisateur root (avec sudo ou similaire) contourne ces restrictions de sécurité. OSX nécessite toujours d'être exécuté en tant que root, mais sous Linux, cela dépend de la manière dont vous lancez py-spy et des paramètres de sécurité du système.
Sous Linux, la configuration par défaut consiste à exiger les autorisations root lors de l'attachement à un processus qui n'est pas un enfant. Pour py-spy, cela signifie que vous pouvez créer un profil sans accès root en demandant à py-spy de créer le processus ( py-spy record -- python myprogram.py
), mais l'attachement à un processus existant en spécifiant un PID nécessitera généralement root ( sudo py-spy record --pid 123456
). Vous pouvez supprimer cette restriction sous Linux en définissant la variable sysctl ptrace_scope.
py-spy tente d'inclure uniquement les traces de pile des threads qui exécutent activement du code et d'exclure les threads en veille ou inactifs. Lorsque cela est possible, py-spy tente d'obtenir ces informations sur l'activité du thread à partir du système d'exploitation : en lisant /proc/PID/stat
sous Linux, en utilisant l'appel mach thread_basic_info sous OSX et en regardant si le SysCall actuel est connu pour être inactif. sous Windows.
Il existe cependant certaines limites à cette approche qui peuvent faire en sorte que les threads inactifs soient toujours marqués comme actifs. Tout d'abord, nous devons obtenir ces informations sur l'activité du thread avant de mettre le programme en pause, car les obtenir à partir d'un programme en pause le fera toujours indiquer qu'il est inactif. Cela signifie qu'il existe une condition de concurrence potentielle, dans laquelle nous obtenons l'activité du thread, puis le thread est dans un état différent lorsque nous obtenons la trace de la pile. L'interrogation du système d'exploitation pour l'activité des threads n'est pas encore implémentée pour les processeurs FreeBSD et i686/ARM sous Linux. Sous Windows, les appels bloqués sur IO ne seront pas encore marqués comme inactifs, par exemple lors de la lecture d'une entrée depuis stdin. Enfin, sur certains appels Linux, le ptrace attach que nous utilisons peut provoquer le réveil momentané des threads inactifs, provoquant des faux positifs lors de la lecture à partir de procfs. Pour ces raisons, nous disposons également d'une solution heuristique de secours qui marque certains appels connus en python comme étant inactifs.
Vous pouvez désactiver cette fonctionnalité en définissant l'indicateur --idle
, qui inclura les images que py-spy considère comme inactives.
Nous obtenons l'activité GIL en examinant la valeur threadid pointée par le symbole _PyThreadState_Current
pour Python 3.6 et versions antérieures et en trouvant l'équivalent à partir de la structure _PyRuntime
dans Python 3.7 et versions ultérieures. Ces symboles peuvent ne pas être inclus dans votre distribution Python, ce qui entraînera l'échec de la résolution du thread qui détient le GIL. L'utilisation actuelle du GIL est également affichée dans la vue top
sous la forme %GIL.
Passer l'indicateur --gil
n'inclura que les traces des threads qui conservent le verrouillage global de l'interprète. Dans certains cas, cela peut être une vue plus précise de la façon dont votre programme python passe son temps, mais vous devez être conscient que cela manquera d'activité dans les extensions qui publient le GIL alors qu'elles sont encore actives.
OSX dispose d'une fonctionnalité appelée Protection de l'intégrité du système qui empêche même l'utilisateur root de lire la mémoire de n'importe quel binaire situé dans /usr/bin. Malheureusement, cela inclut l'interpréteur Python fourni avec OSX.
Il existe plusieurs manières différentes de résoudre ce problème :
L'exécution de py-spy à l'intérieur d'un conteneur Docker entraînera également généralement une erreur d'autorisations refusées, même lors de l'exécution en tant que root.
Cette erreur est due au fait que Docker restreint l'appel système process_vm_readv que nous utilisons. Cela peut être remplacé en définissant --cap-add SYS_PTRACE
lors du démarrage du conteneur Docker.
Vous pouvez également modifier le fichier yaml docker-compose
your_service:
cap_add:
- SYS_PTRACE
Notez que vous devrez redémarrer le conteneur Docker pour que ce paramètre prenne effet.
Vous pouvez également utiliser py-spy à partir du système d'exploitation hôte pour profiler un processus en cours d'exécution dans le conteneur Docker.
py-spy a besoin SYS_PTRACE
pour pouvoir lire la mémoire du processus. Kubernetes supprime cette fonctionnalité par défaut, ce qui entraîne l'erreur
Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'
La méthode recommandée pour résoudre ce problème consiste à modifier la spécification et à ajouter cette fonctionnalité. Pour un déploiement, cela se fait en ajoutant ceci à Deployment.spec.template.spec.containers
securityContext:
capabilities:
add:
- SYS_PTRACE
Plus de détails à ce sujet ici : https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container Notez que cela supprimera les pods existants et les créera à nouveau .
Alpine Python se désengage des nombreuses roues manylinux
: pypa/pip#3969 (commentaire). Vous pouvez remplacer ce comportement pour utiliser pip pour installer py-spy sur Alpine en allant :
echo 'manylinux1_compatible = True' > /usr/local/lib/python3.7/site-packages/_manylinux.py
Vous pouvez également télécharger un binaire musl à partir de la page des versions de GitHub.
En définissant l'option --nonblocking
, py-spy ne mettra pas en pause le python cible à partir duquel vous effectuez le profilage. Bien que l'impact sur les performances de l'échantillonnage d'un processus avec py-spy soit généralement extrêmement faible, la définition de cette option évitera totalement d'interrompre votre programme Python en cours d'exécution.
Avec cette option définie, py-spy lira à la place l'état de l'interpréteur à partir du processus python pendant son exécution. Étant donné que les appels que nous utilisons pour lire la mémoire ne sont pas atomiques et que nous devons émettre plusieurs appels pour obtenir une trace de pile, cela signifie que nous obtenons parfois des erreurs lors de l'échantillonnage. Cela peut se manifester par un taux d'erreur accru lors de l'échantillonnage ou par l'inclusion de trames de pile partielles dans la sortie.
Pas encore =).
S'il y a des fonctionnalités que vous aimeriez voir dans py-spy, recherchez le problème approprié ou créez-en un nouveau qui décrit les fonctionnalités manquantes.
py-spy suit la spécification CLICOLOR, donc en définissant CLICOLOR_FORCE=1
dans votre environnement, py-spy imprimera une sortie colorée même lorsqu'elle sera redirigée vers un téléavertisseur.
py-spy est fortement inspiré de l'excellent travail de Julia Evans sur rbspy. En particulier, le code pour générer les fichiers flamegraph et speedscope provient directement de rbspy, et ce projet utilise les caisses read-process-memory et proc-maps qui ont été dérivées de rbspy.
py-spy est publié sous la licence MIT, voir le fichier LICENSE pour le texte intégral.