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.
Files changed (58) hide show
  1. pyinsider-1.0.0/.gitignore +34 -0
  2. pyinsider-1.0.0/CHANGELOG.md +38 -0
  3. pyinsider-1.0.0/LICENSE +21 -0
  4. pyinsider-1.0.0/PKG-INFO +240 -0
  5. pyinsider-1.0.0/README.md +186 -0
  6. pyinsider-1.0.0/docs/limitations.md +181 -0
  7. pyinsider-1.0.0/examples/complex_values.py +51 -0
  8. pyinsider-1.0.0/examples/demo_app/README.md +44 -0
  9. pyinsider-1.0.0/examples/demo_app/__init__.py +12 -0
  10. pyinsider-1.0.0/examples/demo_app/main.py +68 -0
  11. pyinsider-1.0.0/examples/demo_app/orders.py +25 -0
  12. pyinsider-1.0.0/examples/demo_app/pricing.py +33 -0
  13. pyinsider-1.0.0/examples/demo_app/report.py +15 -0
  14. pyinsider-1.0.0/examples/demo_app/validation.py +26 -0
  15. pyinsider-1.0.0/examples/exception.py +26 -0
  16. pyinsider-1.0.0/examples/nested_calls.py +30 -0
  17. pyinsider-1.0.0/examples/recursion.py +24 -0
  18. pyinsider-1.0.0/examples/repeated_calls.py +26 -0
  19. pyinsider-1.0.0/examples/simple.py +36 -0
  20. pyinsider-1.0.0/examples/slow_function.py +37 -0
  21. pyinsider-1.0.0/pyinsider/__init__.py +42 -0
  22. pyinsider-1.0.0/pyinsider/__main__.py +7 -0
  23. pyinsider-1.0.0/pyinsider/cli/__init__.py +11 -0
  24. pyinsider-1.0.0/pyinsider/cli/compare.py +136 -0
  25. pyinsider-1.0.0/pyinsider/cli/format.py +133 -0
  26. pyinsider-1.0.0/pyinsider/cli/main.py +102 -0
  27. pyinsider-1.0.0/pyinsider/cli/open.py +212 -0
  28. pyinsider-1.0.0/pyinsider/cli/run.py +253 -0
  29. pyinsider-1.0.0/pyinsider/core/__init__.py +38 -0
  30. pyinsider-1.0.0/pyinsider/core/analysis.py +219 -0
  31. pyinsider-1.0.0/pyinsider/core/models.py +151 -0
  32. pyinsider-1.0.0/pyinsider/core/serialization.py +223 -0
  33. pyinsider-1.0.0/pyinsider/core/trace.py +109 -0
  34. pyinsider-1.0.0/pyinsider/core/values.py +204 -0
  35. pyinsider-1.0.0/pyinsider/tracer/__init__.py +23 -0
  36. pyinsider-1.0.0/pyinsider/tracer/config.py +97 -0
  37. pyinsider-1.0.0/pyinsider/tracer/engine.py +338 -0
  38. pyinsider-1.0.0/pyinsider/tracer/runner.py +177 -0
  39. pyinsider-1.0.0/pyinsider/tracer/session.py +111 -0
  40. pyinsider-1.0.0/pyinsider/viewer/__init__.py +13 -0
  41. pyinsider-1.0.0/pyinsider/viewer/payload.py +193 -0
  42. pyinsider-1.0.0/pyinsider/viewer/server.py +155 -0
  43. pyinsider-1.0.0/pyinsider/viewer/static/app.css +559 -0
  44. pyinsider-1.0.0/pyinsider/viewer/static/app.js +280 -0
  45. pyinsider-1.0.0/pyinsider/viewer/static/flame.js +251 -0
  46. pyinsider-1.0.0/pyinsider/viewer/static/index.html +176 -0
  47. pyinsider-1.0.0/pyinsider/viewer/static/panels.js +288 -0
  48. pyinsider-1.0.0/pyinsider/viewer/static/state.js +162 -0
  49. pyinsider-1.0.0/pyinsider/viewer/static/timeline.js +349 -0
  50. pyinsider-1.0.0/pyinsider/viewer/static/tree.js +190 -0
  51. pyinsider-1.0.0/pyproject.toml +113 -0
  52. pyinsider-1.0.0/tests/__init__.py +0 -0
  53. pyinsider-1.0.0/tests/conftest.py +32 -0
  54. pyinsider-1.0.0/tests/test_cli.py +147 -0
  55. pyinsider-1.0.0/tests/test_core.py +156 -0
  56. pyinsider-1.0.0/tests/test_tracer.py +243 -0
  57. pyinsider-1.0.0/tests/test_values.py +111 -0
  58. 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
@@ -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.
@@ -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.