pyinsider 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.
- pyinsider-1.0.0/.gitignore +34 -0
- pyinsider-1.0.0/CHANGELOG.md +38 -0
- pyinsider-1.0.0/LICENSE +21 -0
- pyinsider-1.0.0/PKG-INFO +240 -0
- pyinsider-1.0.0/README.md +186 -0
- pyinsider-1.0.0/docs/limitations.md +181 -0
- pyinsider-1.0.0/examples/complex_values.py +51 -0
- pyinsider-1.0.0/examples/demo_app/README.md +44 -0
- pyinsider-1.0.0/examples/demo_app/__init__.py +12 -0
- pyinsider-1.0.0/examples/demo_app/main.py +68 -0
- pyinsider-1.0.0/examples/demo_app/orders.py +25 -0
- pyinsider-1.0.0/examples/demo_app/pricing.py +33 -0
- pyinsider-1.0.0/examples/demo_app/report.py +15 -0
- pyinsider-1.0.0/examples/demo_app/validation.py +26 -0
- pyinsider-1.0.0/examples/exception.py +26 -0
- pyinsider-1.0.0/examples/nested_calls.py +30 -0
- pyinsider-1.0.0/examples/recursion.py +24 -0
- pyinsider-1.0.0/examples/repeated_calls.py +26 -0
- pyinsider-1.0.0/examples/simple.py +36 -0
- pyinsider-1.0.0/examples/slow_function.py +37 -0
- pyinsider-1.0.0/pyinsider/__init__.py +42 -0
- pyinsider-1.0.0/pyinsider/__main__.py +7 -0
- pyinsider-1.0.0/pyinsider/cli/__init__.py +11 -0
- pyinsider-1.0.0/pyinsider/cli/compare.py +136 -0
- pyinsider-1.0.0/pyinsider/cli/format.py +133 -0
- pyinsider-1.0.0/pyinsider/cli/main.py +102 -0
- pyinsider-1.0.0/pyinsider/cli/open.py +212 -0
- pyinsider-1.0.0/pyinsider/cli/run.py +253 -0
- pyinsider-1.0.0/pyinsider/core/__init__.py +38 -0
- pyinsider-1.0.0/pyinsider/core/analysis.py +219 -0
- pyinsider-1.0.0/pyinsider/core/models.py +151 -0
- pyinsider-1.0.0/pyinsider/core/serialization.py +223 -0
- pyinsider-1.0.0/pyinsider/core/trace.py +109 -0
- pyinsider-1.0.0/pyinsider/core/values.py +204 -0
- pyinsider-1.0.0/pyinsider/tracer/__init__.py +23 -0
- pyinsider-1.0.0/pyinsider/tracer/config.py +97 -0
- pyinsider-1.0.0/pyinsider/tracer/engine.py +338 -0
- pyinsider-1.0.0/pyinsider/tracer/runner.py +177 -0
- pyinsider-1.0.0/pyinsider/tracer/session.py +111 -0
- pyinsider-1.0.0/pyinsider/viewer/__init__.py +13 -0
- pyinsider-1.0.0/pyinsider/viewer/payload.py +193 -0
- pyinsider-1.0.0/pyinsider/viewer/server.py +155 -0
- pyinsider-1.0.0/pyinsider/viewer/static/app.css +559 -0
- pyinsider-1.0.0/pyinsider/viewer/static/app.js +280 -0
- pyinsider-1.0.0/pyinsider/viewer/static/flame.js +251 -0
- pyinsider-1.0.0/pyinsider/viewer/static/index.html +176 -0
- pyinsider-1.0.0/pyinsider/viewer/static/panels.js +288 -0
- pyinsider-1.0.0/pyinsider/viewer/static/state.js +162 -0
- pyinsider-1.0.0/pyinsider/viewer/static/timeline.js +349 -0
- pyinsider-1.0.0/pyinsider/viewer/static/tree.js +190 -0
- pyinsider-1.0.0/pyproject.toml +113 -0
- pyinsider-1.0.0/tests/__init__.py +0 -0
- pyinsider-1.0.0/tests/conftest.py +32 -0
- pyinsider-1.0.0/tests/test_cli.py +147 -0
- pyinsider-1.0.0/tests/test_core.py +156 -0
- pyinsider-1.0.0/tests/test_tracer.py +243 -0
- pyinsider-1.0.0/tests/test_values.py +111 -0
- pyinsider-1.0.0/tests/test_viewer.py +106 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# PyInsider output — traces can contain captured values, never commit them
|
|
22
|
+
traces/
|
|
23
|
+
*.tl
|
|
24
|
+
!tests/data/*.tl
|
|
25
|
+
|
|
26
|
+
# Editors / OS
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
31
|
+
|
|
32
|
+
# Scratch
|
|
33
|
+
_smoke.py
|
|
34
|
+
tmp/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to PyInsider are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
Initial release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Deterministic tracer** built on `sys.setprofile` / `threading.setprofile`,
|
|
17
|
+
recording the full call tree with nested-call and recursion handling.
|
|
18
|
+
- **Typed trace model** (`ExecutionEvent`, `Trace`, `RunMetadata`) with
|
|
19
|
+
inclusive and self timings, per-thread stacks, and parent/child links.
|
|
20
|
+
- **Bounded, safe value capture** that never retains references, never consumes
|
|
21
|
+
iterators, and guards every `repr()`.
|
|
22
|
+
- **Exception exploration** that attributes propagating exceptions to the
|
|
23
|
+
recorded call path by walking the traceback.
|
|
24
|
+
- **Versioned `.tl` trace format** — gzip-compressed JSON, schema-checked, no
|
|
25
|
+
pickle.
|
|
26
|
+
- **Analysis functions**: `summarize`, `function_stats`, `hot_paths`, and
|
|
27
|
+
`compare` (recursion-aware inclusive time).
|
|
28
|
+
- **CLI** (`pyinsider`) with `run`, `open`, `compare`, and `--version`;
|
|
29
|
+
preserves program exit codes and forwards arguments.
|
|
30
|
+
- **Interactive local viewer** (stdlib HTTP server bound to `127.0.0.1`) with an
|
|
31
|
+
interactive timeline, virtualised call tree, source view, details panel,
|
|
32
|
+
exceptions view, hot paths, flame graph, and filtering.
|
|
33
|
+
- **Public Python API**: `pyinsider.trace()` context manager and
|
|
34
|
+
`pyinsider.checkpoint()`.
|
|
35
|
+
- Examples, tests, type checking, linting, and honest limitations documentation.
|
|
36
|
+
|
|
37
|
+
[Unreleased]: https://github.com/Yashjindal11/pyinsider/compare/v1.0.0...HEAD
|
|
38
|
+
[1.0.0]: https://github.com/Yashjindal11/pyinsider/releases/tag/v1.0.0
|
pyinsider-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PyInsider contributors
|
|
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.
|
pyinsider-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyinsider
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: See what your Python program actually did — execution tracing, timeline and call-tree exploration.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yashsutar/insider
|
|
6
|
+
Project-URL: Repository, https://github.com/yashsutar/insider
|
|
7
|
+
Project-URL: Issues, https://github.com/yashsutar/insider/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yashsutar/insider/blob/main/CHANGELOG.md
|
|
9
|
+
Author: PyInsider contributors
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 PyInsider contributors
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: call-graph,debugging,developer-tools,flame-graph,observability,performance,profiler,tracing
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
44
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
45
|
+
Classifier: Topic :: System :: Monitoring
|
|
46
|
+
Classifier: Typing :: Typed
|
|
47
|
+
Requires-Python: >=3.10
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# PyInsider
|
|
56
|
+
|
|
57
|
+
**See what your Python program actually did.**
|
|
58
|
+
|
|
59
|
+
PyInsider records the real execution of a Python program — every function call,
|
|
60
|
+
how long each took, how calls nested, what arguments went in, what came back,
|
|
61
|
+
and the exact path that led to an exception — and lets you explore it afterward
|
|
62
|
+
in an interactive local viewer.
|
|
63
|
+
|
|
64
|
+
It is not a profiler that gives you aggregate numbers, and it is not a debugger
|
|
65
|
+
that stops the world. It is an **execution recorder**: run once, then look at
|
|
66
|
+
what happened.
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
$ pyinsider run examples/nested_calls.py
|
|
70
|
+
recorded 22 calls · max depth 5 · 3.14 ms
|
|
71
|
+
trace written to traces/run-2026-08-29-001.tl
|
|
72
|
+
|
|
73
|
+
$ pyinsider open traces/run-2026-08-29-001.tl
|
|
74
|
+
serving viewer at http://127.0.0.1:8721 (Ctrl-C to stop)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Deterministic, not sampled.** Every call is recorded via CPython's
|
|
78
|
+
`sys.setprofile` hook — no statistical guessing.
|
|
79
|
+
- **No AI, no telemetry, no cloud.** Everything runs and stays on your machine.
|
|
80
|
+
The viewer binds only to `127.0.0.1`. Nothing is uploaded, ever.
|
|
81
|
+
- **Local-first trace files.** A run produces a single versioned `.tl` file you
|
|
82
|
+
can save, diff, and share.
|
|
83
|
+
- **Four clean layers.** The tracing and analysis engines have no dependency on
|
|
84
|
+
the UI, so you can use PyInsider entirely from Python if you prefer.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
Requires **Python 3.10+**. PyInsider has **no runtime dependencies**.
|
|
91
|
+
|
|
92
|
+
```console
|
|
93
|
+
pip install pyinsider
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or from a clone, for development:
|
|
97
|
+
|
|
98
|
+
```console
|
|
99
|
+
git clone https://github.com/Yashjindal11/pyinsider.git
|
|
100
|
+
cd pyinsider
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Quickstart
|
|
105
|
+
|
|
106
|
+
Record a script and open the result:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
pyinsider run examples/simple.py
|
|
110
|
+
pyinsider open traces/ # opens the most recent trace in your browser
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Capture arguments and return values too:
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
pyinsider run examples/complex_values.py --detail
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Forward arguments to your program (everything after `--` goes to the script):
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
pyinsider run app.py -- --verbose input.csv
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Prefer to stay in the terminal? Skip the browser:
|
|
126
|
+
|
|
127
|
+
```console
|
|
128
|
+
pyinsider open traces/run-2026-08-29-001.tl --summary # top-level summary
|
|
129
|
+
pyinsider open traces/run-2026-08-29-001.tl --tree # printed call tree
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Using it from Python
|
|
133
|
+
|
|
134
|
+
The recorder is a plain context manager. The UI is optional.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import pyinsider
|
|
138
|
+
|
|
139
|
+
with pyinsider.trace() as run:
|
|
140
|
+
result = do_some_work()
|
|
141
|
+
|
|
142
|
+
trace = run.result
|
|
143
|
+
run.save("work.tl")
|
|
144
|
+
|
|
145
|
+
# Analyse without any UI:
|
|
146
|
+
for stat in pyinsider.hot_paths(trace):
|
|
147
|
+
print(stat.key, stat.inclusive_ms)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`pyinsider.checkpoint("loaded config")` drops a labelled marker into the active
|
|
151
|
+
trace so you can find a moment on the timeline later.
|
|
152
|
+
|
|
153
|
+
## The CLI
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
pyinsider run <script.py | -m module> [--detail] [-o FILE] [-- args...]
|
|
157
|
+
pyinsider open <file.tl | dir> [--summary | --tree | --no-browser]
|
|
158
|
+
pyinsider compare <before.tl> <after.tl>
|
|
159
|
+
pyinsider --version
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `run` preserves your program's exit code and forwards its arguments, so it is
|
|
163
|
+
safe to drop in front of an existing command.
|
|
164
|
+
- `open` accepts either a file or a directory (it picks the newest `.tl`).
|
|
165
|
+
- `compare` diffs two runs and highlights functions that got slower, faster,
|
|
166
|
+
appeared, or disappeared.
|
|
167
|
+
|
|
168
|
+
Colour output honours `NO_COLOR`, `TERM=dumb`, and non-TTY pipes.
|
|
169
|
+
|
|
170
|
+
## Trace files (`.tl`)
|
|
171
|
+
|
|
172
|
+
A `.tl` file is gzip-compressed JSON with an explicit schema version. It
|
|
173
|
+
contains the full call tree, timings (nanoseconds relative to run start),
|
|
174
|
+
captured values, exception information, and run metadata (Python version,
|
|
175
|
+
platform, detail level). There is **no pickle** and no code in a trace file —
|
|
176
|
+
it is safe to open and inspect by hand.
|
|
177
|
+
|
|
178
|
+
Trace files are forward-compatible within a major schema version and are
|
|
179
|
+
rejected if the major version is newer than the reader understands.
|
|
180
|
+
|
|
181
|
+
## The viewer
|
|
182
|
+
|
|
183
|
+
`pyinsider open` starts a tiny local web app (stdlib only, `127.0.0.1`) that
|
|
184
|
+
shows:
|
|
185
|
+
|
|
186
|
+
- an interactive, zoomable **timeline** of the run,
|
|
187
|
+
- a virtualised **call tree** with inclusive/self timings,
|
|
188
|
+
- a **source view** pointing at where each call was defined,
|
|
189
|
+
- a **details** panel with arguments and return values,
|
|
190
|
+
- an **exceptions** view with the call path that led to each failure,
|
|
191
|
+
- **hot paths** and an aggregated **flame graph**,
|
|
192
|
+
- filtering by module, name, duration, and project-vs-library code.
|
|
193
|
+
|
|
194
|
+
## Architecture
|
|
195
|
+
|
|
196
|
+
PyInsider is built in four layers, each depending only on the ones below it:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
cli / api / viewer <- presentation (never imported by the engine)
|
|
200
|
+
|
|
|
201
|
+
trace model + analysis <- Trace, function_stats, hot_paths, compare
|
|
202
|
+
|
|
|
203
|
+
trace events <- typed, serialisable ExecutionEvent records
|
|
204
|
+
|
|
|
205
|
+
tracer / collector <- sys.setprofile instrumentation
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The tracer and analysis code never import the CLI or viewer, so the engine is
|
|
209
|
+
usable as a library and the UI can be replaced without touching the core.
|
|
210
|
+
|
|
211
|
+
## Limitations
|
|
212
|
+
|
|
213
|
+
PyInsider is deliberately honest about what it can and cannot see. Highlights:
|
|
214
|
+
|
|
215
|
+
- **Caught exceptions** are attributed by walking the exception's traceback, so
|
|
216
|
+
only exceptions that propagate are marked; an exception swallowed by a
|
|
217
|
+
`try/except` is not attributed to the function that raised it.
|
|
218
|
+
- **C-level functions** (built-ins, C extensions) are not recorded as calls.
|
|
219
|
+
- Timings include tracing overhead; treat them as *relative*, not absolute.
|
|
220
|
+
- Async and multi-threaded code is recorded per-thread/per-task but concurrency
|
|
221
|
+
interleaving is simplified.
|
|
222
|
+
- Subprocesses are invisible to the tracer.
|
|
223
|
+
|
|
224
|
+
See [`docs/limitations.md`](docs/limitations.md) for the full, detailed list —
|
|
225
|
+
it is required reading before you trust a number.
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```console
|
|
230
|
+
pip install -e ".[dev]"
|
|
231
|
+
pytest # run the test suite
|
|
232
|
+
ruff check pyinsider # lint
|
|
233
|
+
mypy pyinsider # type-check
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and project layout.
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
[MIT](LICENSE). PyInsider is free and open source.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# PyInsider
|
|
2
|
+
|
|
3
|
+
**See what your Python program actually did.**
|
|
4
|
+
|
|
5
|
+
PyInsider records the real execution of a Python program — every function call,
|
|
6
|
+
how long each took, how calls nested, what arguments went in, what came back,
|
|
7
|
+
and the exact path that led to an exception — and lets you explore it afterward
|
|
8
|
+
in an interactive local viewer.
|
|
9
|
+
|
|
10
|
+
It is not a profiler that gives you aggregate numbers, and it is not a debugger
|
|
11
|
+
that stops the world. It is an **execution recorder**: run once, then look at
|
|
12
|
+
what happened.
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ pyinsider run examples/nested_calls.py
|
|
16
|
+
recorded 22 calls · max depth 5 · 3.14 ms
|
|
17
|
+
trace written to traces/run-2026-08-29-001.tl
|
|
18
|
+
|
|
19
|
+
$ pyinsider open traces/run-2026-08-29-001.tl
|
|
20
|
+
serving viewer at http://127.0.0.1:8721 (Ctrl-C to stop)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **Deterministic, not sampled.** Every call is recorded via CPython's
|
|
24
|
+
`sys.setprofile` hook — no statistical guessing.
|
|
25
|
+
- **No AI, no telemetry, no cloud.** Everything runs and stays on your machine.
|
|
26
|
+
The viewer binds only to `127.0.0.1`. Nothing is uploaded, ever.
|
|
27
|
+
- **Local-first trace files.** A run produces a single versioned `.tl` file you
|
|
28
|
+
can save, diff, and share.
|
|
29
|
+
- **Four clean layers.** The tracing and analysis engines have no dependency on
|
|
30
|
+
the UI, so you can use PyInsider entirely from Python if you prefer.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Requires **Python 3.10+**. PyInsider has **no runtime dependencies**.
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
pip install pyinsider
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or from a clone, for development:
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
git clone https://github.com/Yashjindal11/pyinsider.git
|
|
46
|
+
cd pyinsider
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
Record a script and open the result:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
pyinsider run examples/simple.py
|
|
56
|
+
pyinsider open traces/ # opens the most recent trace in your browser
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Capture arguments and return values too:
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
pyinsider run examples/complex_values.py --detail
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Forward arguments to your program (everything after `--` goes to the script):
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
pyinsider run app.py -- --verbose input.csv
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Prefer to stay in the terminal? Skip the browser:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
pyinsider open traces/run-2026-08-29-001.tl --summary # top-level summary
|
|
75
|
+
pyinsider open traces/run-2026-08-29-001.tl --tree # printed call tree
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Using it from Python
|
|
79
|
+
|
|
80
|
+
The recorder is a plain context manager. The UI is optional.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import pyinsider
|
|
84
|
+
|
|
85
|
+
with pyinsider.trace() as run:
|
|
86
|
+
result = do_some_work()
|
|
87
|
+
|
|
88
|
+
trace = run.result
|
|
89
|
+
run.save("work.tl")
|
|
90
|
+
|
|
91
|
+
# Analyse without any UI:
|
|
92
|
+
for stat in pyinsider.hot_paths(trace):
|
|
93
|
+
print(stat.key, stat.inclusive_ms)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`pyinsider.checkpoint("loaded config")` drops a labelled marker into the active
|
|
97
|
+
trace so you can find a moment on the timeline later.
|
|
98
|
+
|
|
99
|
+
## The CLI
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
pyinsider run <script.py | -m module> [--detail] [-o FILE] [-- args...]
|
|
103
|
+
pyinsider open <file.tl | dir> [--summary | --tree | --no-browser]
|
|
104
|
+
pyinsider compare <before.tl> <after.tl>
|
|
105
|
+
pyinsider --version
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `run` preserves your program's exit code and forwards its arguments, so it is
|
|
109
|
+
safe to drop in front of an existing command.
|
|
110
|
+
- `open` accepts either a file or a directory (it picks the newest `.tl`).
|
|
111
|
+
- `compare` diffs two runs and highlights functions that got slower, faster,
|
|
112
|
+
appeared, or disappeared.
|
|
113
|
+
|
|
114
|
+
Colour output honours `NO_COLOR`, `TERM=dumb`, and non-TTY pipes.
|
|
115
|
+
|
|
116
|
+
## Trace files (`.tl`)
|
|
117
|
+
|
|
118
|
+
A `.tl` file is gzip-compressed JSON with an explicit schema version. It
|
|
119
|
+
contains the full call tree, timings (nanoseconds relative to run start),
|
|
120
|
+
captured values, exception information, and run metadata (Python version,
|
|
121
|
+
platform, detail level). There is **no pickle** and no code in a trace file —
|
|
122
|
+
it is safe to open and inspect by hand.
|
|
123
|
+
|
|
124
|
+
Trace files are forward-compatible within a major schema version and are
|
|
125
|
+
rejected if the major version is newer than the reader understands.
|
|
126
|
+
|
|
127
|
+
## The viewer
|
|
128
|
+
|
|
129
|
+
`pyinsider open` starts a tiny local web app (stdlib only, `127.0.0.1`) that
|
|
130
|
+
shows:
|
|
131
|
+
|
|
132
|
+
- an interactive, zoomable **timeline** of the run,
|
|
133
|
+
- a virtualised **call tree** with inclusive/self timings,
|
|
134
|
+
- a **source view** pointing at where each call was defined,
|
|
135
|
+
- a **details** panel with arguments and return values,
|
|
136
|
+
- an **exceptions** view with the call path that led to each failure,
|
|
137
|
+
- **hot paths** and an aggregated **flame graph**,
|
|
138
|
+
- filtering by module, name, duration, and project-vs-library code.
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
PyInsider is built in four layers, each depending only on the ones below it:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
cli / api / viewer <- presentation (never imported by the engine)
|
|
146
|
+
|
|
|
147
|
+
trace model + analysis <- Trace, function_stats, hot_paths, compare
|
|
148
|
+
|
|
|
149
|
+
trace events <- typed, serialisable ExecutionEvent records
|
|
150
|
+
|
|
|
151
|
+
tracer / collector <- sys.setprofile instrumentation
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The tracer and analysis code never import the CLI or viewer, so the engine is
|
|
155
|
+
usable as a library and the UI can be replaced without touching the core.
|
|
156
|
+
|
|
157
|
+
## Limitations
|
|
158
|
+
|
|
159
|
+
PyInsider is deliberately honest about what it can and cannot see. Highlights:
|
|
160
|
+
|
|
161
|
+
- **Caught exceptions** are attributed by walking the exception's traceback, so
|
|
162
|
+
only exceptions that propagate are marked; an exception swallowed by a
|
|
163
|
+
`try/except` is not attributed to the function that raised it.
|
|
164
|
+
- **C-level functions** (built-ins, C extensions) are not recorded as calls.
|
|
165
|
+
- Timings include tracing overhead; treat them as *relative*, not absolute.
|
|
166
|
+
- Async and multi-threaded code is recorded per-thread/per-task but concurrency
|
|
167
|
+
interleaving is simplified.
|
|
168
|
+
- Subprocesses are invisible to the tracer.
|
|
169
|
+
|
|
170
|
+
See [`docs/limitations.md`](docs/limitations.md) for the full, detailed list —
|
|
171
|
+
it is required reading before you trust a number.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```console
|
|
176
|
+
pip install -e ".[dev]"
|
|
177
|
+
pytest # run the test suite
|
|
178
|
+
ruff check pyinsider # lint
|
|
179
|
+
mypy pyinsider # type-check
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the workflow and project layout.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
[MIT](LICENSE). PyInsider is free and open source.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Limitations & honesty
|
|
2
|
+
|
|
3
|
+
PyInsider records a great deal of what a Python program does, but it cannot see
|
|
4
|
+
everything, and some of what it records is an approximation. This document
|
|
5
|
+
states, plainly, where the boundaries are. Read it before you trust a number.
|
|
6
|
+
|
|
7
|
+
The guiding principle: **it is better to record less and be correct than to
|
|
8
|
+
guess and be wrong.** Where PyInsider cannot know something reliably, it does
|
|
9
|
+
not pretend to.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Instrumentation mechanism
|
|
14
|
+
|
|
15
|
+
PyInsider records calls using CPython's `sys.setprofile` hook (and
|
|
16
|
+
`threading.setprofile` for other threads). This has direct consequences.
|
|
17
|
+
|
|
18
|
+
### C-level functions are not recorded as call events
|
|
19
|
+
|
|
20
|
+
`sys.setprofile` reports `c_call` / `c_return` events for built-ins and C
|
|
21
|
+
extension functions, but PyInsider does **not** record these as tree nodes. A
|
|
22
|
+
call into `len()`, `numpy.dot()`, `json.loads()`, or any C function appears as
|
|
23
|
+
time spent *inside its Python caller*, not as a separate node. This keeps the
|
|
24
|
+
tree meaningful (you see your code) but means:
|
|
25
|
+
|
|
26
|
+
- time spent in C is attributed to the nearest Python frame, and
|
|
27
|
+
- you cannot expand a C function to see what it did (there is nothing to see —
|
|
28
|
+
the interpreter does not expose C-internal frames).
|
|
29
|
+
|
|
30
|
+
### Only Python-level frames exist
|
|
31
|
+
|
|
32
|
+
Generator, coroutine, and comprehension frames are Python frames and are
|
|
33
|
+
recorded. Anything implemented in C (including much of the standard library's
|
|
34
|
+
hot paths) is not.
|
|
35
|
+
|
|
36
|
+
### Profile hooks are per-interpreter and can be displaced
|
|
37
|
+
|
|
38
|
+
If your program (or another library) installs its own `sys.setprofile` handler
|
|
39
|
+
while PyInsider is active, recording for the affected scope may stop or be
|
|
40
|
+
disturbed. PyInsider restores the previous hook on exit but does not fight other
|
|
41
|
+
tools for the slot.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Timing
|
|
46
|
+
|
|
47
|
+
### Timings include tracing overhead
|
|
48
|
+
|
|
49
|
+
Every recorded call runs PyInsider's hook on entry and exit. That work is
|
|
50
|
+
included in the elapsed time of the surrounding frames. As a result:
|
|
51
|
+
|
|
52
|
+
- **Absolute durations are inflated** relative to an untraced run, especially
|
|
53
|
+
for programs that make many small, fast calls.
|
|
54
|
+
- **Treat timings as relative,** not as ground truth. "Function A took twice as
|
|
55
|
+
long as function B in this trace" is trustworthy; "function A takes exactly
|
|
56
|
+
1.3 ms in production" is not.
|
|
57
|
+
|
|
58
|
+
The overhead is roughly proportional to the number of calls, not to wall-clock
|
|
59
|
+
time, so call-heavy code is affected most. Detail level `--detail` (value
|
|
60
|
+
capture) adds further overhead.
|
|
61
|
+
|
|
62
|
+
### Self time is inclusive-minus-children
|
|
63
|
+
|
|
64
|
+
"Self time" is computed as a frame's inclusive time minus the inclusive time of
|
|
65
|
+
its recorded children. Because C calls are not recorded as children, time spent
|
|
66
|
+
in C is counted as *self* time of the calling Python frame. This is usually what
|
|
67
|
+
you want, but it means self time is not "time spent on this exact line."
|
|
68
|
+
|
|
69
|
+
### Line-level attribution
|
|
70
|
+
|
|
71
|
+
PyInsider records where each function was *defined* (`co_firstlineno`) and the
|
|
72
|
+
call structure, not a per-line execution profile. It does not claim to tell you
|
|
73
|
+
which line inside a function was slow. If a source location is shown, it points
|
|
74
|
+
at the function, not the hot statement.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Exceptions
|
|
79
|
+
|
|
80
|
+
### Only propagating exceptions are attributed
|
|
81
|
+
|
|
82
|
+
`sys.setprofile` cannot reliably tell you, at return time, whether a frame is
|
|
83
|
+
unwinding because of an exception — `sys.exc_info()` returns `None` during the
|
|
84
|
+
normal return path of the profile hook (verified empirically on CPython 3.10–
|
|
85
|
+
3.11). PyInsider therefore does **not** try to detect exceptions on every
|
|
86
|
+
return.
|
|
87
|
+
|
|
88
|
+
Instead, when an exception propagates out of a traced block, PyInsider walks the
|
|
89
|
+
exception's authoritative traceback and matches each traceback frame back to a
|
|
90
|
+
recorded call. This gives an accurate failing call path — but only for
|
|
91
|
+
exceptions that actually escape.
|
|
92
|
+
|
|
93
|
+
**Consequence:** an exception that is raised and then caught inside a
|
|
94
|
+
`try/except` never reaches the traceback PyInsider inspects, so the function
|
|
95
|
+
that raised it is **not** marked as failing. This is a deliberate correctness
|
|
96
|
+
choice: PyInsider will not guess.
|
|
97
|
+
|
|
98
|
+
### Frame matching and recursion
|
|
99
|
+
|
|
100
|
+
When matching traceback frames to recorded events, PyInsider prefers a frame
|
|
101
|
+
that is a child of the previously matched frame. This disambiguates recursion
|
|
102
|
+
correctly in the common case, but pathological recursive/reentrant patterns
|
|
103
|
+
could in principle be matched to the wrong recorded event.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Concurrency
|
|
108
|
+
|
|
109
|
+
### Threads
|
|
110
|
+
|
|
111
|
+
Threads started while tracing is active are recorded via
|
|
112
|
+
`threading.setprofile`, with a separate call stack per thread. Each event
|
|
113
|
+
carries its thread id and name. However:
|
|
114
|
+
|
|
115
|
+
- Threads that were **already running** before tracing started may not be
|
|
116
|
+
instrumented until they make their next call through the hook.
|
|
117
|
+
- The timeline flattens concurrent activity onto a shared time axis; it shows
|
|
118
|
+
*when* things happened per thread, but does not model lock contention or true
|
|
119
|
+
parallel overlap semantics.
|
|
120
|
+
|
|
121
|
+
### Async / coroutines
|
|
122
|
+
|
|
123
|
+
Coroutine frames are recorded and tagged with a synthetic task id derived from
|
|
124
|
+
the frame identity. This lets you see coroutine calls, but PyInsider does not
|
|
125
|
+
reconstruct the full `await` scheduling graph of an event loop. Suspended-and-
|
|
126
|
+
resumed coroutines are approximated, not perfectly serialized.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Process boundaries
|
|
131
|
+
|
|
132
|
+
### Subprocesses are invisible
|
|
133
|
+
|
|
134
|
+
PyInsider traces the interpreter it runs in. Code executed in a **subprocess**
|
|
135
|
+
(via `subprocess`, `multiprocessing` spawn, `os.exec*`, etc.) runs in a
|
|
136
|
+
different interpreter and is not recorded. You would need to run PyInsider inside
|
|
137
|
+
that child process separately.
|
|
138
|
+
|
|
139
|
+
### `os._exit` and hard crashes
|
|
140
|
+
|
|
141
|
+
If the program calls `os._exit()` or is terminated by a signal / segfault, the
|
|
142
|
+
trace is not finalised and may be incomplete or unsaved. A normal `sys.exit()`
|
|
143
|
+
(`SystemExit`) is handled cleanly and the exit code is preserved.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Value capture
|
|
148
|
+
|
|
149
|
+
When value capture is enabled (`--detail`), PyInsider captures a **bounded,
|
|
150
|
+
best-effort representation** of arguments and return values:
|
|
151
|
+
|
|
152
|
+
- It never retains references to your objects; it snapshots a string/shape
|
|
153
|
+
summary immediately.
|
|
154
|
+
- It never consumes iterators or generators to inspect them.
|
|
155
|
+
- Every `repr()` call is guarded; an object whose `__repr__` raises is recorded
|
|
156
|
+
as an error placeholder rather than crashing the trace.
|
|
157
|
+
- Large containers and strings are truncated; deep/cyclic structures are cut off
|
|
158
|
+
with a recursion marker.
|
|
159
|
+
|
|
160
|
+
Because capture is bounded and best-effort, a captured value is a **faithful
|
|
161
|
+
summary, not a guaranteed round-trippable copy** of the original object.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Bounds and dropping
|
|
166
|
+
|
|
167
|
+
Recording is capped by `max_events` (see `TracerConfig`). If a run exceeds the
|
|
168
|
+
cap, further events are dropped and the count of dropped events is recorded in
|
|
169
|
+
the trace metadata. A trace that hit the cap is truncated, not corrupt — but it
|
|
170
|
+
is not the whole story.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Summary
|
|
175
|
+
|
|
176
|
+
PyInsider is an honest recorder of Python-level execution. It is excellent for
|
|
177
|
+
understanding *what your code did and in what order*, for finding *which of your
|
|
178
|
+
functions dominated a run*, and for seeing *the call path to a failure*. It is
|
|
179
|
+
**not** a substitute for a sampling profiler when you need low-overhead absolute
|
|
180
|
+
timings, nor for a line profiler when you need per-statement cost, nor for a
|
|
181
|
+
debugger when you need to inspect live state.
|