pyvitals-profiler 1.0.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.
- pyvitals_profiler-1.0.0/LICENCE +21 -0
- pyvitals_profiler-1.0.0/PKG-INFO +126 -0
- pyvitals_profiler-1.0.0/README.md +110 -0
- pyvitals_profiler-1.0.0/pyproject.toml +37 -0
- pyvitals_profiler-1.0.0/setup.cfg +4 -0
- pyvitals_profiler-1.0.0/src/pyvitals/__init__.py +6 -0
- pyvitals_profiler-1.0.0/src/pyvitals/analyzer.py +133 -0
- pyvitals_profiler-1.0.0/src/pyvitals/profiler.py +202 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/PKG-INFO +126 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/SOURCES.txt +13 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/dependency_links.txt +1 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/entry_points.txt +2 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/requires.txt +1 -0
- pyvitals_profiler-1.0.0/src/pyvitals_profiler.egg-info/top_level.txt +1 -0
- pyvitals_profiler-1.0.0/tests/test_profiler.py +85 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvitals-profiler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports.
|
|
5
|
+
Author-email: Antoine M <projet24.apprentissage@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENCE
|
|
14
|
+
Requires-Dist: rich>=10.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PyVitals
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
20
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
21
|
+
[](https://github.com/yourusername/pyvitals/actions)
|
|
22
|
+
[](https://opensource.org/licenses/MIT)
|
|
23
|
+
|
|
24
|
+
**PyVitals** is an intelligent, lightweight performance profiler for Python.
|
|
25
|
+
It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
|
|
26
|
+
|
|
27
|
+
Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
|
|
34
|
+
- **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
|
|
35
|
+
- **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
|
|
36
|
+
- **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
|
|
37
|
+
- **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
|
|
38
|
+
- **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Install via `pip`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pyvitals-profiler
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
1. As a Decorator (Sync & Async)
|
|
52
|
+
Simply add @track() to any function you want to profile.
|
|
53
|
+
|
|
54
|
+
```Python
|
|
55
|
+
import asyncio
|
|
56
|
+
from pyvitals import track
|
|
57
|
+
|
|
58
|
+
@track(name="Database Query", max_time_s=1.5)
|
|
59
|
+
async def fetch_users():
|
|
60
|
+
await asyncio.sleep(1.0) # Simulating I/O Wait
|
|
61
|
+
return {"status": "success"}
|
|
62
|
+
|
|
63
|
+
asyncio.run(fetch_users())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
2. As a Context Manager
|
|
67
|
+
If you only want to profile a specific loop without decorating the whole function, use the with statement.
|
|
68
|
+
|
|
69
|
+
```Python
|
|
70
|
+
from pyvitals import track
|
|
71
|
+
|
|
72
|
+
def process_heavy_data():
|
|
73
|
+
print("Initializing...")
|
|
74
|
+
|
|
75
|
+
# Only profile this specific block
|
|
76
|
+
with track(name="Matrix Math", max_mem_mb=50):
|
|
77
|
+
data = [x**2 for x in range(1_000_000)]
|
|
78
|
+
|
|
79
|
+
print("Done!")
|
|
80
|
+
|
|
81
|
+
process_heavy_data()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Exporting & Analyzing Data (CLI)
|
|
85
|
+
PyVitals allows you to save your performance history to detect regressions.
|
|
86
|
+
|
|
87
|
+
Step 1: Export to JSON
|
|
88
|
+
Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
|
|
89
|
+
|
|
90
|
+
```Python
|
|
91
|
+
from pyvitals import track
|
|
92
|
+
|
|
93
|
+
@track(export_to="vitals_history.json", quiet=True)
|
|
94
|
+
def background_task():
|
|
95
|
+
# ... task logic ...
|
|
96
|
+
pass
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Step 2: Use the CLI Analyzer
|
|
100
|
+
PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
|
|
101
|
+
|
|
102
|
+
```Bash
|
|
103
|
+
pyvitals vitals_history.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
|
|
107
|
+
|
|
108
|
+
## Production Kill-Switch
|
|
109
|
+
You don't need to remove all your @track decorators before deploying to production.
|
|
110
|
+
Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
|
|
111
|
+
|
|
112
|
+
```Bash
|
|
113
|
+
export PYVITALS_ENABLED=0
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
118
|
+
|
|
119
|
+
Clone the repository.
|
|
120
|
+
|
|
121
|
+
Install dependencies: pip install -e .
|
|
122
|
+
|
|
123
|
+
Run tests: pytest
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# PyVitals
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
4
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
5
|
+
[](https://github.com/yourusername/pyvitals/actions)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**PyVitals** is an intelligent, lightweight performance profiler for Python.
|
|
9
|
+
It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
|
|
10
|
+
|
|
11
|
+
Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
|
|
18
|
+
- **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
|
|
19
|
+
- **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
|
|
20
|
+
- **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
|
|
21
|
+
- **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
|
|
22
|
+
- **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install via `pip`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install pyvitals-profiler
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
1. As a Decorator (Sync & Async)
|
|
36
|
+
Simply add @track() to any function you want to profile.
|
|
37
|
+
|
|
38
|
+
```Python
|
|
39
|
+
import asyncio
|
|
40
|
+
from pyvitals import track
|
|
41
|
+
|
|
42
|
+
@track(name="Database Query", max_time_s=1.5)
|
|
43
|
+
async def fetch_users():
|
|
44
|
+
await asyncio.sleep(1.0) # Simulating I/O Wait
|
|
45
|
+
return {"status": "success"}
|
|
46
|
+
|
|
47
|
+
asyncio.run(fetch_users())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. As a Context Manager
|
|
51
|
+
If you only want to profile a specific loop without decorating the whole function, use the with statement.
|
|
52
|
+
|
|
53
|
+
```Python
|
|
54
|
+
from pyvitals import track
|
|
55
|
+
|
|
56
|
+
def process_heavy_data():
|
|
57
|
+
print("Initializing...")
|
|
58
|
+
|
|
59
|
+
# Only profile this specific block
|
|
60
|
+
with track(name="Matrix Math", max_mem_mb=50):
|
|
61
|
+
data = [x**2 for x in range(1_000_000)]
|
|
62
|
+
|
|
63
|
+
print("Done!")
|
|
64
|
+
|
|
65
|
+
process_heavy_data()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Exporting & Analyzing Data (CLI)
|
|
69
|
+
PyVitals allows you to save your performance history to detect regressions.
|
|
70
|
+
|
|
71
|
+
Step 1: Export to JSON
|
|
72
|
+
Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
|
|
73
|
+
|
|
74
|
+
```Python
|
|
75
|
+
from pyvitals import track
|
|
76
|
+
|
|
77
|
+
@track(export_to="vitals_history.json", quiet=True)
|
|
78
|
+
def background_task():
|
|
79
|
+
# ... task logic ...
|
|
80
|
+
pass
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Step 2: Use the CLI Analyzer
|
|
84
|
+
PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
|
|
85
|
+
|
|
86
|
+
```Bash
|
|
87
|
+
pyvitals vitals_history.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
|
|
91
|
+
|
|
92
|
+
## Production Kill-Switch
|
|
93
|
+
You don't need to remove all your @track decorators before deploying to production.
|
|
94
|
+
Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
|
|
95
|
+
|
|
96
|
+
```Bash
|
|
97
|
+
export PYVITALS_ENABLED=0
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
101
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
102
|
+
|
|
103
|
+
Clone the repository.
|
|
104
|
+
|
|
105
|
+
Install dependencies: pip install -e .
|
|
106
|
+
|
|
107
|
+
Run tests: pytest
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# pyproject.toml
|
|
2
|
+
|
|
3
|
+
[build-system]
|
|
4
|
+
requires = ["setuptools>=61.0"]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "pyvitals-profiler" # Choose a unique name on PyPI
|
|
9
|
+
version = "1.0.0"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name="Antoine M", email="projet24.apprentissage@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
description = "An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.8"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Topic :: Software Development :: Debuggers",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"rich>=10.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
# This creates a global terminal command automatically when the package is installed!
|
|
29
|
+
pyvitals = "pyvitals.analyzer:main"
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 88
|
|
33
|
+
target-version = "py38"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I"]
|
|
37
|
+
ignore = []
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_vitals_data(filepath: str) -> list:
|
|
12
|
+
"""Loads and parses the JSON vitals data file."""
|
|
13
|
+
if not os.path.exists(filepath):
|
|
14
|
+
console.print(f"[bold red]Error:[/] File '{filepath}' not found.")
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
except json.JSONDecodeError:
|
|
21
|
+
console.print(
|
|
22
|
+
f"[bold red]Error:[/] File '{filepath}' is not a valid JSON file."
|
|
23
|
+
)
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def analyze_and_display(data: list):
|
|
28
|
+
"""
|
|
29
|
+
Analyzes the raw profiling data, groups it by function name,
|
|
30
|
+
calculates Wall/CPU statistics, and renders a rich terminal table.
|
|
31
|
+
"""
|
|
32
|
+
if not data:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Group data by function name
|
|
36
|
+
grouped_data = {}
|
|
37
|
+
for entry in data:
|
|
38
|
+
name = entry.get("name", "Unknown")
|
|
39
|
+
if name not in grouped_data:
|
|
40
|
+
grouped_data[name] = {"wall_times": [], "cpu_times": [], "peaks": []}
|
|
41
|
+
|
|
42
|
+
# Handle backward compatibility with Phase 2 data
|
|
43
|
+
wall = entry.get("wall_time_s", entry.get("execution_time_s", 0))
|
|
44
|
+
# If cpu_time_s is missing (old data), default to 0
|
|
45
|
+
cpu = entry.get("cpu_time_s", 0)
|
|
46
|
+
|
|
47
|
+
grouped_data[name]["wall_times"].append(wall)
|
|
48
|
+
grouped_data[name]["cpu_times"].append(cpu)
|
|
49
|
+
grouped_data[name]["peaks"].append(entry.get("peak_memory_mb", 0))
|
|
50
|
+
|
|
51
|
+
# Prepare the Rich Table
|
|
52
|
+
table = Table(title="Performance Analysis", border_style="blue")
|
|
53
|
+
table.add_column("Function Name", style="bold yellow")
|
|
54
|
+
table.add_column("Runs", justify="right", style="cyan")
|
|
55
|
+
table.add_column("Avg Wall Time", justify="right")
|
|
56
|
+
table.add_column("Avg CPU Time", justify="right", style="magenta")
|
|
57
|
+
table.add_column("Workload Profile", justify="center")
|
|
58
|
+
table.add_column("Trend (Wall)", justify="right")
|
|
59
|
+
table.add_column("Max Peak Mem", justify="right", style="green")
|
|
60
|
+
|
|
61
|
+
for name, metrics in grouped_data.items():
|
|
62
|
+
wall_times = metrics["wall_times"]
|
|
63
|
+
cpu_times = metrics["cpu_times"]
|
|
64
|
+
peaks = metrics["peaks"]
|
|
65
|
+
|
|
66
|
+
runs = len(wall_times)
|
|
67
|
+
avg_wall = sum(wall_times) / runs if runs else 0
|
|
68
|
+
avg_cpu = sum(cpu_times) / runs if runs else 0
|
|
69
|
+
last_wall = wall_times[-1]
|
|
70
|
+
max_peak = max(peaks) if peaks else 0
|
|
71
|
+
|
|
72
|
+
# Calculate performance trend percentage (based on Wall Time)
|
|
73
|
+
if avg_wall > 0:
|
|
74
|
+
diff_pct = ((last_wall - avg_wall) / avg_wall) * 100
|
|
75
|
+
else:
|
|
76
|
+
diff_pct = 0.0
|
|
77
|
+
|
|
78
|
+
# Format trend coloring
|
|
79
|
+
if diff_pct > 5.0:
|
|
80
|
+
trend_str = f"[bold red]▲ +{diff_pct:.1f}%[/bold red]"
|
|
81
|
+
elif diff_pct < -5.0:
|
|
82
|
+
trend_str = f"[bold green]▼ {diff_pct:.1f}%[/bold green]"
|
|
83
|
+
else:
|
|
84
|
+
trend_str = f"[dim]~ {diff_pct:.1f}%[/dim]"
|
|
85
|
+
|
|
86
|
+
# Determine Workload Profile based on averages
|
|
87
|
+
if avg_wall > 0 and avg_cpu > 0:
|
|
88
|
+
utilization = (avg_cpu / avg_wall) * 100
|
|
89
|
+
if utilization > 80:
|
|
90
|
+
workload = "[bold red]CPU Bound[/bold red]"
|
|
91
|
+
elif utilization < 20:
|
|
92
|
+
workload = "[bold blue]I/O Bound[/bold blue]"
|
|
93
|
+
else:
|
|
94
|
+
workload = "[bold yellow]Mixed[/bold yellow]"
|
|
95
|
+
elif avg_wall > 0 and avg_cpu == 0:
|
|
96
|
+
# Catch strict I/O bounds where CPU time might round to exactly 0
|
|
97
|
+
workload = "[bold blue]I/O Bound[/bold blue]"
|
|
98
|
+
else:
|
|
99
|
+
workload = "[dim]Unknown[/dim]"
|
|
100
|
+
|
|
101
|
+
table.add_row(
|
|
102
|
+
name,
|
|
103
|
+
str(runs),
|
|
104
|
+
f"{avg_wall:.4f} s",
|
|
105
|
+
f"{avg_cpu:.4f} s",
|
|
106
|
+
workload,
|
|
107
|
+
trend_str,
|
|
108
|
+
f"{max_peak:.4f} MB",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
console.print(table)
|
|
112
|
+
console.print(
|
|
113
|
+
"[dim]Trend compares the last Wall Time to the historical average.[/dim]"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main():
|
|
118
|
+
"""CLI entry point."""
|
|
119
|
+
parser = argparse.ArgumentParser(description="Analyze PyVitals JSON export files.")
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"filepath",
|
|
122
|
+
type=str,
|
|
123
|
+
help="Path to the JSON vitals file (e.g., vitals_history.json)",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
args = parser.parse_args()
|
|
127
|
+
|
|
128
|
+
data = load_vitals_data(args.filepath)
|
|
129
|
+
analyze_and_display(data)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import tracemalloc
|
|
7
|
+
import warnings
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class track:
|
|
18
|
+
"""
|
|
19
|
+
Intelligent profiler for execution time (Wall/CPU) and memory usage.
|
|
20
|
+
Can be used as a decorator (@track) or a context manager (with track()).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, name=None, max_time_s=None, max_mem_mb=None, export_to=None, quiet=False
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the tracker with optional thresholds, data export, and verbosity control.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name (str, optional): Custom name for the display panel.
|
|
31
|
+
max_time_s (float, optional): Maximum allowed Wall Time in seconds.
|
|
32
|
+
max_mem_mb (float, optional): Maximum allowed peak memory in megabytes.
|
|
33
|
+
export_to (str, optional): File path to export the results in JSON format.
|
|
34
|
+
quiet (bool, optional): If True, suppresses the terminal output.
|
|
35
|
+
"""
|
|
36
|
+
self.name = name
|
|
37
|
+
self.max_time_s = max_time_s
|
|
38
|
+
self.max_mem_mb = max_mem_mb
|
|
39
|
+
self.export_to = export_to
|
|
40
|
+
self.quiet = quiet
|
|
41
|
+
self._start_time = None
|
|
42
|
+
self._start_cpu_time = None
|
|
43
|
+
self._is_enabled = os.getenv("PYVITALS_ENABLED", "1") != "0"
|
|
44
|
+
|
|
45
|
+
def _render_vitals(self, exec_time, cpu_time, current_mem, peak_mem, display_name):
|
|
46
|
+
"""Generates and prints the visual terminal interface with threshold checks."""
|
|
47
|
+
peak_mb = peak_mem / (1024 * 1024)
|
|
48
|
+
current_mb = current_mem / (1024 * 1024)
|
|
49
|
+
|
|
50
|
+
time_color = "bold cyan"
|
|
51
|
+
mem_color = "bold yellow"
|
|
52
|
+
panel_border = "blue"
|
|
53
|
+
|
|
54
|
+
# Check execution time threshold
|
|
55
|
+
if self.max_time_s and exec_time > self.max_time_s:
|
|
56
|
+
time_color = "bold red"
|
|
57
|
+
panel_border = "red"
|
|
58
|
+
warnings.warn(
|
|
59
|
+
f"[{display_name}] Wall Time ({exec_time:.4f}s) exceeded limit ({self.max_time_s}s)!",
|
|
60
|
+
RuntimeWarning,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check peak memory threshold
|
|
64
|
+
if self.max_mem_mb and peak_mb > self.max_mem_mb:
|
|
65
|
+
mem_color = "bold red"
|
|
66
|
+
panel_border = "red"
|
|
67
|
+
warnings.warn(
|
|
68
|
+
f"[{display_name}] Peak memory ({peak_mb:.4f}MB) exceeded limit ({self.max_mem_mb}MB)!",
|
|
69
|
+
RuntimeWarning,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Calculate CPU Utilization to determine the bottleneck type
|
|
73
|
+
cpu_utilization = (cpu_time / exec_time) * 100 if exec_time > 0 else 0
|
|
74
|
+
if cpu_utilization > 80:
|
|
75
|
+
bottleneck = "[bold red]CPU Bound (Heavy Math/Loops)[/bold red]"
|
|
76
|
+
elif cpu_utilization < 20:
|
|
77
|
+
bottleneck = "[bold blue]I/O Bound (Waiting/Network/Sleep)[/bold blue]"
|
|
78
|
+
else:
|
|
79
|
+
bottleneck = "[bold yellow]Mixed Workload[/bold yellow]"
|
|
80
|
+
|
|
81
|
+
# Table Construction (Using explicit spacing to avoid emoji-width bugs)
|
|
82
|
+
table = Table(show_header=False, box=None)
|
|
83
|
+
table.add_column(width=24)
|
|
84
|
+
table.add_column(justify="right")
|
|
85
|
+
|
|
86
|
+
table.add_row(
|
|
87
|
+
"Wall Time (Total)", f"[{time_color}]{exec_time:.4f} s[/{time_color}]"
|
|
88
|
+
)
|
|
89
|
+
table.add_row(
|
|
90
|
+
"CPU Time (Active)", f"[bold magenta]{cpu_time:.4f} s[/bold magenta]"
|
|
91
|
+
)
|
|
92
|
+
table.add_row("Workload Profile", bottleneck)
|
|
93
|
+
table.add_row("", "") # Blank row for visual spacing
|
|
94
|
+
table.add_row(
|
|
95
|
+
"Max Memory (Peak)", f"[{mem_color}]{peak_mb:.4f} MB[/{mem_color}]"
|
|
96
|
+
)
|
|
97
|
+
table.add_row(
|
|
98
|
+
"Remaining Memory", f"[bold green]{current_mb:.4f} MB[/bold green]"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Panel Display
|
|
102
|
+
panel = Panel(
|
|
103
|
+
table,
|
|
104
|
+
title=f"Results : [bold yellow]{display_name}[/bold yellow]",
|
|
105
|
+
expand=False,
|
|
106
|
+
border_style=panel_border,
|
|
107
|
+
)
|
|
108
|
+
console.print(panel)
|
|
109
|
+
|
|
110
|
+
def _export_to_json(self, exec_time, cpu_time, current_mem, peak_mem, display_name):
|
|
111
|
+
"""Appends the profiling metrics to a JSON file."""
|
|
112
|
+
if not self.export_to:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
peak_mb = peak_mem / (1024 * 1024)
|
|
116
|
+
current_mb = current_mem / (1024 * 1024)
|
|
117
|
+
|
|
118
|
+
payload = {
|
|
119
|
+
"timestamp": datetime.now().isoformat(),
|
|
120
|
+
"name": display_name,
|
|
121
|
+
"wall_time_s": round(exec_time, 4),
|
|
122
|
+
"cpu_time_s": round(cpu_time, 4),
|
|
123
|
+
"peak_memory_mb": round(peak_mb, 4),
|
|
124
|
+
"remaining_memory_mb": round(current_mb, 4),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
data = []
|
|
128
|
+
if os.path.exists(self.export_to):
|
|
129
|
+
try:
|
|
130
|
+
with open(self.export_to, "r", encoding="utf-8") as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
except (json.JSONDecodeError, IOError):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
data.append(payload)
|
|
136
|
+
|
|
137
|
+
with open(self.export_to, "w", encoding="utf-8") as f:
|
|
138
|
+
json.dump(data, f, indent=4)
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------
|
|
141
|
+
# CONTEXT MANAGER IMPLEMENTATION
|
|
142
|
+
# ---------------------------------------------------------
|
|
143
|
+
def __enter__(self):
|
|
144
|
+
"""Triggered when entering a 'with' block."""
|
|
145
|
+
if not self._is_enabled:
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
tracemalloc.start()
|
|
149
|
+
# Track both Wall Time and CPU Time
|
|
150
|
+
self._start_time = time.perf_counter()
|
|
151
|
+
self._start_cpu_time = time.process_time()
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
155
|
+
"""Triggered when exiting a 'with' block."""
|
|
156
|
+
if not self._is_enabled:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
# Stop trackers
|
|
160
|
+
end_time = time.perf_counter()
|
|
161
|
+
end_cpu_time = time.process_time()
|
|
162
|
+
current_mem, peak_mem = tracemalloc.get_traced_memory()
|
|
163
|
+
tracemalloc.stop()
|
|
164
|
+
|
|
165
|
+
exec_time = end_time - self._start_time
|
|
166
|
+
cpu_time = end_cpu_time - self._start_cpu_time
|
|
167
|
+
display_name = self.name or "Code Block"
|
|
168
|
+
|
|
169
|
+
if not self.quiet:
|
|
170
|
+
self._render_vitals(
|
|
171
|
+
exec_time, cpu_time, current_mem, peak_mem, display_name
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self._export_to_json(exec_time, cpu_time, current_mem, peak_mem, display_name)
|
|
175
|
+
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------
|
|
179
|
+
# DECORATOR IMPLEMENTATION
|
|
180
|
+
# ---------------------------------------------------------
|
|
181
|
+
def __call__(self, func):
|
|
182
|
+
"""Triggered when used as a decorator (@track)."""
|
|
183
|
+
if not self._is_enabled:
|
|
184
|
+
return func
|
|
185
|
+
|
|
186
|
+
display_name = self.name or f"{func.__name__}()"
|
|
187
|
+
|
|
188
|
+
@functools.wraps(func)
|
|
189
|
+
def sync_wrapper(*args, **kwargs):
|
|
190
|
+
self.name = display_name
|
|
191
|
+
with self:
|
|
192
|
+
return func(*args, **kwargs)
|
|
193
|
+
|
|
194
|
+
@functools.wraps(func)
|
|
195
|
+
async def async_wrapper(*args, **kwargs):
|
|
196
|
+
self.name = display_name
|
|
197
|
+
with self:
|
|
198
|
+
return await func(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
if inspect.iscoroutinefunction(func):
|
|
201
|
+
return async_wrapper
|
|
202
|
+
return sync_wrapper
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvitals-profiler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An intelligent, lightweight Wall/CPU time and memory profiler with beautiful terminal reports.
|
|
5
|
+
Author-email: Antoine M <projet24.apprentissage@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENCE
|
|
14
|
+
Requires-Dist: rich>=10.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PyVitals
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
20
|
+
[](https://pypi.org/project/pyvitals-profiler/)
|
|
21
|
+
[](https://github.com/yourusername/pyvitals/actions)
|
|
22
|
+
[](https://opensource.org/licenses/MIT)
|
|
23
|
+
|
|
24
|
+
**PyVitals** is an intelligent, lightweight performance profiler for Python.
|
|
25
|
+
It tracks **Wall Time**, **CPU Time**, and **Peak Memory** usage with surgical precision, generating beautiful terminal reports and JSON history logs.
|
|
26
|
+
|
|
27
|
+
Whether you are debugging a slow API or hunting down a memory leak, PyVitals tells you exactly if your code is *CPU Bound* or *I/O Bound*.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Universal:** Works seamlessly with both synchronous (`def`) and asynchronous (`async def`) functions.
|
|
34
|
+
- **Flexible:** Use it as a `@decorator` or as a `with` context manager for specific code blocks.
|
|
35
|
+
- **Smart Diagnostics:** Automatically calculates CPU vs. Wall Time to detect I/O or CPU bottlenecks.
|
|
36
|
+
- **Threshold Alerts:** Set limits for execution time or memory and get visual warnings if exceeded.
|
|
37
|
+
- **Zero-Overhead Production:** Built-in Kill-Switch environment variable to disable tracking instantly in production.
|
|
38
|
+
- **CLI Analyzer:** Export your metrics to JSON and use the built-in CLI tool to track performance regressions over time.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Install via `pip`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pyvitals-profiler
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
1. As a Decorator (Sync & Async)
|
|
52
|
+
Simply add @track() to any function you want to profile.
|
|
53
|
+
|
|
54
|
+
```Python
|
|
55
|
+
import asyncio
|
|
56
|
+
from pyvitals import track
|
|
57
|
+
|
|
58
|
+
@track(name="Database Query", max_time_s=1.5)
|
|
59
|
+
async def fetch_users():
|
|
60
|
+
await asyncio.sleep(1.0) # Simulating I/O Wait
|
|
61
|
+
return {"status": "success"}
|
|
62
|
+
|
|
63
|
+
asyncio.run(fetch_users())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
2. As a Context Manager
|
|
67
|
+
If you only want to profile a specific loop without decorating the whole function, use the with statement.
|
|
68
|
+
|
|
69
|
+
```Python
|
|
70
|
+
from pyvitals import track
|
|
71
|
+
|
|
72
|
+
def process_heavy_data():
|
|
73
|
+
print("Initializing...")
|
|
74
|
+
|
|
75
|
+
# Only profile this specific block
|
|
76
|
+
with track(name="Matrix Math", max_mem_mb=50):
|
|
77
|
+
data = [x**2 for x in range(1_000_000)]
|
|
78
|
+
|
|
79
|
+
print("Done!")
|
|
80
|
+
|
|
81
|
+
process_heavy_data()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Exporting & Analyzing Data (CLI)
|
|
85
|
+
PyVitals allows you to save your performance history to detect regressions.
|
|
86
|
+
|
|
87
|
+
Step 1: Export to JSON
|
|
88
|
+
Use the export_to parameter. You can also use quiet=True to hide the terminal output if you are running the function thousands of times.
|
|
89
|
+
|
|
90
|
+
```Python
|
|
91
|
+
from pyvitals import track
|
|
92
|
+
|
|
93
|
+
@track(export_to="vitals_history.json", quiet=True)
|
|
94
|
+
def background_task():
|
|
95
|
+
# ... task logic ...
|
|
96
|
+
pass
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Step 2: Use the CLI Analyzer
|
|
100
|
+
PyVitals comes with a built-in command-line tool. Point it to your JSON file to generate a rich historical trend analysis table:
|
|
101
|
+
|
|
102
|
+
```Bash
|
|
103
|
+
pyvitals vitals_history.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The CLI will display the average Wall/CPU time, the maximum peak memory, the workload profile (I/O vs CPU), and highlight performance regressions (e.g., ▲ +12.5% slower).
|
|
107
|
+
|
|
108
|
+
## Production Kill-Switch
|
|
109
|
+
You don't need to remove all your @track decorators before deploying to production.
|
|
110
|
+
Set the following environment variable to 0 to instantly disable PyVitals. It will bypass the tracking logic entirely, ensuring 0ms performance overhead.
|
|
111
|
+
|
|
112
|
+
```Bash
|
|
113
|
+
export PYVITALS_ENABLED=0
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
118
|
+
|
|
119
|
+
Clone the repository.
|
|
120
|
+
|
|
121
|
+
Install dependencies: pip install -e .
|
|
122
|
+
|
|
123
|
+
Run tests: pytest
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pyvitals/__init__.py
|
|
5
|
+
src/pyvitals/analyzer.py
|
|
6
|
+
src/pyvitals/profiler.py
|
|
7
|
+
src/pyvitals_profiler.egg-info/PKG-INFO
|
|
8
|
+
src/pyvitals_profiler.egg-info/SOURCES.txt
|
|
9
|
+
src/pyvitals_profiler.egg-info/dependency_links.txt
|
|
10
|
+
src/pyvitals_profiler.egg-info/entry_points.txt
|
|
11
|
+
src/pyvitals_profiler.egg-info/requires.txt
|
|
12
|
+
src/pyvitals_profiler.egg-info/top_level.txt
|
|
13
|
+
tests/test_profiler.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rich>=10.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyvitals
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pyvitals import track
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------
|
|
10
|
+
# 1. Test Synchronous Decorator
|
|
11
|
+
# ---------------------------------------------------------
|
|
12
|
+
def test_sync_decorator_returns_correct_value():
|
|
13
|
+
@track(quiet=True)
|
|
14
|
+
def compute(a, b):
|
|
15
|
+
return a + b
|
|
16
|
+
|
|
17
|
+
result = compute(5, 7)
|
|
18
|
+
assert result == 12, "The decorator modified the function's return value!"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------
|
|
22
|
+
# 2. Test Asynchronous Decorator
|
|
23
|
+
# ---------------------------------------------------------
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_async_decorator_returns_correct_value():
|
|
26
|
+
@track(quiet=True)
|
|
27
|
+
async def fetch_data():
|
|
28
|
+
await asyncio.sleep(0.01)
|
|
29
|
+
return {"status": "ok"}
|
|
30
|
+
|
|
31
|
+
result = await fetch_data()
|
|
32
|
+
assert result["status"] == "ok", "The async decorator failed to return the value!"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------
|
|
36
|
+
# 3. Test Context Manager
|
|
37
|
+
# ---------------------------------------------------------
|
|
38
|
+
def test_context_manager_executes_without_error():
|
|
39
|
+
data = []
|
|
40
|
+
with track(name="Test Loop", quiet=True):
|
|
41
|
+
for i in range(10):
|
|
42
|
+
data.append(i)
|
|
43
|
+
|
|
44
|
+
assert len(data) == 10, "Context manager blocked code execution!"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------
|
|
48
|
+
# 4. Test JSON Export
|
|
49
|
+
# ---------------------------------------------------------
|
|
50
|
+
def test_json_export_appends_data(tmp_path):
|
|
51
|
+
# tmp_path is a built-in pytest fixture for temporary directories
|
|
52
|
+
export_file = tmp_path / "test_vitals.json"
|
|
53
|
+
|
|
54
|
+
@track(export_to=str(export_file), quiet=True)
|
|
55
|
+
def dummy_task():
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# Run twice
|
|
59
|
+
dummy_task()
|
|
60
|
+
dummy_task()
|
|
61
|
+
|
|
62
|
+
# Verify the file was created and contains exactly 2 records
|
|
63
|
+
assert export_file.exists(), "JSON export file was not created!"
|
|
64
|
+
|
|
65
|
+
with open(export_file, "r") as f:
|
|
66
|
+
data = json.load(f)
|
|
67
|
+
|
|
68
|
+
assert len(data) == 2, f"Expected 2 records in JSON, found {len(data)}"
|
|
69
|
+
assert "wall_time_s" in data[0], "Missing keys in JSON payload"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------
|
|
73
|
+
# 5. Test Global Kill-Switch
|
|
74
|
+
# ---------------------------------------------------------
|
|
75
|
+
def test_kill_switch_disables_profiler(monkeypatch):
|
|
76
|
+
# Simulate setting the environment variable in production
|
|
77
|
+
monkeypatch.setenv("PYVITALS_ENABLED", "0")
|
|
78
|
+
|
|
79
|
+
@track(quiet=True)
|
|
80
|
+
def simple_func():
|
|
81
|
+
return "alive"
|
|
82
|
+
|
|
83
|
+
assert simple_func() == "alive"
|
|
84
|
+
# If the kill-switch works, tracemalloc won't crash if called outside,
|
|
85
|
+
# and the function should behave exactly as if undecorated.
|