dsabench 0.1.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 (51) hide show
  1. dsabench-0.1.0/.gitignore +33 -0
  2. dsabench-0.1.0/CHANGELOG.md +44 -0
  3. dsabench-0.1.0/LICENSE +21 -0
  4. dsabench-0.1.0/PKG-INFO +337 -0
  5. dsabench-0.1.0/README.md +298 -0
  6. dsabench-0.1.0/examples/01_basic.py +17 -0
  7. dsabench-0.1.0/examples/02_decorator.py +25 -0
  8. dsabench-0.1.0/examples/03_auto.py +32 -0
  9. dsabench-0.1.0/examples/04_compare.py +44 -0
  10. dsabench-0.1.0/examples/05_complexity.py +33 -0
  11. dsabench-0.1.0/examples/06_export_and_graphs.py +37 -0
  12. dsabench-0.1.0/examples/07_async_generators_threads.py +45 -0
  13. dsabench-0.1.0/examples/demo.py +31 -0
  14. dsabench-0.1.0/pyproject.toml +93 -0
  15. dsabench-0.1.0/src/bench/__init__.py +86 -0
  16. dsabench-0.1.0/src/bench/auto.py +309 -0
  17. dsabench-0.1.0/src/bench/benchmark.py +423 -0
  18. dsabench-0.1.0/src/bench/compare.py +118 -0
  19. dsabench-0.1.0/src/bench/complexity.py +125 -0
  20. dsabench-0.1.0/src/bench/config.py +153 -0
  21. dsabench-0.1.0/src/bench/cpu.py +50 -0
  22. dsabench-0.1.0/src/bench/decorator.py +138 -0
  23. dsabench-0.1.0/src/bench/exceptions.py +30 -0
  24. dsabench-0.1.0/src/bench/export.py +224 -0
  25. dsabench-0.1.0/src/bench/graph.py +147 -0
  26. dsabench-0.1.0/src/bench/memory.py +113 -0
  27. dsabench-0.1.0/src/bench/profiler.py +155 -0
  28. dsabench-0.1.0/src/bench/py.typed +0 -0
  29. dsabench-0.1.0/src/bench/report.py +239 -0
  30. dsabench-0.1.0/src/bench/stats.py +116 -0
  31. dsabench-0.1.0/src/bench/timer.py +51 -0
  32. dsabench-0.1.0/src/bench/types.py +401 -0
  33. dsabench-0.1.0/src/bench/utils.py +172 -0
  34. dsabench-0.1.0/tests/conftest.py +17 -0
  35. dsabench-0.1.0/tests/test_async_gen.py +100 -0
  36. dsabench-0.1.0/tests/test_auto.py +123 -0
  37. dsabench-0.1.0/tests/test_bench.py +176 -0
  38. dsabench-0.1.0/tests/test_compare.py +100 -0
  39. dsabench-0.1.0/tests/test_complexity.py +82 -0
  40. dsabench-0.1.0/tests/test_config.py +95 -0
  41. dsabench-0.1.0/tests/test_cpu.py +34 -0
  42. dsabench-0.1.0/tests/test_decorator.py +119 -0
  43. dsabench-0.1.0/tests/test_export.py +107 -0
  44. dsabench-0.1.0/tests/test_graph.py +54 -0
  45. dsabench-0.1.0/tests/test_integration.py +58 -0
  46. dsabench-0.1.0/tests/test_memory.py +64 -0
  47. dsabench-0.1.0/tests/test_profiler.py +82 -0
  48. dsabench-0.1.0/tests/test_stats.py +68 -0
  49. dsabench-0.1.0/tests/test_timer.py +44 -0
  50. dsabench-0.1.0/tests/test_types.py +96 -0
  51. dsabench-0.1.0/tests/test_utils.py +100 -0
