simple-python-audit 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_python_audit-1.3.0/PKG-INFO +180 -0
- simple_python_audit-1.3.0/README.md +161 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/pyproject.toml +2 -2
- simple_python_audit-1.3.0/src/simple_python_audit/engine.py +352 -0
- simple_python_audit-1.3.0/src/simple_python_audit.egg-info/PKG-INFO +180 -0
- simple_python_audit-1.2.0/PKG-INFO +0 -56
- simple_python_audit-1.2.0/README.md +0 -37
- simple_python_audit-1.2.0/src/simple_python_audit/engine.py +0 -226
- simple_python_audit-1.2.0/src/simple_python_audit.egg-info/PKG-INFO +0 -56
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/setup.cfg +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit/__init__.py +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit/server.py +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/SOURCES.txt +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/dependency_links.txt +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/entry_points.txt +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/requires.txt +0 -0
- {simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-python-audit
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Ferramenta de profiling com widget flutuante, exportação CSV e servidor de gerenciamento de logs.
|
|
5
|
+
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: odoo,profiler,performance,pyinstrument,audit
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Framework :: Odoo
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pyinstrument>=4.6.0
|
|
18
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
19
|
+
|
|
20
|
+
# Simple Python Audit Tool 🚀
|
|
21
|
+
|
|
22
|
+
Ferramenta completa para medição de performance e auditoria de variáveis para Python.
|
|
23
|
+
|
|
24
|
+
## 📦 Instalação
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install .
|
|
28
|
+
# Ou via GitHub
|
|
29
|
+
pip install git+https://github.com/sadson/simple_python_audit.git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🎯 Como Usar
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from simple_python_audit import profile
|
|
36
|
+
|
|
37
|
+
@profile(
|
|
38
|
+
html=True,
|
|
39
|
+
trace=True,
|
|
40
|
+
deep=True,
|
|
41
|
+
print_terminal=True,
|
|
42
|
+
save_csv=True,
|
|
43
|
+
output_path="/tmp/simple_python_audit_perf",
|
|
44
|
+
)
|
|
45
|
+
def meu_metodo_lento(self):
|
|
46
|
+
# Seu código aqui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Parâmetros do decorador
|
|
50
|
+
|
|
51
|
+
| Parâmetro | Padrão | Descrição |
|
|
52
|
+
|------------------|-------------------------------------|---------------------------------------------------------------------------|
|
|
53
|
+
| `html` | `False` | Gera relatório interativo em HTML com widget flutuante |
|
|
54
|
+
| `trace` | `False` | Rastreia chamadas de funções e coleta métricas por função |
|
|
55
|
+
| `deep` | `False` | Aumenta granularidade do profiler (intervalo de 0,0001 s em vez de 0,001 s) |
|
|
56
|
+
| `print_terminal` | `False` | Imprime tabela de métricas formatada no terminal ao final da execução |
|
|
57
|
+
| `save_csv` | `False` | Salva os dados em arquivos CSV prontos para análise com pandas/IA |
|
|
58
|
+
| `output_path` | `"/tmp/simple_python_audit_perf"` | Diretório onde os relatórios (HTML e CSV) serão gravados |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📊 Saída no Terminal (`print_terminal=True`)
|
|
63
|
+
|
|
64
|
+
Exibe uma tabela formatada diretamente no stdout ao término da função:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
==============================================================
|
|
68
|
+
AUDIT: meu_metodo_lento | 1.2345s
|
|
69
|
+
==============================================================
|
|
70
|
+
CPU Total: 0.4567s
|
|
71
|
+
CPU Usage: 37.0%
|
|
72
|
+
Memory Current: 12.345 MB
|
|
73
|
+
Memory Peak: 15.678 MB
|
|
74
|
+
Memory Avg/Call: 2.34 KB/call
|
|
75
|
+
Total Calls Traced: 1234
|
|
76
|
+
--------------------------------------------------------------
|
|
77
|
+
Top Functions by CPU Time:
|
|
78
|
+
Function Calls Total Avg Peak
|
|
79
|
+
meu_metodo_lento 1 1.2300s 1.23000s 1.23000s
|
|
80
|
+
_query 42 0.4500s 0.01071s 0.02100s
|
|
81
|
+
...
|
|
82
|
+
--------------------------------------------------------------
|
|
83
|
+
Top Functions by Memory:
|
|
84
|
+
Function Calls Total KB Avg KB Peak KB
|
|
85
|
+
_query 42 512.30 12.20 48.50
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 💾 Exportação CSV (`save_csv=True`)
|
|
92
|
+
|
|
93
|
+
Gera dois arquivos CSV em `output_path`, ideais para carregar em um `DataFrame` do pandas, alimentar uma IA ou construir dashboards.
|
|
94
|
+
|
|
95
|
+
### `AUDIT_{func}_{timestamp}_summary.csv`
|
|
96
|
+
|
|
97
|
+
Uma linha por execução com as métricas globais da chamada. Acumulando múltiplas execuções é possível rastrear regressões de performance ao longo do tempo.
|
|
98
|
+
|
|
99
|
+
| Coluna | Tipo | Descrição |
|
|
100
|
+
|-------------------------|---------|------------------------------------------------|
|
|
101
|
+
| `timestamp` | int | Unix timestamp da execução |
|
|
102
|
+
| `func_name` | str | Nome da função decorada |
|
|
103
|
+
| `duration_s` | float | Tempo total de execução (segundos) |
|
|
104
|
+
| `cpu_total_s` | float | Tempo de CPU consumido (segundos) |
|
|
105
|
+
| `cpu_pct` | float | Percentual de uso de CPU |
|
|
106
|
+
| `mem_current_mb` | float | Memória alocada ao final (MB) |
|
|
107
|
+
| `mem_peak_mb` | float | Pico de memória durante a execução (MB) |
|
|
108
|
+
| `mem_avg_kb_per_call` | float | Média de memória por chamada de função (KB) |
|
|
109
|
+
| `total_calls_traced` | int | Total de chamadas de função rastreadas |
|
|
110
|
+
|
|
111
|
+
### `AUDIT_{func}_{timestamp}_trace.csv`
|
|
112
|
+
|
|
113
|
+
Uma linha por função rastreada (requer `trace=True`). Permite identificar os gargalos com precisão.
|
|
114
|
+
|
|
115
|
+
| Coluna | Tipo | Descrição |
|
|
116
|
+
|--------------------|-------|-----------------------------------------------|
|
|
117
|
+
| `timestamp` | int | Unix timestamp — chave para junção com summary |
|
|
118
|
+
| `func_name` | str | Nome da função decorada |
|
|
119
|
+
| `called_function` | str | Nome da função rastreada |
|
|
120
|
+
| `call_count` | int | Número de chamadas |
|
|
121
|
+
| `total_time_s` | float | Tempo total acumulado (segundos) |
|
|
122
|
+
| `avg_time_s` | float | Tempo médio por chamada (segundos) |
|
|
123
|
+
| `peak_time_s` | float | Pico de tempo em uma única chamada (segundos) |
|
|
124
|
+
| `total_mem_kb` | float | Memória total alocada (KB) |
|
|
125
|
+
| `avg_mem_kb` | float | Memória média por chamada (KB) |
|
|
126
|
+
| `peak_mem_kb` | float | Pico de memória em uma única chamada (KB) |
|
|
127
|
+
|
|
128
|
+
### Exemplo de análise com pandas
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import pandas as pd
|
|
132
|
+
import glob
|
|
133
|
+
|
|
134
|
+
# Carregar todos os resumos de execuções
|
|
135
|
+
summary = pd.concat([pd.read_csv(f) for f in glob.glob("/tmp/simple_python_audit_perf/*_summary.csv")])
|
|
136
|
+
summary["timestamp"] = pd.to_datetime(summary["timestamp"], unit="s")
|
|
137
|
+
summary.sort_values("timestamp").plot(x="timestamp", y="duration_s", title="Evolução do tempo de execução")
|
|
138
|
+
|
|
139
|
+
# Carregar trace de uma execução específica
|
|
140
|
+
trace = pd.read_csv("/tmp/simple_python_audit_perf/AUDIT_meu_metodo_lento_1234567890_trace.csv")
|
|
141
|
+
trace.sort_values("total_time_s", ascending=False).head(10)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🌐 Relatório HTML (`html=True`)
|
|
147
|
+
|
|
148
|
+
Gera um arquivo `AUDIT_{func}_{timestamp}.html` com um widget flutuante interativo contendo:
|
|
149
|
+
|
|
150
|
+
- **Painel de rastreamento (🚀):** tabela de chamadas de função com busca em tempo real
|
|
151
|
+
- **Painel de métricas (📊):** cards de CPU e memória, tabelas de CPU e memória por função
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 🛠️ Servidor de Auditoria
|
|
156
|
+
|
|
157
|
+
Interface web para visualizar e gerenciar os arquivos de relatório gerados.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Via linha de comando
|
|
161
|
+
simple-python-audit-server --port 8080 --dir /tmp/simple_python_audit_perf
|
|
162
|
+
|
|
163
|
+
# Via Python
|
|
164
|
+
from simple_python_audit import start_server
|
|
165
|
+
start_server(port=8080, directory="/tmp/simple_python_audit_perf")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🔗 Integração com Odoo
|
|
171
|
+
|
|
172
|
+
Ao instalar em um ambiente com Odoo, o decorador `profile` é injetado automaticamente em `odoo.api`:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from odoo import api
|
|
176
|
+
|
|
177
|
+
@api.profile(html=True, trace=True, print_terminal=True, save_csv=True)
|
|
178
|
+
def action_processar(self):
|
|
179
|
+
...
|
|
180
|
+
```
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Simple Python Audit Tool 🚀
|
|
2
|
+
|
|
3
|
+
Ferramenta completa para medição de performance e auditoria de variáveis para Python.
|
|
4
|
+
|
|
5
|
+
## 📦 Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
# Ou via GitHub
|
|
10
|
+
pip install git+https://github.com/sadson/simple_python_audit.git
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 🎯 Como Usar
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from simple_python_audit import profile
|
|
17
|
+
|
|
18
|
+
@profile(
|
|
19
|
+
html=True,
|
|
20
|
+
trace=True,
|
|
21
|
+
deep=True,
|
|
22
|
+
print_terminal=True,
|
|
23
|
+
save_csv=True,
|
|
24
|
+
output_path="/tmp/simple_python_audit_perf",
|
|
25
|
+
)
|
|
26
|
+
def meu_metodo_lento(self):
|
|
27
|
+
# Seu código aqui
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Parâmetros do decorador
|
|
31
|
+
|
|
32
|
+
| Parâmetro | Padrão | Descrição |
|
|
33
|
+
|------------------|-------------------------------------|---------------------------------------------------------------------------|
|
|
34
|
+
| `html` | `False` | Gera relatório interativo em HTML com widget flutuante |
|
|
35
|
+
| `trace` | `False` | Rastreia chamadas de funções e coleta métricas por função |
|
|
36
|
+
| `deep` | `False` | Aumenta granularidade do profiler (intervalo de 0,0001 s em vez de 0,001 s) |
|
|
37
|
+
| `print_terminal` | `False` | Imprime tabela de métricas formatada no terminal ao final da execução |
|
|
38
|
+
| `save_csv` | `False` | Salva os dados em arquivos CSV prontos para análise com pandas/IA |
|
|
39
|
+
| `output_path` | `"/tmp/simple_python_audit_perf"` | Diretório onde os relatórios (HTML e CSV) serão gravados |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📊 Saída no Terminal (`print_terminal=True`)
|
|
44
|
+
|
|
45
|
+
Exibe uma tabela formatada diretamente no stdout ao término da função:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
==============================================================
|
|
49
|
+
AUDIT: meu_metodo_lento | 1.2345s
|
|
50
|
+
==============================================================
|
|
51
|
+
CPU Total: 0.4567s
|
|
52
|
+
CPU Usage: 37.0%
|
|
53
|
+
Memory Current: 12.345 MB
|
|
54
|
+
Memory Peak: 15.678 MB
|
|
55
|
+
Memory Avg/Call: 2.34 KB/call
|
|
56
|
+
Total Calls Traced: 1234
|
|
57
|
+
--------------------------------------------------------------
|
|
58
|
+
Top Functions by CPU Time:
|
|
59
|
+
Function Calls Total Avg Peak
|
|
60
|
+
meu_metodo_lento 1 1.2300s 1.23000s 1.23000s
|
|
61
|
+
_query 42 0.4500s 0.01071s 0.02100s
|
|
62
|
+
...
|
|
63
|
+
--------------------------------------------------------------
|
|
64
|
+
Top Functions by Memory:
|
|
65
|
+
Function Calls Total KB Avg KB Peak KB
|
|
66
|
+
_query 42 512.30 12.20 48.50
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 💾 Exportação CSV (`save_csv=True`)
|
|
73
|
+
|
|
74
|
+
Gera dois arquivos CSV em `output_path`, ideais para carregar em um `DataFrame` do pandas, alimentar uma IA ou construir dashboards.
|
|
75
|
+
|
|
76
|
+
### `AUDIT_{func}_{timestamp}_summary.csv`
|
|
77
|
+
|
|
78
|
+
Uma linha por execução com as métricas globais da chamada. Acumulando múltiplas execuções é possível rastrear regressões de performance ao longo do tempo.
|
|
79
|
+
|
|
80
|
+
| Coluna | Tipo | Descrição |
|
|
81
|
+
|-------------------------|---------|------------------------------------------------|
|
|
82
|
+
| `timestamp` | int | Unix timestamp da execução |
|
|
83
|
+
| `func_name` | str | Nome da função decorada |
|
|
84
|
+
| `duration_s` | float | Tempo total de execução (segundos) |
|
|
85
|
+
| `cpu_total_s` | float | Tempo de CPU consumido (segundos) |
|
|
86
|
+
| `cpu_pct` | float | Percentual de uso de CPU |
|
|
87
|
+
| `mem_current_mb` | float | Memória alocada ao final (MB) |
|
|
88
|
+
| `mem_peak_mb` | float | Pico de memória durante a execução (MB) |
|
|
89
|
+
| `mem_avg_kb_per_call` | float | Média de memória por chamada de função (KB) |
|
|
90
|
+
| `total_calls_traced` | int | Total de chamadas de função rastreadas |
|
|
91
|
+
|
|
92
|
+
### `AUDIT_{func}_{timestamp}_trace.csv`
|
|
93
|
+
|
|
94
|
+
Uma linha por função rastreada (requer `trace=True`). Permite identificar os gargalos com precisão.
|
|
95
|
+
|
|
96
|
+
| Coluna | Tipo | Descrição |
|
|
97
|
+
|--------------------|-------|-----------------------------------------------|
|
|
98
|
+
| `timestamp` | int | Unix timestamp — chave para junção com summary |
|
|
99
|
+
| `func_name` | str | Nome da função decorada |
|
|
100
|
+
| `called_function` | str | Nome da função rastreada |
|
|
101
|
+
| `call_count` | int | Número de chamadas |
|
|
102
|
+
| `total_time_s` | float | Tempo total acumulado (segundos) |
|
|
103
|
+
| `avg_time_s` | float | Tempo médio por chamada (segundos) |
|
|
104
|
+
| `peak_time_s` | float | Pico de tempo em uma única chamada (segundos) |
|
|
105
|
+
| `total_mem_kb` | float | Memória total alocada (KB) |
|
|
106
|
+
| `avg_mem_kb` | float | Memória média por chamada (KB) |
|
|
107
|
+
| `peak_mem_kb` | float | Pico de memória em uma única chamada (KB) |
|
|
108
|
+
|
|
109
|
+
### Exemplo de análise com pandas
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import pandas as pd
|
|
113
|
+
import glob
|
|
114
|
+
|
|
115
|
+
# Carregar todos os resumos de execuções
|
|
116
|
+
summary = pd.concat([pd.read_csv(f) for f in glob.glob("/tmp/simple_python_audit_perf/*_summary.csv")])
|
|
117
|
+
summary["timestamp"] = pd.to_datetime(summary["timestamp"], unit="s")
|
|
118
|
+
summary.sort_values("timestamp").plot(x="timestamp", y="duration_s", title="Evolução do tempo de execução")
|
|
119
|
+
|
|
120
|
+
# Carregar trace de uma execução específica
|
|
121
|
+
trace = pd.read_csv("/tmp/simple_python_audit_perf/AUDIT_meu_metodo_lento_1234567890_trace.csv")
|
|
122
|
+
trace.sort_values("total_time_s", ascending=False).head(10)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🌐 Relatório HTML (`html=True`)
|
|
128
|
+
|
|
129
|
+
Gera um arquivo `AUDIT_{func}_{timestamp}.html` com um widget flutuante interativo contendo:
|
|
130
|
+
|
|
131
|
+
- **Painel de rastreamento (🚀):** tabela de chamadas de função com busca em tempo real
|
|
132
|
+
- **Painel de métricas (📊):** cards de CPU e memória, tabelas de CPU e memória por função
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🛠️ Servidor de Auditoria
|
|
137
|
+
|
|
138
|
+
Interface web para visualizar e gerenciar os arquivos de relatório gerados.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Via linha de comando
|
|
142
|
+
simple-python-audit-server --port 8080 --dir /tmp/simple_python_audit_perf
|
|
143
|
+
|
|
144
|
+
# Via Python
|
|
145
|
+
from simple_python_audit import start_server
|
|
146
|
+
start_server(port=8080, directory="/tmp/simple_python_audit_perf")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🔗 Integração com Odoo
|
|
152
|
+
|
|
153
|
+
Ao instalar em um ambiente com Odoo, o decorador `profile` é injetado automaticamente em `odoo.api`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from odoo import api
|
|
157
|
+
|
|
158
|
+
@api.profile(html=True, trace=True, print_terminal=True, save_csv=True)
|
|
159
|
+
def action_processar(self):
|
|
160
|
+
...
|
|
161
|
+
```
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simple-python-audit"
|
|
7
|
-
version = "1.
|
|
8
|
-
description = "Ferramenta de profiling com widget flutuante e servidor de gerenciamento de logs."
|
|
7
|
+
version = "1.3.0"
|
|
8
|
+
description = "Ferramenta de profiling com widget flutuante, exportação CSV e servidor de gerenciamento de logs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv as _csv
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import logging
|
|
7
|
+
import tracemalloc
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from pyinstrument import Profiler
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
def html_escape(text):
|
|
15
|
+
return str(text).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
16
|
+
|
|
17
|
+
def _print_terminal(func_name, duration, cpu_total, current_mem, peak_mem,
|
|
18
|
+
total_calls, stats_data, mem_stats_data, trace):
|
|
19
|
+
"""Print audit metrics as a formatted table to stdout."""
|
|
20
|
+
cpu_pct = round(cpu_total / duration * 100, 1) if duration > 0 else 0
|
|
21
|
+
mem_current_mb = round(current_mem / 1024 / 1024, 3)
|
|
22
|
+
mem_peak_mb = round(peak_mem / 1024 / 1024, 3)
|
|
23
|
+
mem_avg = f"{round(peak_mem / total_calls / 1024, 2)} KB/call" if total_calls > 0 else "N/A"
|
|
24
|
+
|
|
25
|
+
width = 62
|
|
26
|
+
sep = "=" * width
|
|
27
|
+
thin = "-" * width
|
|
28
|
+
|
|
29
|
+
print(f"\n{sep}")
|
|
30
|
+
print(f" AUDIT: {func_name} | {duration:.4f}s")
|
|
31
|
+
print(sep)
|
|
32
|
+
print(f" {'CPU Total:':<22} {cpu_total:.4f}s")
|
|
33
|
+
print(f" {'CPU Usage:':<22} {cpu_pct}%")
|
|
34
|
+
print(f" {'Memory Current:':<22} {mem_current_mb} MB")
|
|
35
|
+
print(f" {'Memory Peak:':<22} {mem_peak_mb} MB")
|
|
36
|
+
print(f" {'Memory Avg/Call:':<22} {mem_avg}")
|
|
37
|
+
if trace:
|
|
38
|
+
print(f" {'Total Calls Traced:':<22} {total_calls}")
|
|
39
|
+
print(thin)
|
|
40
|
+
|
|
41
|
+
if trace and stats_data:
|
|
42
|
+
print(f" Top Functions by CPU Time:")
|
|
43
|
+
header = f" {'Function':<30} {'Calls':>6} {'Total':>9} {'Avg':>9} {'Peak':>9}"
|
|
44
|
+
print(header)
|
|
45
|
+
print(f" {'-'*30} {'-'*6} {'-'*9} {'-'*9} {'-'*9}")
|
|
46
|
+
for s in stats_data[:15]:
|
|
47
|
+
name = s['name'][:30]
|
|
48
|
+
print(f" {name:<30} {s['count']:>6} {s['total']:>8}s {s['avg']:>8}s {s['peak']:>8}s")
|
|
49
|
+
print(thin)
|
|
50
|
+
|
|
51
|
+
if trace and mem_stats_data:
|
|
52
|
+
print(f" Top Functions by Memory:")
|
|
53
|
+
header = f" {'Function':<30} {'Calls':>6} {'Total KB':>10} {'Avg KB':>8} {'Peak KB':>8}"
|
|
54
|
+
print(header)
|
|
55
|
+
print(f" {'-'*30} {'-'*6} {'-'*10} {'-'*8} {'-'*8}")
|
|
56
|
+
for s in mem_stats_data[:15]:
|
|
57
|
+
name = s['name'][:30]
|
|
58
|
+
print(f" {name:<30} {s['count']:>6} {s['total_mem']:>10} {s['avg_mem']:>8} {s['peak_mem']:>8}")
|
|
59
|
+
print(thin)
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _save_csv(output_path, func_name, timestamp, duration, cpu_total, current_mem, peak_mem,
|
|
65
|
+
total_calls, stats_data, mem_stats_data, trace):
|
|
66
|
+
"""
|
|
67
|
+
Save audit metrics to CSV files suitable for pandas/AI analysis.
|
|
68
|
+
|
|
69
|
+
Generates two files:
|
|
70
|
+
- AUDIT_{func_name}_{timestamp}_summary.csv : one row with all summary metrics
|
|
71
|
+
- AUDIT_{func_name}_{timestamp}_trace.csv : per-function trace rows (when trace=True)
|
|
72
|
+
"""
|
|
73
|
+
cpu_pct = round(cpu_total / duration * 100, 1) if duration > 0 else 0
|
|
74
|
+
|
|
75
|
+
# --- Summary CSV ---
|
|
76
|
+
summary_path = os.path.join(output_path, f"AUDIT_{func_name}_{timestamp}_summary.csv")
|
|
77
|
+
summary_row = {
|
|
78
|
+
"timestamp": timestamp,
|
|
79
|
+
"func_name": func_name,
|
|
80
|
+
"duration_s": round(duration, 6),
|
|
81
|
+
"cpu_total_s": round(cpu_total, 6),
|
|
82
|
+
"cpu_pct": cpu_pct,
|
|
83
|
+
"mem_current_mb": round(current_mem / 1024 / 1024, 6),
|
|
84
|
+
"mem_peak_mb": round(peak_mem / 1024 / 1024, 6),
|
|
85
|
+
"mem_avg_kb_per_call": round(peak_mem / total_calls / 1024, 4) if total_calls > 0 else None,
|
|
86
|
+
"total_calls_traced": total_calls if trace else None,
|
|
87
|
+
}
|
|
88
|
+
with open(summary_path, "w", newline="", encoding="utf-8") as f:
|
|
89
|
+
writer = _csv.DictWriter(f, fieldnames=list(summary_row.keys()))
|
|
90
|
+
writer.writeheader()
|
|
91
|
+
writer.writerow(summary_row)
|
|
92
|
+
|
|
93
|
+
# --- Trace CSV (only when trace=True) ---
|
|
94
|
+
if trace and (stats_data or mem_stats_data):
|
|
95
|
+
trace_path = os.path.join(output_path, f"AUDIT_{func_name}_{timestamp}_trace.csv")
|
|
96
|
+
# Merge cpu and mem stats by function name
|
|
97
|
+
mem_by_name = {s["name"]: s for s in mem_stats_data}
|
|
98
|
+
fieldnames = [
|
|
99
|
+
"timestamp", "func_name", "called_function",
|
|
100
|
+
"call_count",
|
|
101
|
+
"total_time_s", "avg_time_s", "peak_time_s",
|
|
102
|
+
"total_mem_kb", "avg_mem_kb", "peak_mem_kb",
|
|
103
|
+
]
|
|
104
|
+
with open(trace_path, "w", newline="", encoding="utf-8") as f:
|
|
105
|
+
writer = _csv.DictWriter(f, fieldnames=fieldnames)
|
|
106
|
+
writer.writeheader()
|
|
107
|
+
for s in stats_data:
|
|
108
|
+
mem = mem_by_name.get(s["name"], {})
|
|
109
|
+
writer.writerow({
|
|
110
|
+
"timestamp": timestamp,
|
|
111
|
+
"func_name": func_name,
|
|
112
|
+
"called_function": s["name"],
|
|
113
|
+
"call_count": s["count"],
|
|
114
|
+
"total_time_s": s["total"],
|
|
115
|
+
"avg_time_s": s["avg"],
|
|
116
|
+
"peak_time_s": s["peak"],
|
|
117
|
+
"total_mem_kb": mem.get("total_mem", None),
|
|
118
|
+
"avg_mem_kb": mem.get("avg_mem", None),
|
|
119
|
+
"peak_mem_kb": mem.get("peak_mem", None),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return summary_path
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def profile(html=False, output_path="/tmp/simple_python_audit_perf", deep=False, trace=False,
|
|
126
|
+
print_terminal=False, save_csv=False):
|
|
127
|
+
def decorator(func):
|
|
128
|
+
@wraps(func)
|
|
129
|
+
def wrapper(*args, **kwargs):
|
|
130
|
+
interval = 0.0001 if deep else 0.001
|
|
131
|
+
profiler = Profiler(interval=interval)
|
|
132
|
+
call_stats = defaultdict(lambda: {
|
|
133
|
+
'count': 0, 'total_time': 0.0, 'peak_time': 0.0,
|
|
134
|
+
'total_mem': 0, 'peak_mem': 0,
|
|
135
|
+
'stack': [], 'mem_stack': []
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
def trace_calls(frame, event, arg):
|
|
139
|
+
if event == 'call':
|
|
140
|
+
f_name = frame.f_code.co_name
|
|
141
|
+
call_stats[f_name]['count'] += 1
|
|
142
|
+
call_stats[f_name]['stack'].append(time.time())
|
|
143
|
+
call_stats[f_name]['mem_stack'].append(
|
|
144
|
+
tracemalloc.get_traced_memory()[0] if tracemalloc.is_tracing() else 0
|
|
145
|
+
)
|
|
146
|
+
elif event == 'return':
|
|
147
|
+
f_name = frame.f_code.co_name
|
|
148
|
+
if call_stats[f_name]['stack']:
|
|
149
|
+
elapsed = time.time() - call_stats[f_name]['stack'].pop()
|
|
150
|
+
call_stats[f_name]['total_time'] += elapsed
|
|
151
|
+
call_stats[f_name]['peak_time'] = max(call_stats[f_name]['peak_time'], elapsed)
|
|
152
|
+
if call_stats[f_name]['mem_stack']:
|
|
153
|
+
start_mem = call_stats[f_name]['mem_stack'].pop()
|
|
154
|
+
if tracemalloc.is_tracing():
|
|
155
|
+
mem_delta = max(0, tracemalloc.get_traced_memory()[0] - start_mem)
|
|
156
|
+
call_stats[f_name]['total_mem'] += mem_delta
|
|
157
|
+
call_stats[f_name]['peak_mem'] = max(call_stats[f_name]['peak_mem'], mem_delta)
|
|
158
|
+
return trace_calls
|
|
159
|
+
|
|
160
|
+
needs_output = html or print_terminal or save_csv
|
|
161
|
+
if needs_output:
|
|
162
|
+
_was_tracing = tracemalloc.is_tracing()
|
|
163
|
+
if not _was_tracing:
|
|
164
|
+
tracemalloc.start()
|
|
165
|
+
cpu_start = time.process_time()
|
|
166
|
+
|
|
167
|
+
if trace:
|
|
168
|
+
sys.settrace(trace_calls)
|
|
169
|
+
|
|
170
|
+
profiler.start()
|
|
171
|
+
start_real_time = time.time()
|
|
172
|
+
try:
|
|
173
|
+
return func(*args, **kwargs)
|
|
174
|
+
finally:
|
|
175
|
+
profiler.stop()
|
|
176
|
+
if trace:
|
|
177
|
+
sys.settrace(None)
|
|
178
|
+
|
|
179
|
+
duration = time.time() - start_real_time
|
|
180
|
+
|
|
181
|
+
if needs_output:
|
|
182
|
+
cpu_total = time.process_time() - cpu_start
|
|
183
|
+
if tracemalloc.is_tracing():
|
|
184
|
+
current_mem, peak_mem = tracemalloc.get_traced_memory()
|
|
185
|
+
else:
|
|
186
|
+
current_mem, peak_mem = 0, 0
|
|
187
|
+
if not _was_tracing:
|
|
188
|
+
tracemalloc.stop()
|
|
189
|
+
|
|
190
|
+
if not os.path.exists(output_path):
|
|
191
|
+
os.makedirs(output_path)
|
|
192
|
+
|
|
193
|
+
stats_data = []
|
|
194
|
+
mem_stats_data = []
|
|
195
|
+
if trace:
|
|
196
|
+
stats_data = [
|
|
197
|
+
{
|
|
198
|
+
"name": k,
|
|
199
|
+
"count": v['count'],
|
|
200
|
+
"total": round(v['total_time'], 4),
|
|
201
|
+
"avg": round(v['total_time'] / v['count'], 5),
|
|
202
|
+
"peak": round(v['peak_time'], 5),
|
|
203
|
+
}
|
|
204
|
+
for k, v in sorted(call_stats.items(), key=lambda x: x[1]['total_time'], reverse=True)
|
|
205
|
+
if v['count'] > 0
|
|
206
|
+
][:50]
|
|
207
|
+
mem_stats_data = [
|
|
208
|
+
{
|
|
209
|
+
"name": k,
|
|
210
|
+
"count": v['count'],
|
|
211
|
+
"total_mem": round(v['total_mem'] / 1024, 2),
|
|
212
|
+
"avg_mem": round(v['total_mem'] / v['count'] / 1024, 2) if v['count'] > 0 else 0,
|
|
213
|
+
"peak_mem": round(v['peak_mem'] / 1024, 2),
|
|
214
|
+
}
|
|
215
|
+
for k, v in sorted(call_stats.items(), key=lambda x: x[1]['total_mem'], reverse=True)
|
|
216
|
+
if v['count'] > 0
|
|
217
|
+
][:50]
|
|
218
|
+
|
|
219
|
+
total_calls = sum(v['count'] for v in call_stats.values()) if trace else 0
|
|
220
|
+
|
|
221
|
+
if html:
|
|
222
|
+
vars_snap = {"args": [str(a)[:500] for a in args], "kwargs": {k: str(v)[:500] for k, v in kwargs.items()}}
|
|
223
|
+
perf_data = {
|
|
224
|
+
"func": func.__name__,
|
|
225
|
+
"dur": f"{duration:.4f}s",
|
|
226
|
+
"cpu_total": f"{cpu_total:.4f}s",
|
|
227
|
+
"cpu_pct": f"{round(cpu_total / duration * 100, 1) if duration > 0 else 0}%",
|
|
228
|
+
"mem_current": f"{round(current_mem / 1024 / 1024, 3)} MB",
|
|
229
|
+
"mem_peak": f"{round(peak_mem / 1024 / 1024, 3)} MB",
|
|
230
|
+
"mem_avg": f"{round(peak_mem / total_calls / 1024, 2)} KB/call" if total_calls > 0 else "N/A",
|
|
231
|
+
"has_trace": trace,
|
|
232
|
+
"stats": stats_data,
|
|
233
|
+
"mem_stats": mem_stats_data,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
injection_script = f"""
|
|
237
|
+
<script>
|
|
238
|
+
(function() {{
|
|
239
|
+
const d = {{ func: "{func.__name__}", dur: "{duration:.4f}s", vars: {json.dumps(vars_snap)}, stats: {json.dumps(stats_data)} }};
|
|
240
|
+
const p = {json.dumps(perf_data)};
|
|
241
|
+
function render() {{
|
|
242
|
+
if (document.getElementById('odoo-audit-host')) return;
|
|
243
|
+
const host = document.createElement('div');
|
|
244
|
+
host.id = 'odoo-audit-host';
|
|
245
|
+
host.style = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;';
|
|
246
|
+
document.documentElement.appendChild(host);
|
|
247
|
+
const shadow = host.attachShadow({{mode:'open'}});
|
|
248
|
+
const root = document.createElement('div');
|
|
249
|
+
root.innerHTML = `
|
|
250
|
+
<style>
|
|
251
|
+
:host {{ all: initial; }}
|
|
252
|
+
.fab {{ width:55px;height:55px;color:white;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 15px rgba(0,0,0,0.4);font-size:22px;border:2px solid white;position:fixed;bottom:20px;transition:0.2s; }}
|
|
253
|
+
.fab:hover {{ transform:scale(1.1); }}
|
|
254
|
+
.fab1 {{ background:#714B67;right:20px; }}
|
|
255
|
+
.fab2 {{ background:#1a6b8a;right:85px; }}
|
|
256
|
+
.modal {{ display:none;position:fixed;bottom:85px;right:20px;width:500px;max-height:75vh;background:white;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.4);flex-direction:column;font-family:sans-serif;overflow:hidden; }}
|
|
257
|
+
.modal.open {{ display:flex; }}
|
|
258
|
+
.hdr {{ color:white;padding:12px;display:flex;justify-content:space-between;font-weight:bold; }}
|
|
259
|
+
.hdr1 {{ background:#714B67; }}
|
|
260
|
+
.hdr2 {{ background:#1a6b8a; }}
|
|
261
|
+
.body {{ padding:15px;overflow-y:auto;background:white;color:#333; }}
|
|
262
|
+
input[type=text] {{ width:95%;padding:8px;margin:10px 0;border:1px solid #ddd;border-radius:4px; }}
|
|
263
|
+
table {{ width:100%;border-collapse:collapse;font-size:12px; }}
|
|
264
|
+
th {{ background:#f4f4f4;padding:8px;text-align:left;position:sticky;top:0; }}
|
|
265
|
+
td {{ padding:8px;border-bottom:1px solid #eee; }}
|
|
266
|
+
pre {{ background:#1e1e1e;color:#9cdcfe;padding:10px;font-size:11px;border-radius:4px;max-height:150px;overflow:auto; }}
|
|
267
|
+
.grid {{ display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px; }}
|
|
268
|
+
.card {{ background:#f8f9fa;border-radius:8px;padding:10px;border-left:4px solid #1a6b8a; }}
|
|
269
|
+
.card.warn {{ border-left-color:#e53e3e; }}
|
|
270
|
+
.card .lbl {{ font-size:10px;color:#666;text-transform:uppercase;letter-spacing:.5px; }}
|
|
271
|
+
.card .val {{ font-size:17px;font-weight:bold;color:#1a6b8a;margin-top:3px; }}
|
|
272
|
+
.card.warn .val {{ color:#e53e3e; }}
|
|
273
|
+
.sec {{ font-size:12px;font-weight:bold;color:#444;margin:10px 0 5px;border-bottom:1px solid #eee;padding-bottom:3px; }}
|
|
274
|
+
.no-trace {{ background:#fff3cd;padding:10px;border-radius:6px;font-size:12px;color:#856404;text-align:center;margin:8px 0; }}
|
|
275
|
+
</style>
|
|
276
|
+
<div class="modal" id="m1">
|
|
277
|
+
<div class="hdr hdr1"><span>🚀 ${{d.func}}</span><span>${{d.dur}}</span></div>
|
|
278
|
+
<div class="body">
|
|
279
|
+
<details><summary style="cursor:pointer;color:#714B67">📦 Vars</summary><pre>${{JSON.stringify(d.vars,null,2)}}</pre></details>
|
|
280
|
+
<input type="text" id="s1" placeholder="Buscar função...">
|
|
281
|
+
<table><thead><tr><th>Função</th><th>Calls</th><th>Total</th><th>Média</th><th>Pico</th></tr></thead>
|
|
282
|
+
<tbody id="tb1">${{d.stats.map(s=>`<tr class="r1"><td class="n1" style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total}}s</td><td align="right">${{s.avg}}s</td><td align="right">${{s.peak||'-'}}s</td></tr>`).join('')}}</tbody>
|
|
283
|
+
</table>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="modal" id="m2">
|
|
287
|
+
<div class="hdr hdr2"><span>📊 ${{p.func}}</span><span>${{p.dur}}</span></div>
|
|
288
|
+
<div class="body">
|
|
289
|
+
<div class="grid">
|
|
290
|
+
<div class="card"><div class="lbl">CPU Total</div><div class="val">${{p.cpu_total}}</div></div>
|
|
291
|
+
<div class="card"><div class="lbl">Uso de CPU</div><div class="val">${{p.cpu_pct}}</div></div>
|
|
292
|
+
<div class="card"><div class="lbl">Memória Final</div><div class="val">${{p.mem_current}}</div></div>
|
|
293
|
+
<div class="card warn"><div class="lbl">Pico de Memória</div><div class="val">${{p.mem_peak}}</div></div>
|
|
294
|
+
<div class="card" style="grid-column:1/-1"><div class="lbl">Memória Média por Chamada</div><div class="val">${{p.mem_avg}}</div></div>
|
|
295
|
+
</div>
|
|
296
|
+
${{p.has_trace ?
|
|
297
|
+
`<div class="sec">⏱ CPU por Função</div>
|
|
298
|
+
<input type="text" id="s2" placeholder="Buscar função...">
|
|
299
|
+
<table><thead><tr><th>Função</th><th>Calls</th><th>Total</th><th>Média</th><th>Pico</th></tr></thead>
|
|
300
|
+
<tbody id="tb2">${{p.stats.map(s=>`<tr class="r2"><td class="n2" style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total}}s</td><td align="right">${{s.avg}}s</td><td align="right">${{s.peak}}s</td></tr>`).join('')}}</tbody>
|
|
301
|
+
</table>
|
|
302
|
+
<div class="sec">💾 Memória por Função</div>
|
|
303
|
+
<table><thead><tr><th>Função</th><th>Calls</th><th>Total (KB)</th><th>Média (KB)</th><th>Pico (KB)</th></tr></thead>
|
|
304
|
+
<tbody>${{p.mem_stats.map(s=>`<tr><td style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total_mem}}</td><td align="right">${{s.avg_mem}}</td><td align="right">${{s.peak_mem}}</td></tr>`).join('')}}</tbody>
|
|
305
|
+
</table>`
|
|
306
|
+
: `<div class="no-trace">⚠️ Habilite <b>trace=True</b> para métricas por função</div>`
|
|
307
|
+
}}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="fab fab1" id="b1">🚀</div>
|
|
311
|
+
<div class="fab fab2" id="b2">📊</div>
|
|
312
|
+
`;
|
|
313
|
+
const m1 = root.querySelector('#m1');
|
|
314
|
+
const m2 = root.querySelector('#m2');
|
|
315
|
+
root.querySelector('#b1').onclick = () => {{ m1.classList.toggle('open'); m2.classList.remove('open'); }};
|
|
316
|
+
root.querySelector('#b2').onclick = () => {{ m2.classList.toggle('open'); m1.classList.remove('open'); }};
|
|
317
|
+
root.querySelector('#s1').oninput = (e) => {{
|
|
318
|
+
const v = e.target.value.toLowerCase();
|
|
319
|
+
root.querySelectorAll('.r1').forEach(r => r.style.display = r.querySelector('.n1').textContent.toLowerCase().includes(v) ? '' : 'none');
|
|
320
|
+
}};
|
|
321
|
+
const s2 = root.querySelector('#s2');
|
|
322
|
+
if (s2) s2.oninput = (e) => {{
|
|
323
|
+
const v = e.target.value.toLowerCase();
|
|
324
|
+
root.querySelectorAll('.r2').forEach(r => r.style.display = r.querySelector('.n2').textContent.toLowerCase().includes(v) ? '' : 'none');
|
|
325
|
+
}};
|
|
326
|
+
shadow.appendChild(root);
|
|
327
|
+
}}
|
|
328
|
+
setInterval(render, 1000); render();
|
|
329
|
+
}})();
|
|
330
|
+
</script>
|
|
331
|
+
"""
|
|
332
|
+
pyinstrument_html = profiler.output_html()
|
|
333
|
+
final_html = pyinstrument_html.replace('</html>', f'{injection_script}</html>')
|
|
334
|
+
filepath = os.path.join(output_path, f"AUDIT_{func.__name__}_{int(time.time())}.html")
|
|
335
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
336
|
+
f.write(final_html)
|
|
337
|
+
|
|
338
|
+
if print_terminal:
|
|
339
|
+
_print_terminal(
|
|
340
|
+
func.__name__, duration, cpu_total, current_mem, peak_mem,
|
|
341
|
+
total_calls, stats_data, mem_stats_data, trace,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if save_csv:
|
|
345
|
+
_save_csv(
|
|
346
|
+
output_path, func.__name__, int(time.time()),
|
|
347
|
+
duration, cpu_total, current_mem, peak_mem,
|
|
348
|
+
total_calls, stats_data, mem_stats_data, trace,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return wrapper
|
|
352
|
+
return decorator
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-python-audit
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Ferramenta de profiling com widget flutuante, exportação CSV e servidor de gerenciamento de logs.
|
|
5
|
+
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: odoo,profiler,performance,pyinstrument,audit
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Framework :: Odoo
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pyinstrument>=4.6.0
|
|
18
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
19
|
+
|
|
20
|
+
# Simple Python Audit Tool 🚀
|
|
21
|
+
|
|
22
|
+
Ferramenta completa para medição de performance e auditoria de variáveis para Python.
|
|
23
|
+
|
|
24
|
+
## 📦 Instalação
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install .
|
|
28
|
+
# Ou via GitHub
|
|
29
|
+
pip install git+https://github.com/sadson/simple_python_audit.git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🎯 Como Usar
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from simple_python_audit import profile
|
|
36
|
+
|
|
37
|
+
@profile(
|
|
38
|
+
html=True,
|
|
39
|
+
trace=True,
|
|
40
|
+
deep=True,
|
|
41
|
+
print_terminal=True,
|
|
42
|
+
save_csv=True,
|
|
43
|
+
output_path="/tmp/simple_python_audit_perf",
|
|
44
|
+
)
|
|
45
|
+
def meu_metodo_lento(self):
|
|
46
|
+
# Seu código aqui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Parâmetros do decorador
|
|
50
|
+
|
|
51
|
+
| Parâmetro | Padrão | Descrição |
|
|
52
|
+
|------------------|-------------------------------------|---------------------------------------------------------------------------|
|
|
53
|
+
| `html` | `False` | Gera relatório interativo em HTML com widget flutuante |
|
|
54
|
+
| `trace` | `False` | Rastreia chamadas de funções e coleta métricas por função |
|
|
55
|
+
| `deep` | `False` | Aumenta granularidade do profiler (intervalo de 0,0001 s em vez de 0,001 s) |
|
|
56
|
+
| `print_terminal` | `False` | Imprime tabela de métricas formatada no terminal ao final da execução |
|
|
57
|
+
| `save_csv` | `False` | Salva os dados em arquivos CSV prontos para análise com pandas/IA |
|
|
58
|
+
| `output_path` | `"/tmp/simple_python_audit_perf"` | Diretório onde os relatórios (HTML e CSV) serão gravados |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📊 Saída no Terminal (`print_terminal=True`)
|
|
63
|
+
|
|
64
|
+
Exibe uma tabela formatada diretamente no stdout ao término da função:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
==============================================================
|
|
68
|
+
AUDIT: meu_metodo_lento | 1.2345s
|
|
69
|
+
==============================================================
|
|
70
|
+
CPU Total: 0.4567s
|
|
71
|
+
CPU Usage: 37.0%
|
|
72
|
+
Memory Current: 12.345 MB
|
|
73
|
+
Memory Peak: 15.678 MB
|
|
74
|
+
Memory Avg/Call: 2.34 KB/call
|
|
75
|
+
Total Calls Traced: 1234
|
|
76
|
+
--------------------------------------------------------------
|
|
77
|
+
Top Functions by CPU Time:
|
|
78
|
+
Function Calls Total Avg Peak
|
|
79
|
+
meu_metodo_lento 1 1.2300s 1.23000s 1.23000s
|
|
80
|
+
_query 42 0.4500s 0.01071s 0.02100s
|
|
81
|
+
...
|
|
82
|
+
--------------------------------------------------------------
|
|
83
|
+
Top Functions by Memory:
|
|
84
|
+
Function Calls Total KB Avg KB Peak KB
|
|
85
|
+
_query 42 512.30 12.20 48.50
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 💾 Exportação CSV (`save_csv=True`)
|
|
92
|
+
|
|
93
|
+
Gera dois arquivos CSV em `output_path`, ideais para carregar em um `DataFrame` do pandas, alimentar uma IA ou construir dashboards.
|
|
94
|
+
|
|
95
|
+
### `AUDIT_{func}_{timestamp}_summary.csv`
|
|
96
|
+
|
|
97
|
+
Uma linha por execução com as métricas globais da chamada. Acumulando múltiplas execuções é possível rastrear regressões de performance ao longo do tempo.
|
|
98
|
+
|
|
99
|
+
| Coluna | Tipo | Descrição |
|
|
100
|
+
|-------------------------|---------|------------------------------------------------|
|
|
101
|
+
| `timestamp` | int | Unix timestamp da execução |
|
|
102
|
+
| `func_name` | str | Nome da função decorada |
|
|
103
|
+
| `duration_s` | float | Tempo total de execução (segundos) |
|
|
104
|
+
| `cpu_total_s` | float | Tempo de CPU consumido (segundos) |
|
|
105
|
+
| `cpu_pct` | float | Percentual de uso de CPU |
|
|
106
|
+
| `mem_current_mb` | float | Memória alocada ao final (MB) |
|
|
107
|
+
| `mem_peak_mb` | float | Pico de memória durante a execução (MB) |
|
|
108
|
+
| `mem_avg_kb_per_call` | float | Média de memória por chamada de função (KB) |
|
|
109
|
+
| `total_calls_traced` | int | Total de chamadas de função rastreadas |
|
|
110
|
+
|
|
111
|
+
### `AUDIT_{func}_{timestamp}_trace.csv`
|
|
112
|
+
|
|
113
|
+
Uma linha por função rastreada (requer `trace=True`). Permite identificar os gargalos com precisão.
|
|
114
|
+
|
|
115
|
+
| Coluna | Tipo | Descrição |
|
|
116
|
+
|--------------------|-------|-----------------------------------------------|
|
|
117
|
+
| `timestamp` | int | Unix timestamp — chave para junção com summary |
|
|
118
|
+
| `func_name` | str | Nome da função decorada |
|
|
119
|
+
| `called_function` | str | Nome da função rastreada |
|
|
120
|
+
| `call_count` | int | Número de chamadas |
|
|
121
|
+
| `total_time_s` | float | Tempo total acumulado (segundos) |
|
|
122
|
+
| `avg_time_s` | float | Tempo médio por chamada (segundos) |
|
|
123
|
+
| `peak_time_s` | float | Pico de tempo em uma única chamada (segundos) |
|
|
124
|
+
| `total_mem_kb` | float | Memória total alocada (KB) |
|
|
125
|
+
| `avg_mem_kb` | float | Memória média por chamada (KB) |
|
|
126
|
+
| `peak_mem_kb` | float | Pico de memória em uma única chamada (KB) |
|
|
127
|
+
|
|
128
|
+
### Exemplo de análise com pandas
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import pandas as pd
|
|
132
|
+
import glob
|
|
133
|
+
|
|
134
|
+
# Carregar todos os resumos de execuções
|
|
135
|
+
summary = pd.concat([pd.read_csv(f) for f in glob.glob("/tmp/simple_python_audit_perf/*_summary.csv")])
|
|
136
|
+
summary["timestamp"] = pd.to_datetime(summary["timestamp"], unit="s")
|
|
137
|
+
summary.sort_values("timestamp").plot(x="timestamp", y="duration_s", title="Evolução do tempo de execução")
|
|
138
|
+
|
|
139
|
+
# Carregar trace de uma execução específica
|
|
140
|
+
trace = pd.read_csv("/tmp/simple_python_audit_perf/AUDIT_meu_metodo_lento_1234567890_trace.csv")
|
|
141
|
+
trace.sort_values("total_time_s", ascending=False).head(10)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🌐 Relatório HTML (`html=True`)
|
|
147
|
+
|
|
148
|
+
Gera um arquivo `AUDIT_{func}_{timestamp}.html` com um widget flutuante interativo contendo:
|
|
149
|
+
|
|
150
|
+
- **Painel de rastreamento (🚀):** tabela de chamadas de função com busca em tempo real
|
|
151
|
+
- **Painel de métricas (📊):** cards de CPU e memória, tabelas de CPU e memória por função
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 🛠️ Servidor de Auditoria
|
|
156
|
+
|
|
157
|
+
Interface web para visualizar e gerenciar os arquivos de relatório gerados.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Via linha de comando
|
|
161
|
+
simple-python-audit-server --port 8080 --dir /tmp/simple_python_audit_perf
|
|
162
|
+
|
|
163
|
+
# Via Python
|
|
164
|
+
from simple_python_audit import start_server
|
|
165
|
+
start_server(port=8080, directory="/tmp/simple_python_audit_perf")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🔗 Integração com Odoo
|
|
171
|
+
|
|
172
|
+
Ao instalar em um ambiente com Odoo, o decorador `profile` é injetado automaticamente em `odoo.api`:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from odoo import api
|
|
176
|
+
|
|
177
|
+
@api.profile(html=True, trace=True, print_terminal=True, save_csv=True)
|
|
178
|
+
def action_processar(self):
|
|
179
|
+
...
|
|
180
|
+
```
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: simple-python-audit
|
|
3
|
-
Version: 1.2.0
|
|
4
|
-
Summary: Ferramenta de profiling com widget flutuante e servidor de gerenciamento de logs.
|
|
5
|
-
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: odoo,profiler,performance,pyinstrument,audit
|
|
8
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Framework :: Odoo
|
|
14
|
-
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
-
Requires-Python: >=3.8
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
Requires-Dist: pyinstrument>=4.6.0
|
|
18
|
-
Requires-Dist: typing-extensions>=4.0.0
|
|
19
|
-
|
|
20
|
-
# Simple Python Audit Tool 🚀
|
|
21
|
-
|
|
22
|
-
Ferramenta completa para medição de performance e auditoria de variáveis para python.
|
|
23
|
-
|
|
24
|
-
## 📦 Instalação
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
pip install .
|
|
28
|
-
# Ou via GitHub
|
|
29
|
-
pip install git+[https://github.com/sadson/simple_python_audit.git](https://github.com/sadson/simple_python_audit.git)
|
|
30
|
-
|
|
31
|
-
## 🎯 Como Usar
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from simple_python_audit import profile
|
|
35
|
-
|
|
36
|
-
@profile(html=True, output_path="/tmp/simple_python_audit_perf" ,trace=True, deep=True)
|
|
37
|
-
def meu_metodo_lento(self):
|
|
38
|
-
# Seu código aqui
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Parâmetros:**
|
|
42
|
-
- `html=True`: Gera relatório em HTML
|
|
43
|
-
- `trace=True`: Rastreia chamadas de funções
|
|
44
|
-
- `deep=True`: Rastreia chamadas com tempo de execução a partir de 0.0001 ms
|
|
45
|
-
- `output_path`: Caminho para salvar os relatórios gerados
|
|
46
|
-
|
|
47
|
-
## 🛠️ Executar Servidor de Auditoria
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
simple-python-audit-server --port 8080 --dir /tmp/odoo_perf
|
|
51
|
-
ou
|
|
52
|
-
via python
|
|
53
|
-
|
|
54
|
-
from simple_python_audit import start_server
|
|
55
|
-
start_server(port=8080, directory="/tmp/odoo_perf")
|
|
56
|
-
```
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# Simple Python Audit Tool 🚀
|
|
2
|
-
|
|
3
|
-
Ferramenta completa para medição de performance e auditoria de variáveis para python.
|
|
4
|
-
|
|
5
|
-
## 📦 Instalação
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install .
|
|
9
|
-
# Ou via GitHub
|
|
10
|
-
pip install git+[https://github.com/sadson/simple_python_audit.git](https://github.com/sadson/simple_python_audit.git)
|
|
11
|
-
|
|
12
|
-
## 🎯 Como Usar
|
|
13
|
-
|
|
14
|
-
```python
|
|
15
|
-
from simple_python_audit import profile
|
|
16
|
-
|
|
17
|
-
@profile(html=True, output_path="/tmp/simple_python_audit_perf" ,trace=True, deep=True)
|
|
18
|
-
def meu_metodo_lento(self):
|
|
19
|
-
# Seu código aqui
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**Parâmetros:**
|
|
23
|
-
- `html=True`: Gera relatório em HTML
|
|
24
|
-
- `trace=True`: Rastreia chamadas de funções
|
|
25
|
-
- `deep=True`: Rastreia chamadas com tempo de execução a partir de 0.0001 ms
|
|
26
|
-
- `output_path`: Caminho para salvar os relatórios gerados
|
|
27
|
-
|
|
28
|
-
## 🛠️ Executar Servidor de Auditoria
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
simple-python-audit-server --port 8080 --dir /tmp/odoo_perf
|
|
32
|
-
ou
|
|
33
|
-
via python
|
|
34
|
-
|
|
35
|
-
from simple_python_audit import start_server
|
|
36
|
-
start_server(port=8080, directory="/tmp/odoo_perf")
|
|
37
|
-
```
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
import logging
|
|
6
|
-
import tracemalloc
|
|
7
|
-
from functools import wraps
|
|
8
|
-
from pyinstrument import Profiler
|
|
9
|
-
from collections import defaultdict
|
|
10
|
-
|
|
11
|
-
_logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
def html_escape(text):
|
|
14
|
-
return str(text).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
15
|
-
|
|
16
|
-
def profile(html=False, output_path="/tmp/simple_python_audit_perf", deep=False, trace=False):
|
|
17
|
-
def decorator(func):
|
|
18
|
-
@wraps(func)
|
|
19
|
-
def wrapper(*args, **kwargs):
|
|
20
|
-
interval = 0.0001 if deep else 0.001
|
|
21
|
-
profiler = Profiler(interval=interval)
|
|
22
|
-
call_stats = defaultdict(lambda: {
|
|
23
|
-
'count': 0, 'total_time': 0.0, 'peak_time': 0.0,
|
|
24
|
-
'total_mem': 0, 'peak_mem': 0,
|
|
25
|
-
'stack': [], 'mem_stack': []
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
def trace_calls(frame, event, arg):
|
|
29
|
-
if event == 'call':
|
|
30
|
-
f_name = frame.f_code.co_name
|
|
31
|
-
call_stats[f_name]['count'] += 1
|
|
32
|
-
call_stats[f_name]['stack'].append(time.time())
|
|
33
|
-
call_stats[f_name]['mem_stack'].append(
|
|
34
|
-
tracemalloc.get_traced_memory()[0] if tracemalloc.is_tracing() else 0
|
|
35
|
-
)
|
|
36
|
-
elif event == 'return':
|
|
37
|
-
f_name = frame.f_code.co_name
|
|
38
|
-
if call_stats[f_name]['stack']:
|
|
39
|
-
elapsed = time.time() - call_stats[f_name]['stack'].pop()
|
|
40
|
-
call_stats[f_name]['total_time'] += elapsed
|
|
41
|
-
call_stats[f_name]['peak_time'] = max(call_stats[f_name]['peak_time'], elapsed)
|
|
42
|
-
if call_stats[f_name]['mem_stack']:
|
|
43
|
-
start_mem = call_stats[f_name]['mem_stack'].pop()
|
|
44
|
-
if tracemalloc.is_tracing():
|
|
45
|
-
mem_delta = max(0, tracemalloc.get_traced_memory()[0] - start_mem)
|
|
46
|
-
call_stats[f_name]['total_mem'] += mem_delta
|
|
47
|
-
call_stats[f_name]['peak_mem'] = max(call_stats[f_name]['peak_mem'], mem_delta)
|
|
48
|
-
return trace_calls
|
|
49
|
-
|
|
50
|
-
if html:
|
|
51
|
-
_was_tracing = tracemalloc.is_tracing()
|
|
52
|
-
if not _was_tracing:
|
|
53
|
-
tracemalloc.start()
|
|
54
|
-
cpu_start = time.process_time()
|
|
55
|
-
|
|
56
|
-
if trace:
|
|
57
|
-
sys.settrace(trace_calls)
|
|
58
|
-
|
|
59
|
-
profiler.start()
|
|
60
|
-
start_real_time = time.time()
|
|
61
|
-
try:
|
|
62
|
-
return func(*args, **kwargs)
|
|
63
|
-
finally:
|
|
64
|
-
profiler.stop()
|
|
65
|
-
if trace:
|
|
66
|
-
sys.settrace(None)
|
|
67
|
-
|
|
68
|
-
duration = time.time() - start_real_time
|
|
69
|
-
|
|
70
|
-
if html:
|
|
71
|
-
cpu_total = time.process_time() - cpu_start
|
|
72
|
-
if tracemalloc.is_tracing():
|
|
73
|
-
current_mem, peak_mem = tracemalloc.get_traced_memory()
|
|
74
|
-
else:
|
|
75
|
-
current_mem, peak_mem = 0, 0
|
|
76
|
-
if not _was_tracing:
|
|
77
|
-
tracemalloc.stop()
|
|
78
|
-
|
|
79
|
-
if not os.path.exists(output_path):
|
|
80
|
-
os.makedirs(output_path)
|
|
81
|
-
|
|
82
|
-
pyinstrument_html = profiler.output_html()
|
|
83
|
-
vars_snap = {"args": [str(a)[:500] for a in args], "kwargs": {k: str(v)[:500] for k, v in kwargs.items()}}
|
|
84
|
-
|
|
85
|
-
stats_data = []
|
|
86
|
-
mem_stats_data = []
|
|
87
|
-
if trace:
|
|
88
|
-
stats_data = [
|
|
89
|
-
{
|
|
90
|
-
"name": k,
|
|
91
|
-
"count": v['count'],
|
|
92
|
-
"total": round(v['total_time'], 4),
|
|
93
|
-
"avg": round(v['total_time'] / v['count'], 5),
|
|
94
|
-
"peak": round(v['peak_time'], 5),
|
|
95
|
-
}
|
|
96
|
-
for k, v in sorted(call_stats.items(), key=lambda x: x[1]['total_time'], reverse=True)
|
|
97
|
-
if v['count'] > 0
|
|
98
|
-
][:50]
|
|
99
|
-
mem_stats_data = [
|
|
100
|
-
{
|
|
101
|
-
"name": k,
|
|
102
|
-
"count": v['count'],
|
|
103
|
-
"total_mem": round(v['total_mem'] / 1024, 2),
|
|
104
|
-
"avg_mem": round(v['total_mem'] / v['count'] / 1024, 2) if v['count'] > 0 else 0,
|
|
105
|
-
"peak_mem": round(v['peak_mem'] / 1024, 2),
|
|
106
|
-
}
|
|
107
|
-
for k, v in sorted(call_stats.items(), key=lambda x: x[1]['total_mem'], reverse=True)
|
|
108
|
-
if v['count'] > 0
|
|
109
|
-
][:50]
|
|
110
|
-
|
|
111
|
-
total_calls = sum(v['count'] for v in call_stats.values()) if trace else 0
|
|
112
|
-
|
|
113
|
-
perf_data = {
|
|
114
|
-
"func": func.__name__,
|
|
115
|
-
"dur": f"{duration:.4f}s",
|
|
116
|
-
"cpu_total": f"{cpu_total:.4f}s",
|
|
117
|
-
"cpu_pct": f"{round(cpu_total / duration * 100, 1) if duration > 0 else 0}%",
|
|
118
|
-
"mem_current": f"{round(current_mem / 1024 / 1024, 3)} MB",
|
|
119
|
-
"mem_peak": f"{round(peak_mem / 1024 / 1024, 3)} MB",
|
|
120
|
-
"mem_avg": f"{round(peak_mem / total_calls / 1024, 2)} KB/call" if total_calls > 0 else "N/A",
|
|
121
|
-
"has_trace": trace,
|
|
122
|
-
"stats": stats_data,
|
|
123
|
-
"mem_stats": mem_stats_data,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
injection_script = f"""
|
|
127
|
-
<script>
|
|
128
|
-
(function() {{
|
|
129
|
-
const d = {{ func: "{func.__name__}", dur: "{duration:.4f}s", vars: {json.dumps(vars_snap)}, stats: {json.dumps(stats_data)} }};
|
|
130
|
-
const p = {json.dumps(perf_data)};
|
|
131
|
-
function render() {{
|
|
132
|
-
if (document.getElementById('odoo-audit-host')) return;
|
|
133
|
-
const host = document.createElement('div');
|
|
134
|
-
host.id = 'odoo-audit-host';
|
|
135
|
-
host.style = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;';
|
|
136
|
-
document.documentElement.appendChild(host);
|
|
137
|
-
const shadow = host.attachShadow({{mode:'open'}});
|
|
138
|
-
const root = document.createElement('div');
|
|
139
|
-
root.innerHTML = `
|
|
140
|
-
<style>
|
|
141
|
-
:host {{ all: initial; }}
|
|
142
|
-
.fab {{ width:55px;height:55px;color:white;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 15px rgba(0,0,0,0.4);font-size:22px;border:2px solid white;position:fixed;bottom:20px;transition:0.2s; }}
|
|
143
|
-
.fab:hover {{ transform:scale(1.1); }}
|
|
144
|
-
.fab1 {{ background:#714B67;right:20px; }}
|
|
145
|
-
.fab2 {{ background:#1a6b8a;right:85px; }}
|
|
146
|
-
.modal {{ display:none;position:fixed;bottom:85px;right:20px;width:500px;max-height:75vh;background:white;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.4);flex-direction:column;font-family:sans-serif;overflow:hidden; }}
|
|
147
|
-
.modal.open {{ display:flex; }}
|
|
148
|
-
.hdr {{ color:white;padding:12px;display:flex;justify-content:space-between;font-weight:bold; }}
|
|
149
|
-
.hdr1 {{ background:#714B67; }}
|
|
150
|
-
.hdr2 {{ background:#1a6b8a; }}
|
|
151
|
-
.body {{ padding:15px;overflow-y:auto;background:white;color:#333; }}
|
|
152
|
-
input[type=text] {{ width:95%;padding:8px;margin:10px 0;border:1px solid #ddd;border-radius:4px; }}
|
|
153
|
-
table {{ width:100%;border-collapse:collapse;font-size:12px; }}
|
|
154
|
-
th {{ background:#f4f4f4;padding:8px;text-align:left;position:sticky;top:0; }}
|
|
155
|
-
td {{ padding:8px;border-bottom:1px solid #eee; }}
|
|
156
|
-
pre {{ background:#1e1e1e;color:#9cdcfe;padding:10px;font-size:11px;border-radius:4px;max-height:150px;overflow:auto; }}
|
|
157
|
-
.grid {{ display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px; }}
|
|
158
|
-
.card {{ background:#f8f9fa;border-radius:8px;padding:10px;border-left:4px solid #1a6b8a; }}
|
|
159
|
-
.card.warn {{ border-left-color:#e53e3e; }}
|
|
160
|
-
.card .lbl {{ font-size:10px;color:#666;text-transform:uppercase;letter-spacing:.5px; }}
|
|
161
|
-
.card .val {{ font-size:17px;font-weight:bold;color:#1a6b8a;margin-top:3px; }}
|
|
162
|
-
.card.warn .val {{ color:#e53e3e; }}
|
|
163
|
-
.sec {{ font-size:12px;font-weight:bold;color:#444;margin:10px 0 5px;border-bottom:1px solid #eee;padding-bottom:3px; }}
|
|
164
|
-
.no-trace {{ background:#fff3cd;padding:10px;border-radius:6px;font-size:12px;color:#856404;text-align:center;margin:8px 0; }}
|
|
165
|
-
</style>
|
|
166
|
-
<div class="modal" id="m1">
|
|
167
|
-
<div class="hdr hdr1"><span>🚀 ${{d.func}}</span><span>${{d.dur}}</span></div>
|
|
168
|
-
<div class="body">
|
|
169
|
-
<details><summary style="cursor:pointer;color:#714B67">📦 Vars</summary><pre>${{JSON.stringify(d.vars,null,2)}}</pre></details>
|
|
170
|
-
<input type="text" id="s1" placeholder="Buscar função...">
|
|
171
|
-
<table><thead><tr><th>Função</th><th>Calls</th><th>Total</th><th>Média</th><th>Pico</th></tr></thead>
|
|
172
|
-
<tbody id="tb1">${{d.stats.map(s=>`<tr class="r1"><td class="n1" style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total}}s</td><td align="right">${{s.avg}}s</td><td align="right">${{s.peak||'-'}}s</td></tr>`).join('')}}</tbody>
|
|
173
|
-
</table>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
<div class="modal" id="m2">
|
|
177
|
-
<div class="hdr hdr2"><span>📊 ${{p.func}}</span><span>${{p.dur}}</span></div>
|
|
178
|
-
<div class="body">
|
|
179
|
-
<div class="grid">
|
|
180
|
-
<div class="card"><div class="lbl">CPU Total</div><div class="val">${{p.cpu_total}}</div></div>
|
|
181
|
-
<div class="card"><div class="lbl">Uso de CPU</div><div class="val">${{p.cpu_pct}}</div></div>
|
|
182
|
-
<div class="card"><div class="lbl">Memória Final</div><div class="val">${{p.mem_current}}</div></div>
|
|
183
|
-
<div class="card warn"><div class="lbl">Pico de Memória</div><div class="val">${{p.mem_peak}}</div></div>
|
|
184
|
-
<div class="card" style="grid-column:1/-1"><div class="lbl">Memória Média por Chamada</div><div class="val">${{p.mem_avg}}</div></div>
|
|
185
|
-
</div>
|
|
186
|
-
${{p.has_trace ?
|
|
187
|
-
`<div class="sec">⏱ CPU por Função</div>
|
|
188
|
-
<input type="text" id="s2" placeholder="Buscar função...">
|
|
189
|
-
<table><thead><tr><th>Função</th><th>Calls</th><th>Total</th><th>Média</th><th>Pico</th></tr></thead>
|
|
190
|
-
<tbody id="tb2">${{p.stats.map(s=>`<tr class="r2"><td class="n2" style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total}}s</td><td align="right">${{s.avg}}s</td><td align="right">${{s.peak}}s</td></tr>`).join('')}}</tbody>
|
|
191
|
-
</table>
|
|
192
|
-
<div class="sec">💾 Memória por Função</div>
|
|
193
|
-
<table><thead><tr><th>Função</th><th>Calls</th><th>Total (KB)</th><th>Média (KB)</th><th>Pico (KB)</th></tr></thead>
|
|
194
|
-
<tbody>${{p.mem_stats.map(s=>`<tr><td style="font-family:monospace">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total_mem}}</td><td align="right">${{s.avg_mem}}</td><td align="right">${{s.peak_mem}}</td></tr>`).join('')}}</tbody>
|
|
195
|
-
</table>`
|
|
196
|
-
: `<div class="no-trace">⚠️ Habilite <b>trace=True</b> para métricas por função</div>`
|
|
197
|
-
}}
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
<div class="fab fab1" id="b1">🚀</div>
|
|
201
|
-
<div class="fab fab2" id="b2">📊</div>
|
|
202
|
-
`;
|
|
203
|
-
const m1 = root.querySelector('#m1');
|
|
204
|
-
const m2 = root.querySelector('#m2');
|
|
205
|
-
root.querySelector('#b1').onclick = () => {{ m1.classList.toggle('open'); m2.classList.remove('open'); }};
|
|
206
|
-
root.querySelector('#b2').onclick = () => {{ m2.classList.toggle('open'); m1.classList.remove('open'); }};
|
|
207
|
-
root.querySelector('#s1').oninput = (e) => {{
|
|
208
|
-
const v = e.target.value.toLowerCase();
|
|
209
|
-
root.querySelectorAll('.r1').forEach(r => r.style.display = r.querySelector('.n1').textContent.toLowerCase().includes(v) ? '' : 'none');
|
|
210
|
-
}};
|
|
211
|
-
const s2 = root.querySelector('#s2');
|
|
212
|
-
if (s2) s2.oninput = (e) => {{
|
|
213
|
-
const v = e.target.value.toLowerCase();
|
|
214
|
-
root.querySelectorAll('.r2').forEach(r => r.style.display = r.querySelector('.n2').textContent.toLowerCase().includes(v) ? '' : 'none');
|
|
215
|
-
}};
|
|
216
|
-
shadow.appendChild(root);
|
|
217
|
-
}}
|
|
218
|
-
setInterval(render, 1000); render();
|
|
219
|
-
}})();
|
|
220
|
-
</script>
|
|
221
|
-
"""
|
|
222
|
-
final_html = pyinstrument_html.replace('</html>', f'{injection_script}</html>')
|
|
223
|
-
filepath = os.path.join(output_path, f"AUDIT_{func.__name__}_{int(time.time())}.html")
|
|
224
|
-
with open(filepath, "w", encoding="utf-8") as f: f.write(final_html)
|
|
225
|
-
return wrapper
|
|
226
|
-
return decorator
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: simple-python-audit
|
|
3
|
-
Version: 1.2.0
|
|
4
|
-
Summary: Ferramenta de profiling com widget flutuante e servidor de gerenciamento de logs.
|
|
5
|
-
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: odoo,profiler,performance,pyinstrument,audit
|
|
8
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Framework :: Odoo
|
|
14
|
-
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
-
Requires-Python: >=3.8
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
Requires-Dist: pyinstrument>=4.6.0
|
|
18
|
-
Requires-Dist: typing-extensions>=4.0.0
|
|
19
|
-
|
|
20
|
-
# Simple Python Audit Tool 🚀
|
|
21
|
-
|
|
22
|
-
Ferramenta completa para medição de performance e auditoria de variáveis para python.
|
|
23
|
-
|
|
24
|
-
## 📦 Instalação
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
pip install .
|
|
28
|
-
# Ou via GitHub
|
|
29
|
-
pip install git+[https://github.com/sadson/simple_python_audit.git](https://github.com/sadson/simple_python_audit.git)
|
|
30
|
-
|
|
31
|
-
## 🎯 Como Usar
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from simple_python_audit import profile
|
|
35
|
-
|
|
36
|
-
@profile(html=True, output_path="/tmp/simple_python_audit_perf" ,trace=True, deep=True)
|
|
37
|
-
def meu_metodo_lento(self):
|
|
38
|
-
# Seu código aqui
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Parâmetros:**
|
|
42
|
-
- `html=True`: Gera relatório em HTML
|
|
43
|
-
- `trace=True`: Rastreia chamadas de funções
|
|
44
|
-
- `deep=True`: Rastreia chamadas com tempo de execução a partir de 0.0001 ms
|
|
45
|
-
- `output_path`: Caminho para salvar os relatórios gerados
|
|
46
|
-
|
|
47
|
-
## 🛠️ Executar Servidor de Auditoria
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
simple-python-audit-server --port 8080 --dir /tmp/odoo_perf
|
|
51
|
-
ou
|
|
52
|
-
via python
|
|
53
|
-
|
|
54
|
-
from simple_python_audit import start_server
|
|
55
|
-
start_server(port=8080, directory="/tmp/odoo_perf")
|
|
56
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_python_audit-1.2.0 → simple_python_audit-1.3.0}/src/simple_python_audit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|