oracletrace 2.0.0__tar.gz → 2.0.2__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.
- oracletrace-2.0.2/PKG-INFO +242 -0
- oracletrace-2.0.2/README.md +206 -0
- oracletrace-2.0.2/oracletrace/cli.py +160 -0
- oracletrace-2.0.2/oracletrace/compare.py +80 -0
- oracletrace-2.0.2/oracletrace/py.typed +0 -0
- oracletrace-2.0.2/oracletrace/reporters/__init__.py +3 -0
- oracletrace-2.0.2/oracletrace/reporters/html.py +273 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace/tracer.py +89 -57
- oracletrace-2.0.2/oracletrace.egg-info/PKG-INFO +242 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace.egg-info/SOURCES.txt +6 -1
- {oracletrace-2.0.0 → oracletrace-2.0.2}/pyproject.toml +4 -1
- oracletrace-2.0.2/tests/test_cli.py +438 -0
- oracletrace-2.0.2/tests/test_compare.py +299 -0
- oracletrace-2.0.2/tests/test_html_reporter.py +125 -0
- oracletrace-2.0.0/PKG-INFO +0 -313
- oracletrace-2.0.0/README.md +0 -264
- oracletrace-2.0.0/oracletrace/cli.py +0 -123
- oracletrace-2.0.0/oracletrace/compare.py +0 -56
- oracletrace-2.0.0/oracletrace.egg-info/PKG-INFO +0 -313
- oracletrace-2.0.0/tests/test_cli.py +0 -233
- {oracletrace-2.0.0 → oracletrace-2.0.2}/LICENSE +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/MANIFEST.in +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace/__init__.py +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace.egg-info/dependency_links.txt +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace.egg-info/entry_points.txt +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace.egg-info/requires.txt +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/oracletrace.egg-info/top_level.txt +0 -0
- {oracletrace-2.0.0 → oracletrace-2.0.2}/setup.cfg +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oracletrace
|
|
3
|
+
Version: 2.0.2
|
|
4
|
+
Summary: Detect Python performance regressions and compare execution traces with lightweight call graph visualization
|
|
5
|
+
Author: Kayk Caputo, André Gustavo
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Kayk Caputo and André Gustavo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://kaykcaputo.github.io/oracletrace/
|
|
28
|
+
Project-URL: Repository, https://github.com/KaykCaputo/oracletrace
|
|
29
|
+
Project-URL: Documentation, https://kaykcaputo.github.io/oracletrace/
|
|
30
|
+
Project-URL: Issues, https://github.com/KaykCaputo/oracletrace/issues
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: rich
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# OracleTrace — Detect Python Performance Regressions with Execution Diff
|
|
38
|
+
|
|
39
|
+
> Detect performance regressions between runs of your Python script in seconds.
|
|
40
|
+
|
|
41
|
+
<table><tr>
|
|
42
|
+
<td><img src="https://raw.githubusercontent.com/KaykCaputo/oracletrace/master/oracletracecat.png" alt="OracleTrace Logo" width="185"/></td>
|
|
43
|
+
<td>
|
|
44
|
+
|
|
45
|
+
**Fail your CI when performance regresses.**
|
|
46
|
+
|
|
47
|
+
OracleTrace is a **git diff for performance.**
|
|
48
|
+
|
|
49
|
+
**Run your script twice and instantly see what got slower — with function-level precision.**
|
|
50
|
+
|
|
51
|
+
</td>
|
|
52
|
+
</tr></table>
|
|
53
|
+
|
|
54
|
+
[](https://pypi.org/project/oracletrace)
|
|
55
|
+
[](https://pepy.tech/projects/oracletrace)
|
|
56
|
+
[](https://github.com/KaykCaputo/oracletrace/stargazers)
|
|
57
|
+
[](https://github.com/KaykCaputo/oracletrace/network/members)
|
|
58
|
+
[](https://github.com/KaykCaputo/oracletrace/actions/workflows/tests.yml)
|
|
59
|
+
|
|
60
|
+
Documentation: [https://kaykcaputo.github.io/oracletrace/](https://kaykcaputo.github.io/oracletrace/)
|
|
61
|
+
|
|
62
|
+
**Featured in:** [PyCoder's Weekly #729](https://pycoders.com/issues/729) • [awesome-debugger](https://github.com/taowen/awesome-debugger) • [awesome-profiling](https://github.com/msaroufim/awesome-profiling)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
### Installation
|
|
66
|
+
```bash
|
|
67
|
+
pip install oracletrace
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### 1. See where your program spends time instantly:
|
|
73
|
+
```bash
|
|
74
|
+
oracletrace app.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Compare runs and detect regressions:
|
|
78
|
+
```bash
|
|
79
|
+
oracletrace app.py --json baseline.json
|
|
80
|
+
oracletrace app.py --json new.json --compare baseline.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## See it in action
|
|
86
|
+
|
|
87
|
+
See exactly which functions got slower between runs:
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Example Output
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Starting application...
|
|
97
|
+
|
|
98
|
+
Iteration 1:
|
|
99
|
+
> Processing data...
|
|
100
|
+
> Calculating results...
|
|
101
|
+
|
|
102
|
+
Iteration 2:
|
|
103
|
+
> Processing data...
|
|
104
|
+
> Calculating results...
|
|
105
|
+
|
|
106
|
+
Application finished.
|
|
107
|
+
|
|
108
|
+
Summary:
|
|
109
|
+
Top functions by Total Time
|
|
110
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
|
|
111
|
+
┃ Function ┃ Total Time (s) ┃ Calls ┃ Avg. Time/Call (ms) ┃
|
|
112
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
|
|
113
|
+
│ my_app.py:main │ 0.6025 │ 1 │ 602.510 │
|
|
114
|
+
│ my_app.py:process_data │ 0.6021 │ 2 │ 301.050 │
|
|
115
|
+
│ my_app.py:calculate_results │ 0.4015 │ 2 │ 200.750 │
|
|
116
|
+
└──────────────────────────────┴────────────────┴───────┴─────────────────────┘
|
|
117
|
+
|
|
118
|
+
Logic Flow:
|
|
119
|
+
<module>
|
|
120
|
+
└── my_app.py:main (1x, 0.6025s)
|
|
121
|
+
└── my_app.py:process_data (2x, 0.6021s)
|
|
122
|
+
└── my_app.py:calculate_results (2x, 0.4015s)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Why OracleTrace?
|
|
128
|
+
|
|
129
|
+
### Problem
|
|
130
|
+
|
|
131
|
+
Performance regressions are hard to detect early.
|
|
132
|
+
|
|
133
|
+
### Solution
|
|
134
|
+
|
|
135
|
+
OracleTrace compares execution traces and highlights what got slower.
|
|
136
|
+
|
|
137
|
+
### How it works
|
|
138
|
+
|
|
139
|
+
1. Run your script
|
|
140
|
+
2. Generate a trace
|
|
141
|
+
3. Compare results
|
|
142
|
+
4. Identify slowdowns
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## CI Integration
|
|
147
|
+
|
|
148
|
+
Fail your pipeline when performance degrades:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
oracletrace app.py --json current.json --compare baseline.json --fail-on-regression --threshold 25
|
|
152
|
+
```
|
|
153
|
+
Add it to your CI to automatically fail on performance regressions.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Key Features
|
|
158
|
+
|
|
159
|
+
* Detect slower and faster functions
|
|
160
|
+
* Identify new or removed functions
|
|
161
|
+
* Execution time and call count analysis
|
|
162
|
+
* Call graph visualization
|
|
163
|
+
* JSON and CSV export
|
|
164
|
+
* Regex-based filtering (`--ignore`)
|
|
165
|
+
* Top-N function focus (`--top`)
|
|
166
|
+
* CI regression gates
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## CLI Reference
|
|
171
|
+
|
|
172
|
+
| Flag | Description |
|
|
173
|
+
| ---------------------- | -------------------------------------- |
|
|
174
|
+
| `--json` | Export trace to JSON |
|
|
175
|
+
| `--csv` | Export trace to CSV |
|
|
176
|
+
| `--compare` | Compare with another trace |
|
|
177
|
+
| `--fail-on-regression` | Exit with error if regression detected |
|
|
178
|
+
| `--threshold` | Regression percentage threshold |
|
|
179
|
+
| `--ignore` | Ignore functions/files via regex |
|
|
180
|
+
| `--top` | Show top N functions |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Use Cases
|
|
185
|
+
|
|
186
|
+
### Primary
|
|
187
|
+
|
|
188
|
+
* Detect performance regressions between runs
|
|
189
|
+
|
|
190
|
+
### Secondary
|
|
191
|
+
|
|
192
|
+
* CI performance validation
|
|
193
|
+
* Execution trace inspection
|
|
194
|
+
* Call graph visualization
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## How It Works
|
|
199
|
+
|
|
200
|
+
OracleTrace uses Python’s `sys.setprofile()` to intercept function calls and returns.
|
|
201
|
+
|
|
202
|
+
It measures execution time per function and records caller–callee relationships.
|
|
203
|
+
|
|
204
|
+
Filtering removes external/internal calls to focus on application code.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Requirements
|
|
209
|
+
|
|
210
|
+
* Python >= 3.10
|
|
211
|
+
* rich
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Contributing
|
|
216
|
+
|
|
217
|
+
Contributions are welcome.
|
|
218
|
+
|
|
219
|
+
Please read the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, coding standards, and the contribution process.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Contributors
|
|
224
|
+
|
|
225
|
+
<a href="https://github.com/KaykCaputo/oracletrace/graphs/contributors">
|
|
226
|
+
<img src="https://contrib.rocks/image?repo=KaykCaputo/oracletrace" />
|
|
227
|
+
</a>
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## ⭐ Support the Project
|
|
232
|
+
|
|
233
|
+
If OracleTrace is useful, consider giving it a star:
|
|
234
|
+
|
|
235
|
+
[GitHub Repository](https://github.com/KaykCaputo/oracletrace)
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Maintainers
|
|
240
|
+
|
|
241
|
+
* [Kayk Caputo](https://github.com/KaykCaputo)
|
|
242
|
+
* [André Gustavo](https://github.com/AndreXP1)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# OracleTrace — Detect Python Performance Regressions with Execution Diff
|
|
2
|
+
|
|
3
|
+
> Detect performance regressions between runs of your Python script in seconds.
|
|
4
|
+
|
|
5
|
+
<table><tr>
|
|
6
|
+
<td><img src="https://raw.githubusercontent.com/KaykCaputo/oracletrace/master/oracletracecat.png" alt="OracleTrace Logo" width="185"/></td>
|
|
7
|
+
<td>
|
|
8
|
+
|
|
9
|
+
**Fail your CI when performance regresses.**
|
|
10
|
+
|
|
11
|
+
OracleTrace is a **git diff for performance.**
|
|
12
|
+
|
|
13
|
+
**Run your script twice and instantly see what got slower — with function-level precision.**
|
|
14
|
+
|
|
15
|
+
</td>
|
|
16
|
+
</tr></table>
|
|
17
|
+
|
|
18
|
+
[](https://pypi.org/project/oracletrace)
|
|
19
|
+
[](https://pepy.tech/projects/oracletrace)
|
|
20
|
+
[](https://github.com/KaykCaputo/oracletrace/stargazers)
|
|
21
|
+
[](https://github.com/KaykCaputo/oracletrace/network/members)
|
|
22
|
+
[](https://github.com/KaykCaputo/oracletrace/actions/workflows/tests.yml)
|
|
23
|
+
|
|
24
|
+
Documentation: [https://kaykcaputo.github.io/oracletrace/](https://kaykcaputo.github.io/oracletrace/)
|
|
25
|
+
|
|
26
|
+
**Featured in:** [PyCoder's Weekly #729](https://pycoders.com/issues/729) • [awesome-debugger](https://github.com/taowen/awesome-debugger) • [awesome-profiling](https://github.com/msaroufim/awesome-profiling)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
### Installation
|
|
30
|
+
```bash
|
|
31
|
+
pip install oracletrace
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### 1. See where your program spends time instantly:
|
|
37
|
+
```bash
|
|
38
|
+
oracletrace app.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Compare runs and detect regressions:
|
|
42
|
+
```bash
|
|
43
|
+
oracletrace app.py --json baseline.json
|
|
44
|
+
oracletrace app.py --json new.json --compare baseline.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## See it in action
|
|
50
|
+
|
|
51
|
+
See exactly which functions got slower between runs:
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example Output
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Starting application...
|
|
61
|
+
|
|
62
|
+
Iteration 1:
|
|
63
|
+
> Processing data...
|
|
64
|
+
> Calculating results...
|
|
65
|
+
|
|
66
|
+
Iteration 2:
|
|
67
|
+
> Processing data...
|
|
68
|
+
> Calculating results...
|
|
69
|
+
|
|
70
|
+
Application finished.
|
|
71
|
+
|
|
72
|
+
Summary:
|
|
73
|
+
Top functions by Total Time
|
|
74
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
|
|
75
|
+
┃ Function ┃ Total Time (s) ┃ Calls ┃ Avg. Time/Call (ms) ┃
|
|
76
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
|
|
77
|
+
│ my_app.py:main │ 0.6025 │ 1 │ 602.510 │
|
|
78
|
+
│ my_app.py:process_data │ 0.6021 │ 2 │ 301.050 │
|
|
79
|
+
│ my_app.py:calculate_results │ 0.4015 │ 2 │ 200.750 │
|
|
80
|
+
└──────────────────────────────┴────────────────┴───────┴─────────────────────┘
|
|
81
|
+
|
|
82
|
+
Logic Flow:
|
|
83
|
+
<module>
|
|
84
|
+
└── my_app.py:main (1x, 0.6025s)
|
|
85
|
+
└── my_app.py:process_data (2x, 0.6021s)
|
|
86
|
+
└── my_app.py:calculate_results (2x, 0.4015s)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Why OracleTrace?
|
|
92
|
+
|
|
93
|
+
### Problem
|
|
94
|
+
|
|
95
|
+
Performance regressions are hard to detect early.
|
|
96
|
+
|
|
97
|
+
### Solution
|
|
98
|
+
|
|
99
|
+
OracleTrace compares execution traces and highlights what got slower.
|
|
100
|
+
|
|
101
|
+
### How it works
|
|
102
|
+
|
|
103
|
+
1. Run your script
|
|
104
|
+
2. Generate a trace
|
|
105
|
+
3. Compare results
|
|
106
|
+
4. Identify slowdowns
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## CI Integration
|
|
111
|
+
|
|
112
|
+
Fail your pipeline when performance degrades:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
oracletrace app.py --json current.json --compare baseline.json --fail-on-regression --threshold 25
|
|
116
|
+
```
|
|
117
|
+
Add it to your CI to automatically fail on performance regressions.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Key Features
|
|
122
|
+
|
|
123
|
+
* Detect slower and faster functions
|
|
124
|
+
* Identify new or removed functions
|
|
125
|
+
* Execution time and call count analysis
|
|
126
|
+
* Call graph visualization
|
|
127
|
+
* JSON and CSV export
|
|
128
|
+
* Regex-based filtering (`--ignore`)
|
|
129
|
+
* Top-N function focus (`--top`)
|
|
130
|
+
* CI regression gates
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## CLI Reference
|
|
135
|
+
|
|
136
|
+
| Flag | Description |
|
|
137
|
+
| ---------------------- | -------------------------------------- |
|
|
138
|
+
| `--json` | Export trace to JSON |
|
|
139
|
+
| `--csv` | Export trace to CSV |
|
|
140
|
+
| `--compare` | Compare with another trace |
|
|
141
|
+
| `--fail-on-regression` | Exit with error if regression detected |
|
|
142
|
+
| `--threshold` | Regression percentage threshold |
|
|
143
|
+
| `--ignore` | Ignore functions/files via regex |
|
|
144
|
+
| `--top` | Show top N functions |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Use Cases
|
|
149
|
+
|
|
150
|
+
### Primary
|
|
151
|
+
|
|
152
|
+
* Detect performance regressions between runs
|
|
153
|
+
|
|
154
|
+
### Secondary
|
|
155
|
+
|
|
156
|
+
* CI performance validation
|
|
157
|
+
* Execution trace inspection
|
|
158
|
+
* Call graph visualization
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## How It Works
|
|
163
|
+
|
|
164
|
+
OracleTrace uses Python’s `sys.setprofile()` to intercept function calls and returns.
|
|
165
|
+
|
|
166
|
+
It measures execution time per function and records caller–callee relationships.
|
|
167
|
+
|
|
168
|
+
Filtering removes external/internal calls to focus on application code.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
* Python >= 3.10
|
|
175
|
+
* rich
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Contributions are welcome.
|
|
182
|
+
|
|
183
|
+
Please read the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, coding standards, and the contribution process.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Contributors
|
|
188
|
+
|
|
189
|
+
<a href="https://github.com/KaykCaputo/oracletrace/graphs/contributors">
|
|
190
|
+
<img src="https://contrib.rocks/image?repo=KaykCaputo/oracletrace" />
|
|
191
|
+
</a>
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## ⭐ Support the Project
|
|
196
|
+
|
|
197
|
+
If OracleTrace is useful, consider giving it a star:
|
|
198
|
+
|
|
199
|
+
[GitHub Repository](https://github.com/KaykCaputo/oracletrace)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Maintainers
|
|
204
|
+
|
|
205
|
+
* [Kayk Caputo](https://github.com/KaykCaputo)
|
|
206
|
+
* [André Gustavo](https://github.com/AndreXP1)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import runpy
|
|
6
|
+
import csv
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
from re import Pattern
|
|
10
|
+
from argparse import ArgumentParser, Namespace
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
from .tracer import Tracer, TracerData
|
|
13
|
+
from .compare import compare_traces, ComparisonData
|
|
14
|
+
from .reporters import generate_html_report
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
module_name = __name__.split(".")[0]
|
|
19
|
+
parser: ArgumentParser = ArgumentParser(
|
|
20
|
+
prog=module_name,
|
|
21
|
+
description="OracleTrace - Lightweight execution tracer for Python projects"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("target", help="Python script to trace")
|
|
24
|
+
parser.add_argument("--json", help="Export trace result to JSON file")
|
|
25
|
+
parser.add_argument("--compare", help="Compare against previous trace JSON")
|
|
26
|
+
parser.add_argument("--csv", help="Export trace result to CSV file")
|
|
27
|
+
parser.add_argument("--html", help="Export trace result to interactive HTML file")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--ignore",
|
|
30
|
+
metavar="REGEX",
|
|
31
|
+
nargs="+",
|
|
32
|
+
help="Space separated list of regex patterns for keys (file path and function name) to ignore."
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--top",
|
|
36
|
+
metavar="NUMBER",
|
|
37
|
+
help="Limits the number of functions shown in the summary table"
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--fail-on-regression",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Exit with a non-zero code when regression exceeds threshold.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--threshold",
|
|
46
|
+
type=float,
|
|
47
|
+
default=5.0,
|
|
48
|
+
help="Regression threshold percentage used with --fail-on-regression.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--only-regressions",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Hide functions which didn't run slower than baseline. Use with --compare"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--version",
|
|
58
|
+
action="version",
|
|
59
|
+
help="Print version information then exit with a zero code",
|
|
60
|
+
version=f"%(prog)s {version(module_name)}"
|
|
61
|
+
)
|
|
62
|
+
args: Namespace = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
target: str = args.target
|
|
65
|
+
|
|
66
|
+
# Validate --top argument manually to ensure all paths are testable
|
|
67
|
+
if args.top is not None:
|
|
68
|
+
try:
|
|
69
|
+
args.top = int(args.top)
|
|
70
|
+
except ValueError:
|
|
71
|
+
print(f"argument --top: invalid int value: '{args.top}'", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
if args.top < 1:
|
|
74
|
+
print(f"--top must be a positive integer, got: {args.top}", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
if not os.path.exists(target):
|
|
78
|
+
print(f"Target not found: {target}", file=sys.stderr)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
target = os.path.abspath(target)
|
|
82
|
+
root: str = os.getcwd()
|
|
83
|
+
target_dir: str = os.path.dirname(target)
|
|
84
|
+
# Setup paths so imports work correctly in the target script
|
|
85
|
+
sys.path.insert(0, target_dir)
|
|
86
|
+
ignored_args: List[str] = [] if args.ignore is None else args.ignore
|
|
87
|
+
ignore_patterns: List[Pattern] = []
|
|
88
|
+
|
|
89
|
+
for pattern in ignored_args:
|
|
90
|
+
try:
|
|
91
|
+
ignore_patterns.append(re.compile(pattern))
|
|
92
|
+
except re.error as e:
|
|
93
|
+
print(f"Regex error: {pattern} -> {e}", file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
# Start tracing, run the script, then stop
|
|
97
|
+
tracer: Tracer = Tracer(root, ignore_patterns=ignore_patterns)
|
|
98
|
+
tracer.start()
|
|
99
|
+
try:
|
|
100
|
+
runpy.run_path(target, run_name="__main__")
|
|
101
|
+
finally:
|
|
102
|
+
tracer.stop()
|
|
103
|
+
|
|
104
|
+
data: TracerData = tracer.get_trace_data()
|
|
105
|
+
|
|
106
|
+
# Save json
|
|
107
|
+
if args.json:
|
|
108
|
+
with open(args.json, "w", encoding="utf-8") as f:
|
|
109
|
+
json.dump(asdict(data), f, indent=4)
|
|
110
|
+
|
|
111
|
+
# Display the analysis
|
|
112
|
+
if args.top is not None:
|
|
113
|
+
tracer.show_results(args.top)
|
|
114
|
+
else:
|
|
115
|
+
tracer.show_results(None)
|
|
116
|
+
|
|
117
|
+
# Export as csv
|
|
118
|
+
if args.csv:
|
|
119
|
+
with open(args.csv, "w", newline="", encoding="utf-8") as f:
|
|
120
|
+
writer: csv.DictWriter = csv.DictWriter(f, fieldnames=["function", "total_time", "calls", "avg_time"])
|
|
121
|
+
writer.writeheader()
|
|
122
|
+
for fn in data.functions:
|
|
123
|
+
writer.writerow({
|
|
124
|
+
"function": fn.name,
|
|
125
|
+
"total_time": fn.total_time,
|
|
126
|
+
"calls": fn.call_count,
|
|
127
|
+
"avg_time": fn.avg_time,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
# Generare a report as html
|
|
131
|
+
if args.html:
|
|
132
|
+
generate_html_report(data, args.html)
|
|
133
|
+
print(f"HTML report generated: {os.path.abspath(args.html)}")
|
|
134
|
+
|
|
135
|
+
comparison_result: Optional[ComparisonData] = None
|
|
136
|
+
|
|
137
|
+
# Compare jsons
|
|
138
|
+
if args.compare:
|
|
139
|
+
if not os.path.exists(args.compare):
|
|
140
|
+
print(f"Compare file not found: {args.compare}", file=sys.stderr)
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
with open(args.compare, "r", encoding="utf-8") as f:
|
|
144
|
+
old_data: TracerData = TracerData.from_dict(json.load(f))
|
|
145
|
+
|
|
146
|
+
comparison_result = compare_traces(old_data, data, threshold=args.threshold, show_only_regressions=args.only_regressions)
|
|
147
|
+
|
|
148
|
+
if args.fail_on_regression and comparison_result.has_regression:
|
|
149
|
+
print(
|
|
150
|
+
f"Build failed: performance regression above {args.threshold:.2f}% detected.",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
return 2
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from .tracer import TracerData, FunctionData
|
|
2
|
+
from rich import print
|
|
3
|
+
from typing import Any, Dict, List, Set, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class RegressionData:
|
|
8
|
+
name: str
|
|
9
|
+
old_time: float
|
|
10
|
+
new_time: float
|
|
11
|
+
percent: float
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ComparisonData:
|
|
15
|
+
regressions: List[RegressionData]
|
|
16
|
+
has_regression: bool
|
|
17
|
+
|
|
18
|
+
def compare_traces(
|
|
19
|
+
old_data: TracerData,
|
|
20
|
+
new_data: TracerData,
|
|
21
|
+
threshold: float = 5.0,
|
|
22
|
+
show_only_regressions: bool = False
|
|
23
|
+
) -> ComparisonData:
|
|
24
|
+
old_funcs: Dict[str, FunctionData] = {f.name: f for f in old_data.functions}
|
|
25
|
+
new_funcs: Dict[str, FunctionData] = {f.name: f for f in new_data.functions}
|
|
26
|
+
|
|
27
|
+
regressions: List[RegressionData] = []
|
|
28
|
+
|
|
29
|
+
print("\n[bold cyan]Comparison Results:[/]\n")
|
|
30
|
+
|
|
31
|
+
all_functions: Set[str] = set(old_funcs) | set(new_funcs)
|
|
32
|
+
|
|
33
|
+
for name in sorted(all_functions):
|
|
34
|
+
old: Optional[FunctionData] = old_funcs.get(name)
|
|
35
|
+
new: Optional[FunctionData] = new_funcs.get(name)
|
|
36
|
+
|
|
37
|
+
if not old:
|
|
38
|
+
print(f"[green]+ {name} (new function)[/]")
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if not new:
|
|
42
|
+
print(f"[red]- {name} (removed)[/]")
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
old_time: float = old.total_time
|
|
46
|
+
new_time: float = new.total_time
|
|
47
|
+
|
|
48
|
+
if old_time == 0:
|
|
49
|
+
if new_time == 0:
|
|
50
|
+
print(f"{name} [yellow](no signal)[/]")
|
|
51
|
+
continue
|
|
52
|
+
print(f"{name} [yellow](no baseline)[/]")
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
diff: float = new_time - old_time
|
|
56
|
+
percent: float = (diff / old_time) * 100
|
|
57
|
+
|
|
58
|
+
color: str = "red" if percent > threshold else "green" if percent < -threshold else "yellow"
|
|
59
|
+
|
|
60
|
+
print(
|
|
61
|
+
f"{name}\n"
|
|
62
|
+
f" total_time: {old_time:.4f}s → {new_time:.4f}s "
|
|
63
|
+
f"[{color}]({percent:+.2f}%)[/]\n"
|
|
64
|
+
) if not show_only_regressions or percent > threshold else ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if percent > threshold:
|
|
68
|
+
regressions.append(
|
|
69
|
+
RegressionData(
|
|
70
|
+
name = name,
|
|
71
|
+
new_time = new_time,
|
|
72
|
+
old_time = old_time,
|
|
73
|
+
percent = percent
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return ComparisonData(
|
|
78
|
+
regressions = regressions,
|
|
79
|
+
has_regression = len(regressions) > 0
|
|
80
|
+
)
|
|
File without changes
|