@@ -0,0 +1,33 @@
1
+ # Byte-compiled / caches
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+
8
+ # Packaging
9
+ build/
10
+ dist/
11
+ *.egg-info/
12
+ .eggs/
13
+
14
+ # Environments
15
+ .venv/
16
+ venv/
17
+ env/
18
+
19
+ # Coverage
20
+ .coverage
21
+ .coverage.*
22
+ coverage.xml
23
+ htmlcov/
24
+
25
+ # Editors / OS
26
+ .vscode/
27
+ .idea/
28
+ .DS_Store
29
+
30
+ # Benchmark exports & plots (local runs)
31
+ *.bench.json
32
+ *.bench.csv
33
+ *.bench.md
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
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/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-08
11
+
12
+ ### Added
13
+ - `bench(func, *args, **kwargs)` — one-line benchmarking that prints a rich
14
+ report and returns the function's original return value.
15
+ - `@benchmark` decorator (bare and parameterised forms) with `last_result`
16
+ and `original` attributes; a shared re-entrancy guard keeps recursive and
17
+ nested decorated calls un-instrumented, so exactly one report is produced
18
+ per outermost call.
19
+ - `auto()` / `stop_auto()` — automatic benchmarking of every user-defined
20
+ function call, with live per-call lines, a ranked summary table, and
21
+ fnmatch `include` / `exclude` filters.
22
+ - Metrics: wall time (fastest / slowest / average / median / std dev /
23
+ 95th percentile), CPU time and CPU %, peak / current / delta memory via
24
+ `tracemalloc`, process RSS, function calls, recursive calls, max recursion
25
+ depth, and GC collections.
26
+ - Modes: `fast` (1 run), `default` (10 runs + 1 warmup),
27
+ `accurate` (100 runs + 5 warmups) — plus explicit `repeat=` / `warmup=`.
28
+ - `compare()` — ranked head-to-head table with relative slowdowns and
29
+ output-equality checking.
30
+ - `estimate_complexity()` — empirical Big-O fitting over
31
+ O(1) … O(n³) with R² scores.
32
+ - Exports: JSON, CSV, and Markdown via `export_result()` /
33
+ `configure(export=...)`.
34
+ - Optional matplotlib graphs: `plot_runtime()`, `plot_memory()`,
35
+ `plot_comparison()` (install extra: `dsabench[viz]`).
36
+ - Support for coroutine functions (`bench_async` inside running loops),
37
+ generator functions (fresh generator returned to the caller), and
38
+ threaded code (CPU % above 100 is reported faithfully).
39
+ - Fully typed (`py.typed`), Google-style docstrings, 198-test suite,
40
+ GitHub Actions CI (ruff + black + pytest on Python 3.10–3.13) and
41
+ trusted-publishing release workflow.
42
+
43
+ [Unreleased]: https://github.com/priyadip/dsabench/compare/v0.1.0...HEAD
44
+ [0.1.0]: https://github.com/priyadip/dsabench/releases/tag/v0.1.0
dsabench-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Priyadip
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,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: dsabench
3
+ Version: 0.1.0
4
+ Summary: Beautiful one-line benchmarking for DSA, competitive programming, and interview prep: time, memory, CPU, calls, recursion, comparisons, and empirical complexity.
5
+ Project-URL: Homepage, https://github.com/priyadip/dsabench
6
+ Project-URL: Repository, https://github.com/priyadip/dsabench
7
+ Project-URL: Issues, https://github.com/priyadip/dsabench/issues
8
+ Project-URL: Changelog, https://github.com/priyadip/dsabench/blob/main/CHANGELOG.md
9
+ Author: Priyadip
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: algorithms,benchmark,benchmarking,competitive-programming,complexity,dsa,memory,performance,profiling,timing
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Topic :: System :: Benchmark
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: rich>=13.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: black>=24.0; extra == 'dev'
30
+ Requires-Dist: build>=1.2; extra == 'dev'
31
+ Requires-Dist: matplotlib>=3.7; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.5; extra == 'dev'
35
+ Requires-Dist: twine>=5.0; extra == 'dev'
36
+ Provides-Extra: viz
37
+ Requires-Dist: matplotlib>=3.7; extra == 'viz'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # ⚡ DsaBench
41
+
42
+ **One-line benchmarking for DSA, competitive programming, and interview prep.**
43
+
44
+ [![CI](https://github.com/priyadip/dsabench/actions/workflows/ci.yml/badge.svg)](https://github.com/priyadip/dsabench/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/dsabench.svg)](https://pypi.org/project/dsabench/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/dsabench.svg)](https://pypi.org/project/dsabench/)
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
48
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
49
+ [![Typed](https://img.shields.io/badge/typing-py.typed-blue.svg)](https://peps.python.org/pep-0561/)
50
+
51
+ You wrote two solutions to a problem. Which one is faster? How much memory does
52
+ the recursive one eat? How deep does it recurse? Is your "optimised" version
53
+ actually O(n log n)?
54
+
55
+ `dsabench` answers all of that with **one line** — no boilerplate, no
56
+ `timeit` incantations, no manual `tracemalloc` bookkeeping:
57
+
58
+ ```python
59
+ from bench import bench
60
+
61
+ answer = bench(solve, arr, target) # runs it, prints the report below,
62
+ print(answer) # ...and still hands back the real answer
63
+ ```
64
+
65
+ ```text
66
+ ╭─ ⚡ Benchmark Report ──────────────────────────────────╮
67
+ │ │
68
+ │ Function solve │
69
+ │ Arguments ([3, 1, 2], 4) │
70
+ │ Mode default · 10 runs (+1 warmup) │
71
+ │ Return [1, 3] │
72
+ │ Time │
73
+ │ Fastest 19.293 µs │
74
+ │ Average 22.169 µs │
75
+ │ Median 19.457 µs │
76
+ │ Slowest 46.048 µs │
77
+ │ Std Dev 8.393 µs │
78
+ │ 95th pct 34.303 µs │
79
+ │ Memory │
80
+ │ Peak 2.66 KB │
81
+ │ Current 448 B │
82
+ │ Delta 448 B │
83
+ │ Process RSS 24.95 MB │
84
+ │ CPU │
85
+ │ CPU time 22.680 µs │
86
+ │ CPU % 102.3% │
87
+ │ Calls │
88
+ │ Function calls 465 │
89
+ │ Recursive calls 464 │
90
+ │ Max recursion depth 12 │
91
+ │ GC collections 0 │
92
+ │ │
93
+ ╰────────────────────────────────── 2026-07-08T16:53:33 ─╯
94
+ ```
95
+
96
+ ## Install
97
+
98
+ ```bash
99
+ pip install dsabench # core (only dependency: rich)
100
+ pip install "dsabench[viz]" # + matplotlib graphs
101
+ ```
102
+
103
+ Python 3.10+. Fully typed (`py.typed`), MIT licensed.
104
+
105
+ ## Three ways to use it
106
+
107
+ ### 1 · Function call — `bench()`
108
+
109
+ ```python
110
+ from bench import bench
111
+
112
+ def fib(n):
113
+ return n if n < 2 else fib(n - 1) + fib(n - 2)
114
+
115
+ value = bench(fib, 20) # full report, returns 6765
116
+ value = bench(fib, 20, mode="accurate") # 100 runs + 5 warmups
117
+ value = bench(fib, 20, repeat=50, warmup=2) # explicit control
118
+ value = bench(fib, 20, quiet=True) # measure silently
119
+ ```
120
+
121
+ `bench()` is transparent: whatever the function returns, you get back —
122
+ including for generator functions (you receive a *fresh* generator) and
123
+ coroutine functions (awaited for you when no event loop is running).
124
+
125
+ ### 2 · Decorator — `@benchmark`
126
+
127
+ ```python
128
+ from bench import benchmark
129
+
130
+ @benchmark # bare form
131
+ def solve(nums):
132
+ return sorted(nums)
133
+
134
+ @benchmark(mode="accurate") # parameterised form
135
+ def solve_fast(nums):
136
+ return sorted(nums)
137
+
138
+ solve([3, 1, 2]) # report prints on every call
139
+ solve.last_result.average_ms # latest BenchmarkResult, programmatically
140
+ solve.original([3, 1, 2]) # raw, un-instrumented function
141
+ ```
142
+
143
+ Recursive and nested decorated functions are handled correctly: inner calls
144
+ run the **original** function, so you get exactly one report per outermost
145
+ call and true recursion statistics (a `@benchmark`-ed `fib(10)` reports 177
146
+ calls at depth 10, not a storm of nested reports).
147
+
148
+ ### 3 · Automatic — `auto()`
149
+
150
+ ```python
151
+ from bench import auto, stop_auto
152
+
153
+ auto() # from here on, every user-defined function you call
154
+ # is timed and printed live — stdlib & site-packages
155
+ # are ignored automatically
156
+
157
+ run_my_pipeline()
158
+
159
+ stop_auto() # prints a ranked summary table (calls, total, avg, min, max)
160
+ ```
161
+
162
+ Filter what gets captured with fnmatch patterns:
163
+ `auto(include=["solve*"])` or `auto(exclude=["helper_*"])`.
164
+
165
+ ## Compare solutions head-to-head
166
+
167
+ ```python
168
+ from bench import compare
169
+
170
+ result = compare(
171
+ ("Memoization", fib_memo),
172
+ ("Tabulation", fib_tab),
173
+ ("Matrix power", fib_matrix),
174
+ args=(30,),
175
+ )
176
+ result.winner.name # "Matrix power"
177
+ ```
178
+
179
+ ```text
180
+ ⚡ Comparison — args (30)
181
+ ╭──────┬──────────────┬──────────┬──────────┬──────────┬──────────┬──────────────╮
182
+ │ Rank │ Name │ Average │ Fastest │ Std Dev │ Peak Mem │ Relative │
183
+ ├──────┼──────────────┼──────────┼──────────┼──────────┼──────────┼──────────────┤
184
+ │ 1 │ Matrix power │ 11.9 µs │ 10.8 µs │ 1.1 µs │ 1.2 KB │ baseline │
185
+ │ 2 │ Memoization │ 38.4 µs │ 35.0 µs │ 2.9 µs │ 9.8 KB │ 3.22× slower │
186
+ │ 3 │ Tabulation │ 51.7 µs │ 48.2 µs │ 3.4 µs │ 2.1 KB │ 4.34× slower │
187
+ ╰──────┴──────────────┴──────────┴──────────┴──────────┴──────────┴──────────────╯
188
+ ✓ all outputs match
189
+ ```
190
+
191
+ `compare` also verifies that all candidates return **equal outputs** — the
192
+ fastest wrong answer is still wrong.
193
+
194
+ ## Estimate Big-O empirically
195
+
196
+ ```python
197
+ from bench import estimate_complexity
198
+
199
+ estimate_complexity(my_sort, sizes=[1_000, 2_000, 4_000, 8_000, 16_000],
200
+ args_for=lambda n: (random_list(n),))
201
+ ```
202
+
203
+ ```text
204
+ Estimated complexity: O(n log n) (R² = 0.998)
205
+ ```
206
+
207
+ Fits your measured times against O(1), O(log n), O(n), O(n log n), O(n²), and
208
+ O(n³) by least squares and ranks them by R². Treat it as a sanity check, not a
209
+ proof — constant factors and caches are real.
210
+
211
+ ## Export and plot
212
+
213
+ ```python
214
+ from bench import run, export_result, plot_runtime
215
+
216
+ result = run(fib, args=(25,)) # like bench(), but returns the
217
+ # BenchmarkResult and never prints
218
+ export_result(result, "fib.json") # .json / .csv / .md by extension
219
+ plot_runtime(result, path="fib.png") # needs dsabench[viz]
220
+ ```
221
+
222
+ Or export everything automatically:
223
+
224
+ ```python
225
+ import bench
226
+ bench.configure(export="json", export_dir="benchmarks/")
227
+ ```
228
+
229
+ ## Configuration
230
+
231
+ ```python
232
+ import bench
233
+ bench.configure(repeat=100, warmup=5, color=True, memory=True,
234
+ cpu=True, export="json")
235
+ ```
236
+
237
+ | Option | Default | Meaning |
238
+ | ------------------ | ----------- | ---------------------------------------------------- |
239
+ | `mode` | `"default"` | `fast` (1 run) / `default` (10+1) / `accurate` (100+5) |
240
+ | `repeat` | mode | Timed repetitions (overrides mode) |
241
+ | `warmup` | mode | Untimed warmup runs (overrides mode) |
242
+ | `color` | `True` | Colored Rich output |
243
+ | `memory` | `True` | Track memory via `tracemalloc` |
244
+ | `cpu` | `True` | Track CPU time / CPU % |
245
+ | `profile` | `True` | Track calls / recursion / GC |
246
+ | `quiet` | `False` | Suppress printed reports |
247
+ | `raise_exceptions` | `True` | Re-raise exceptions from benchmarked code |
248
+ | `export` | `None` | Auto-export every result: `"json"`/`"csv"`/`"md"` |
249
+ | `export_dir` | `"."` | Where auto-exports are written |
250
+ | `precision` | `3` | Decimal places in time formatting |
251
+ | `show_args` | `True` | Include the arguments line in reports |
252
+
253
+ `reset_config()` restores the defaults; `get_config()` returns the active
254
+ configuration.
255
+
256
+ ## How measurement works (and why you can trust it)
257
+
258
+ - **Warmups first** (never timed) — caches warm, imports settled.
259
+ - **Clean timed runs** using `time.perf_counter_ns` (wall) and
260
+ `time.process_time_ns` (CPU). Nothing else happens inside the timed region —
261
+ no memory tracking, no call counting, no allocation by dsabench itself.
262
+ - **One separate instrumented pass** afterwards collects memory
263
+ (`tracemalloc`), call counts, recursion depth, and GC activity, so the
264
+ instrumentation overhead never pollutes your timings.
265
+
266
+ That means the default mode calls your function **12 times**: 1 warmup +
267
+ 10 timed + 1 instrumented. In `fast` mode it's a single call with everything
268
+ combined (numbers are slightly noisier — that's the trade-off).
269
+
270
+ Other honest details:
271
+
272
+ - CPU % can exceed 100% for multi-threaded code — `process_time_ns` sums all
273
+ threads. That's signal, not a bug.
274
+ - Call counting uses `sys.setprofile`, counts Python-level frames (stdlib
275
+ included, cProfile-style), and excludes dsabench's own machinery. A
276
+ recursion like `fib(10)` reports exactly 177 calls, 176 recursive, depth 10.
277
+ - For coroutine functions, call/recursion tracking is disabled (profile hooks
278
+ and event loops don't mix reliably); timing and memory still work.
279
+ - `tracemalloc` sees Python allocations. C-extension allocations (NumPy
280
+ buffers, etc.) show up in **Process RSS**, not in peak/delta.
281
+
282
+ ## Jupyter & async
283
+
284
+ Inside a running event loop (Jupyter, async apps), `bench()` on a coroutine
285
+ can't call `asyncio.run` — use the awaitable form:
286
+
287
+ ```python
288
+ value = await bench_async(fetch_data, url)
289
+ ```
290
+
291
+ The `@benchmark` decorator works on `async def` functions out of the box.
292
+
293
+ ## FAQ
294
+
295
+ **Why is the import called `bench` when the package is `dsabench`?**
296
+ `pip install dsabench`, `from bench import bench` — short at the call site,
297
+ unique on PyPI. Heads-up: the Frappe/ERPNext ecosystem ships a CLI tool also
298
+ distributed as `bench` on PyPI. Don't install that package into the same
299
+ environment as dsabench: both would own the `bench` import name and clash.
300
+
301
+ **Do warmup runs affect my numbers?** No — they're executed and discarded
302
+ before the timing loop.
303
+
304
+ **Why do I see 12 executions in default mode?** 1 warmup + 10 timed +
305
+ 1 instrumented pass. Functions with side effects (appending to a list,
306
+ writing files) will apply them each time — benchmark pure functions, or pass
307
+ fresh inputs.
308
+
309
+ **Can it benchmark code that raises?** Yes. The exception is captured, shown
310
+ in the report, and re-raised by default (`configure(raise_exceptions=False)`
311
+ to suppress). `run()` never raises target exceptions; check `result.exception`.
312
+
313
+ **Is `auto()` production-safe?** It installs a `sys.setprofile` hook, which
314
+ slows everything down while active. It's a development and learning tool —
315
+ call `stop_auto()` when done.
316
+
317
+ **Timer resolution?** `perf_counter_ns` has nanosecond *units*; actual
318
+ resolution is platform-dependent (typically tens of ns). For micro-functions,
319
+ use `mode="accurate"` and read the median.
320
+
321
+ ## Roadmap
322
+
323
+ - [ ] `bench.timeline()` — flame-graph style per-call timeline for `auto()`
324
+ - [ ] Statistical significance testing in `compare()` (Mann–Whitney U)
325
+ - [ ] `--bench` pytest plugin for inline regression benchmarks
326
+ - [ ] HTML report export
327
+ - [ ] Per-line memory attribution (top allocating lines from tracemalloc)
328
+
329
+ ## Contributing
330
+
331
+ PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Run
332
+ `ruff check . && black --check . && pytest` before pushing; all three are
333
+ enforced by CI on Python 3.10–3.13.
334
+
335
+ ## License
336
+
337
+ [MIT](LICENSE) © 2026 Priyadip