ocalc 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.
ocalc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ocalc 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.
ocalc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocalc
3
+ Version: 0.1.0
4
+ Summary: Measure a Python function and comment its time and space complexity.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/Anuj-Attri/ocalc
7
+ Project-URL: Documentation, https://anuj-attri.github.io/ocalc/
8
+ Project-URL: Repository, https://github.com/Anuj-Attri/ocalc
9
+ Project-URL: Issues, https://github.com/Anuj-Attri/ocalc/issues
10
+ Keywords: complexity,benchmark,big-o,profiling
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy
18
+ Provides-Extra: mcp
19
+ Requires-Dist: mcp<3,>=2; extra == "mcp"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Provides-Extra: docs
23
+ Requires-Dist: sphinx<8,>=7.2; extra == "docs"
24
+ Requires-Dist: pytorch-sphinx-theme2==0.4.13; extra == "docs"
25
+ Dynamic: license-file
26
+
27
+ # ocalc
28
+
29
+ `ocalc` measures a Python function and writes a one-line estimate of its time and space complexity.
30
+
31
+ ```python
32
+ # ocalc: Time: O(n); Space: O(1); Worst: 0.42 s at n=100000; Median: 0.31 s; Confidence: high; Protocol: min_n=100,max_n=100000,measures=8,budget=5,repeats=5
33
+ def two_sum(nums, target):
34
+ ...
35
+ ```
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install ocalc
41
+ pip install "ocalc[mcp]"
42
+ ```
43
+
44
+ ## Use
45
+
46
+ ```bash
47
+ ocalc measure FILE -f two_sum --sample "[1, 2, 3], 9"
48
+ ocalc annotate FILE -f two_sum --sample "[1, 2, 3], 9"
49
+ ```
50
+
51
+ ![ocalc annotate writes the measured comment above the function](https://raw.githubusercontent.com/Anuj-Attri/ocalc/main/docs/images/annotate.png)
52
+
53
+ Try a snippet on the [demo](https://anuj-attri.github.io/ocalc/demo.html). The page reports the worst case in the file. The guide and API reference are at [anuj-attri.github.io/ocalc](https://anuj-attri.github.io/ocalc/).
54
+
55
+ ## Development
56
+
57
+ ```bash
58
+ pip install -e ".[dev,docs]"
59
+ pytest
60
+ sphinx-build -b html docs docs/_build/html
61
+ ```
ocalc-0.1.0/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # ocalc
2
+
3
+ `ocalc` measures a Python function and writes a one-line estimate of its time and space complexity.
4
+
5
+ ```python
6
+ # ocalc: Time: O(n); Space: O(1); Worst: 0.42 s at n=100000; Median: 0.31 s; Confidence: high; Protocol: min_n=100,max_n=100000,measures=8,budget=5,repeats=5
7
+ def two_sum(nums, target):
8
+ ...
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install ocalc
15
+ pip install "ocalc[mcp]"
16
+ ```
17
+
18
+ ## Use
19
+
20
+ ```bash
21
+ ocalc measure FILE -f two_sum --sample "[1, 2, 3], 9"
22
+ ocalc annotate FILE -f two_sum --sample "[1, 2, 3], 9"
23
+ ```
24
+
25
+ ![ocalc annotate writes the measured comment above the function](https://raw.githubusercontent.com/Anuj-Attri/ocalc/main/docs/images/annotate.png)
26
+
27
+ Try a snippet on the [demo](https://anuj-attri.github.io/ocalc/demo.html). The page reports the worst case in the file. The guide and API reference are at [anuj-attri.github.io/ocalc](https://anuj-attri.github.io/ocalc/).
28
+
29
+ ## Development
30
+
31
+ ```bash
32
+ pip install -e ".[dev,docs]"
33
+ pytest
34
+ sphinx-build -b html docs docs/_build/html
35
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ocalc"
7
+ version = "0.1.0"
8
+ description = "Measure a Python function and comment its time and space complexity."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = ["complexity", "benchmark", "big-o", "profiling"]
14
+ dependencies = ["numpy"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Testing",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/Anuj-Attri/ocalc"
23
+ Documentation = "https://anuj-attri.github.io/ocalc/"
24
+ Repository = "https://github.com/Anuj-Attri/ocalc"
25
+ Issues = "https://github.com/Anuj-Attri/ocalc/issues"
26
+
27
+ [project.optional-dependencies]
28
+ mcp = ["mcp>=2,<3"]
29
+ dev = ["pytest"]
30
+ docs = ["sphinx>=7.2,<8", "pytorch-sphinx-theme2==0.4.13"]
31
+
32
+ [project.scripts]
33
+ ocalc = "ocalc.cli:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ pythonpath = ["src"]
ocalc-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,216 @@
1
+ """Measure a Python function and estimate its time and space complexity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+
10
+ from ocalc.bench import BenchResult, Point, benchmark
11
+ from ocalc.comment import (
12
+ Protocol,
13
+ apply_comment,
14
+ existing_comment,
15
+ format_comment,
16
+ format_table,
17
+ )
18
+ from ocalc.fit import FitResult, fit_curve
19
+ from ocalc.generate import (
20
+ FunctionNotFound,
21
+ NeedSample,
22
+ callable_location,
23
+ load_function,
24
+ parse_sample,
25
+ plan_inputs,
26
+ )
27
+ from ocalc.static import FunctionEstimate, SourceAnalysis, analyze_source
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ "FunctionEstimate",
33
+ "FunctionNotFound",
34
+ "NeedSample",
35
+ "Report",
36
+ "SourceAnalysis",
37
+ "analyze_source",
38
+ "annotate_file",
39
+ "measure",
40
+ "parse_sample",
41
+ ]
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Report:
46
+ comment: str
47
+ points: list[Point]
48
+ time: FitResult
49
+ space: FitResult
50
+ worst_s: float
51
+ median_s: float
52
+ worst_n: int | None
53
+ protocol: Protocol
54
+
55
+
56
+ def measure(
57
+ func,
58
+ *sample_args,
59
+ budget: float | None = None,
60
+ min_n: int | None = None,
61
+ max_n: int | None = None,
62
+ measures: int | None = None,
63
+ repeats: int | None = None,
64
+ file: str | None = None,
65
+ qualname: str | None = None,
66
+ protocol: Protocol | None = None,
67
+ previous_s: float | None = None,
68
+ **sample_kwargs,
69
+ ) -> Report:
70
+ """Measure a function and return its complexity report.
71
+
72
+ Args:
73
+ func: The callable to measure.
74
+ sample_args: Example positional arguments. The first collection grows with ``n``.
75
+ budget: Seconds to spend measuring. The default is 5.
76
+ min_n: Smallest input size.
77
+ max_n: Largest input size.
78
+ measures: How many sizes to try.
79
+ repeats: Timed calls at each size.
80
+ file: Source file, when the callable should be re-imported for the timed run.
81
+ qualname: Qualname used to re-import ``file``.
82
+ protocol: Stored sizes and budget from an earlier comment.
83
+ previous_s: Worst time from that earlier comment.
84
+ sample_kwargs: Example keyword arguments.
85
+
86
+ Returns:
87
+ The comment, the size table, and the fitted time and space classes.
88
+ """
89
+ plan = plan_inputs(func, sample_args, sample_kwargs)
90
+ if protocol is not None and protocol.constant and plan.mode == "constant":
91
+ if budget is None:
92
+ budget = protocol.budget
93
+ if repeats is None:
94
+ repeats = protocol.repeats
95
+ elif protocol is not None and not protocol.constant and plan.mode != "constant":
96
+ if budget is None:
97
+ budget = protocol.budget
98
+ if min_n is None:
99
+ min_n = protocol.min_n
100
+ if max_n is None:
101
+ max_n = protocol.max_n
102
+ if measures is None:
103
+ measures = protocol.measures
104
+ if repeats is None:
105
+ repeats = protocol.repeats
106
+
107
+ located_file, located_name = callable_location(func)
108
+ file = file or located_file
109
+ qualname = qualname or located_name
110
+ result = benchmark(
111
+ func,
112
+ plan,
113
+ budget=5.0 if budget is None else budget,
114
+ min_n=min_n,
115
+ max_n=max_n,
116
+ measures=measures,
117
+ repeats=repeats,
118
+ file=file,
119
+ qualname=qualname,
120
+ )
121
+ time_fit, space_fit = _fits(result)
122
+ comment = format_comment(
123
+ time=time_fit,
124
+ space=space_fit,
125
+ worst_s=result.worst_s,
126
+ worst_n=result.worst_n,
127
+ median_s=result.median_s,
128
+ protocol=result.protocol,
129
+ previous_s=previous_s,
130
+ )
131
+ return Report(
132
+ comment=comment,
133
+ points=result.points,
134
+ time=time_fit,
135
+ space=space_fit,
136
+ worst_s=result.worst_s,
137
+ median_s=result.median_s,
138
+ worst_n=result.worst_n,
139
+ protocol=result.protocol,
140
+ )
141
+
142
+
143
+ def annotate_file(
144
+ path,
145
+ qualname: str,
146
+ *sample_args,
147
+ budget: float | None = None,
148
+ min_n: int | None = None,
149
+ max_n: int | None = None,
150
+ measures: int | None = None,
151
+ repeats: int | None = None,
152
+ **sample_kwargs,
153
+ ) -> Report:
154
+ """Benchmark a function in a file and write the comment above it.
155
+
156
+ A later call reuses the protocol stored in the existing comment and records
157
+ the previous worst time.
158
+
159
+ Args:
160
+ path: Python file that defines the function.
161
+ qualname: Function qualname, such as ``two_sum`` or ``Box.run``.
162
+ sample_args: Example positional arguments.
163
+ budget: Seconds to spend measuring.
164
+ min_n: Smallest input size.
165
+ max_n: Largest input size.
166
+ measures: How many sizes to try.
167
+ repeats: Timed calls at each size.
168
+ sample_kwargs: Example keyword arguments.
169
+
170
+ Returns:
171
+ The report that was written into the file.
172
+ """
173
+ file_path = Path(path)
174
+ source = file_path.read_text(encoding="utf-8")
175
+ previous = existing_comment(source, qualname)
176
+ protocol = previous.protocol if previous is not None else None
177
+ previous_s = previous.worst_s if previous is not None else None
178
+ func = load_function(file_path, qualname)
179
+ report = measure(
180
+ func,
181
+ *sample_args,
182
+ budget=budget,
183
+ min_n=min_n,
184
+ max_n=max_n,
185
+ measures=measures,
186
+ repeats=repeats,
187
+ file=str(file_path.resolve()),
188
+ qualname=qualname,
189
+ protocol=protocol,
190
+ previous_s=previous_s,
191
+ **sample_kwargs,
192
+ )
193
+ updated = apply_comment(source, qualname, report.comment)
194
+ with file_path.open("w", encoding="utf-8", newline="") as handle:
195
+ handle.write(updated)
196
+ return report
197
+
198
+
199
+ def _fits(result: BenchResult) -> tuple[FitResult, FitResult]:
200
+ if result.constant:
201
+ done = FitResult("1", "O(1)", "high")
202
+ return done, done
203
+ sizes = [point.n for point in result.points]
204
+ times = [point.time_s for point in result.points]
205
+ time_fit = fit_curve(sizes, times, exponential=result.exponential)
206
+ return time_fit, _space_fit(sizes, [point.memory_bytes for point in result.points])
207
+
208
+
209
+ def _space_fit(sizes, memory) -> FitResult:
210
+ used = np.asarray(memory, dtype=float)
211
+ if used.size == 0 or np.nanmax(used) <= 0:
212
+ return FitResult("1", "O(1)", "high")
213
+ positive = used[used > 0]
214
+ floor = float(positive.min()) if positive.size else 1.0
215
+ used = np.where(used > 0, used, floor)
216
+ return fit_curve(sizes, used)
@@ -0,0 +1,287 @@
1
+ """Time a function across a size grid and record peak Python allocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import math
8
+ import multiprocessing
9
+ import time
10
+ import tracemalloc
11
+ from dataclasses import dataclass
12
+ from statistics import median
13
+
14
+ from ocalc.comment import Protocol
15
+ from ocalc.generate import InputPlan, load_function
16
+
17
+ DEFAULT_BUDGET = 5.0
18
+ DEFAULT_MEASURES = 8
19
+ DEFAULT_MIN_N = 100
20
+ DEFAULT_MAX_N = 100_000
21
+ DEFAULT_MIN_N_INT = 8
22
+ _REPEATS = 5
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Point:
27
+ n: int | None
28
+ time_s: float
29
+ memory_bytes: float
30
+
31
+
32
+ @dataclass
33
+ class BenchResult:
34
+ points: list[Point]
35
+ protocol: Protocol
36
+ exponential: bool
37
+ constant: bool
38
+ timed_out: bool
39
+ worst_s: float
40
+ median_s: float
41
+ worst_n: int | None
42
+
43
+
44
+ def geometric_sizes(min_n: int, max_n: int, measures: int) -> list[int]:
45
+ """Evenly spaced sizes on a log scale, including both ends."""
46
+ if measures < 1:
47
+ raise ValueError("measures must be at least 1")
48
+ if min_n > max_n:
49
+ raise ValueError("min_n cannot be greater than max_n")
50
+ if measures == 1 or min_n == max_n:
51
+ return [int(max_n)]
52
+ ratio = (max_n / min_n) ** (1 / (measures - 1))
53
+ raw = [int(round(min_n * ratio**i)) for i in range(measures)]
54
+ raw[0] = int(min_n)
55
+ raw[-1] = int(max_n)
56
+ sizes: list[int] = []
57
+ for size in raw:
58
+ size = max(int(min_n), size)
59
+ if not sizes or size > sizes[-1]:
60
+ sizes.append(size)
61
+ if sizes[-1] != int(max_n):
62
+ sizes.append(int(max_n))
63
+ return sizes
64
+
65
+
66
+ def benchmark(
67
+ func,
68
+ plan: InputPlan,
69
+ *,
70
+ budget: float,
71
+ min_n: int | None,
72
+ max_n: int | None,
73
+ measures: int | None,
74
+ repeats: int | None,
75
+ file: str | None,
76
+ qualname: str | None,
77
+ ) -> BenchResult:
78
+ measures = DEFAULT_MEASURES if measures is None else measures
79
+ budget = DEFAULT_BUDGET if budget is None else budget
80
+ repeats = _REPEATS if repeats is None else max(1, int(repeats))
81
+ if plan.mode == "constant":
82
+ return _benchmark_constant(func, plan, budget, repeats, file, qualname)
83
+
84
+ if plan.mode == "int_n":
85
+ min_n = DEFAULT_MIN_N_INT if min_n is None else min_n
86
+ max_n = DEFAULT_MAX_N if max_n is None else max_n
87
+ else:
88
+ min_n = DEFAULT_MIN_N if min_n is None else min_n
89
+ max_n = DEFAULT_MAX_N if max_n is None else max_n
90
+ protocol = Protocol(int(min_n), int(max_n), int(measures), float(budget), repeats=repeats)
91
+ sizes = geometric_sizes(protocol.min_n, protocol.max_n, protocol.measures)
92
+ points: list[Point] = []
93
+ exponential = False
94
+ timed_out = False
95
+ started = time.perf_counter()
96
+
97
+ for size in sizes:
98
+ remaining = budget - (time.perf_counter() - started)
99
+ if remaining <= 0.05:
100
+ break
101
+ if len(points) >= 2 and _explosive(
102
+ points[-2].n, points[-2].time_s, points[-1].n, points[-1].time_s
103
+ ):
104
+ exponential = True
105
+ break
106
+ if points:
107
+ ratio = size / points[-1].n
108
+ predicted = points[-1].time_s * max(ratio, 1.0) ** 2
109
+ if predicted > remaining:
110
+ break
111
+ args, kwargs = plan.build(size)
112
+ status, times, peak = _measure_call(
113
+ func,
114
+ args,
115
+ kwargs,
116
+ file=file,
117
+ qualname=qualname,
118
+ repeats=repeats,
119
+ timeout=max(0.2, remaining),
120
+ )
121
+ if status == "timeout":
122
+ timed_out = True
123
+ if points and (
124
+ points[-1].n <= 40
125
+ and points[-1].time_s > 1e-4
126
+ or (
127
+ len(points) >= 2
128
+ and _explosive(points[-2].n, points[-2].time_s, points[-1].n, points[-1].time_s)
129
+ )
130
+ ):
131
+ exponential = True
132
+ break
133
+ if status == "error":
134
+ raise RuntimeError(times)
135
+ point = Point(size, float(median(times)), float(peak))
136
+ points.append(point)
137
+ if len(points) >= 2 and _explosive(
138
+ points[-2].n, points[-2].time_s, points[-1].n, points[-1].time_s
139
+ ):
140
+ exponential = True
141
+ break
142
+
143
+ if not points:
144
+ raise RuntimeError(
145
+ "No measurement finished within the budget. Raise --budget or lower --min-n."
146
+ )
147
+ slowest = max(points, key=lambda item: item.time_s)
148
+ return BenchResult(
149
+ points=points,
150
+ protocol=protocol,
151
+ exponential=exponential,
152
+ constant=False,
153
+ timed_out=timed_out,
154
+ worst_s=slowest.time_s,
155
+ median_s=float(median(item.time_s for item in points)),
156
+ worst_n=slowest.n,
157
+ )
158
+
159
+
160
+ def _benchmark_constant(func, plan, budget, repeats, file, qualname) -> BenchResult:
161
+ args, kwargs = plan.build(0)
162
+ status, times, peak = _measure_call(
163
+ func,
164
+ args,
165
+ kwargs,
166
+ file=file,
167
+ qualname=qualname,
168
+ repeats=repeats,
169
+ timeout=max(0.2, budget),
170
+ )
171
+ if status == "timeout":
172
+ raise RuntimeError(
173
+ "No measurement finished within the budget. Raise --budget or lower --min-n."
174
+ )
175
+ if status == "error":
176
+ raise RuntimeError(times)
177
+ protocol = Protocol(0, 0, 0, float(budget), constant=True, repeats=repeats)
178
+ point = Point(None, float(median(times)), float(peak))
179
+ return BenchResult(
180
+ points=[point],
181
+ protocol=protocol,
182
+ exponential=False,
183
+ constant=True,
184
+ timed_out=False,
185
+ worst_s=float(max(times)),
186
+ median_s=float(median(times)),
187
+ worst_n=None,
188
+ )
189
+
190
+
191
+ def _explosive(n0, t0, n1, t1) -> bool:
192
+ if t0 <= 0 or t1 <= 0 or n1 <= n0:
193
+ return False
194
+ n_ratio = n1 / n0
195
+ t_ratio = t1 / t0
196
+ if n_ratio < 1.5 and t_ratio > 2 and n1 <= 64 and t1 > 1e-4:
197
+ return True
198
+ if t1 < 1e-3 or n_ratio <= 1:
199
+ return False
200
+ exponent = math.log(t_ratio) / math.log(n_ratio)
201
+ return exponent > 5
202
+
203
+
204
+ def _call(func, args, kwargs):
205
+ result = func(*args, **kwargs)
206
+ if inspect.iscoroutine(result):
207
+ return asyncio.run(result)
208
+ return result
209
+
210
+
211
+ def _measure_call(func, args, kwargs, *, file, qualname, repeats, timeout):
212
+ if file and qualname:
213
+ return _run_process(file, qualname, args, kwargs, repeats, timeout)
214
+ times, peak = collect_sample(func, args, kwargs, repeats)
215
+ return "ok", times, peak
216
+
217
+
218
+ def collect_sample(func, args, kwargs, repeats: int = _REPEATS):
219
+ """Warm up, time several calls, then record one tracemalloc peak."""
220
+ _call(func, args, kwargs)
221
+ batch = 1
222
+
223
+ def once() -> float:
224
+ started = time.perf_counter()
225
+ for _ in range(batch):
226
+ _call(func, args, kwargs)
227
+ return (time.perf_counter() - started) / batch
228
+
229
+ first = once()
230
+ if first < 0.001:
231
+ batch = min(10_000, max(1, math.ceil(0.002 / max(first, 1e-9))))
232
+ first = once()
233
+ times = [first]
234
+ if first <= 0.2:
235
+ for _ in range(repeats - 1):
236
+ duration = once()
237
+ times.append(duration)
238
+ if duration > 0.2:
239
+ break
240
+ tracemalloc.start()
241
+ try:
242
+ _call(func, args, kwargs)
243
+ _current, peak = tracemalloc.get_traced_memory()
244
+ finally:
245
+ tracemalloc.stop()
246
+ return times, float(peak)
247
+
248
+
249
+ def _run_process(file, qualname, args, kwargs, repeats, timeout):
250
+ context = multiprocessing.get_context("spawn")
251
+ parent, child = context.Pipe(duplex=False)
252
+ process = context.Process(
253
+ target=_worker,
254
+ args=(child, str(file), qualname, args, kwargs, repeats),
255
+ daemon=True,
256
+ )
257
+ process.start()
258
+ child.close()
259
+ try:
260
+ process.join(timeout)
261
+ if process.is_alive():
262
+ process.terminate()
263
+ process.join()
264
+ return "timeout", None, None
265
+ if not parent.poll(0.2):
266
+ return "error", "The measurement process stopped without a result.", None
267
+ message = parent.recv()
268
+ finally:
269
+ parent.close()
270
+ if not message or message[0] != "ok":
271
+ detail = message[1] if message and len(message) > 1 else "measurement failed"
272
+ return "error", detail, None
273
+ return "ok", message[1], message[2]
274
+
275
+
276
+ def _worker(conn, file, qualname, args, kwargs, repeats):
277
+ try:
278
+ func = load_function(file, qualname)
279
+ times, peak = collect_sample(func, args, kwargs, repeats)
280
+ conn.send(("ok", times, peak))
281
+ except Exception as exc:
282
+ try:
283
+ conn.send(("err", f"{type(exc).__name__}: {exc}"))
284
+ except Exception:
285
+ pass
286
+ finally:
287
+ conn.close()