austin-python 1.7.1__py3-none-any.whl → 2.0.0__py3-none-any.whl

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.
austin/__init__.py CHANGED
@@ -20,310 +20,3 @@
20
20
  # GNU General Public License for more details.
21
21
  # You should have received a copy of the GNU General Public License
22
22
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
23
-
24
-
25
- import argparse
26
- import functools
27
- import os
28
- import os.path
29
- from abc import ABC
30
- from abc import abstractmethod
31
- from itertools import takewhile
32
- from pathlib import Path
33
- from typing import Any
34
- from typing import Callable
35
- from typing import Dict
36
- from typing import List
37
- from typing import Optional
38
- from typing import Tuple
39
-
40
- import psutil
41
-
42
- from austin.config import AustinConfiguration
43
-
44
-
45
- try:
46
- _cached_property = functools.cached_property # type: ignore[attr-defined]
47
- except AttributeError:
48
-
49
- def _cached_property(f: Callable[..., Any]) -> property: # type: ignore[no-redef]
50
- return property(functools.lru_cache(maxsize=1)(f))
51
-
52
-
53
- SemVer = Tuple[int, int, int]
54
-
55
-
56
- def _to_semver(version: Optional[str]) -> SemVer:
57
- if version is None:
58
- return (0, 0, 0)
59
-
60
- try:
61
- return ( # type: ignore[return-value]
62
- tuple(
63
- int(_)
64
- for _ in "".join(
65
- list(
66
- takewhile(
67
- lambda _: _.isdigit() or _ == ".", version.replace("?", "0")
68
- )
69
- )
70
- ).split(".")
71
- )
72
- + (0, 0, 0)
73
- )[:3]
74
- except ValueError:
75
- raise ValueError("Invalid semantic version") from None
76
-
77
-
78
- class AustinError(Exception):
79
- """Basic Austin Error."""
80
-
81
- pass
82
-
83
-
84
- class AustinTerminated(AustinError):
85
- """Austin termination exception.
86
-
87
- Thrown when Austin is terminated with a call to ``terminate``.
88
- """
89
-
90
- pass
91
-
92
-
93
- class BaseAustin(ABC):
94
- """Base Austin class.
95
-
96
- Defines the general API that abstract Austin as an external process.
97
- Subclasses should implement the :func:`start` method and either define the
98
- :func:`on_sample_received` method or pass it via the constructor.
99
- Additionally, the :func:`on_ready` and the :func:`on_terminate` methods can
100
- be overridden or passed via the constructor to catch the corresponding
101
- events. Austin is considered to be ready when the first sample is received;
102
- it is considered to have terminated when the process has terminated
103
- gracefully.
104
-
105
- If an error was encountered, the :class:`AustinError` exception is thrown.
106
- """
107
-
108
- BINARY = "austin" + (os.name == "nt" and ".exe" or "")
109
- BINARY_VERSION = (3, 0, 0)
110
-
111
- def __init__(
112
- self,
113
- sample_callback: Optional[Callable[[str], None]] = None,
114
- ready_callback: Optional[
115
- Callable[[psutil.Process, psutil.Process, str], None]
116
- ] = None,
117
- terminate_callback: Optional[Callable[[Dict[str, str]], None]] = None,
118
- ) -> None:
119
- """The ``BaseAustin`` constructor.
120
-
121
- The callbacks can be passed as arguments, if they are not
122
- defined/overridden in the definition of a sub-class.
123
- """
124
- self._proc: psutil.Process = None
125
- self._child_proc: psutil.Process = None
126
- self._cmd_line: Optional[str] = None
127
- self._running: bool = False
128
- self._meta: Dict[str, str] = {}
129
-
130
- try:
131
- self._sample_callback = sample_callback or self.on_sample_received # type: ignore[attr-defined]
132
- except AttributeError:
133
- raise AustinError("No sample callback given or implemented.") from None
134
-
135
- self._terminate_callback = terminate_callback or self.on_terminate
136
- self._ready_callback = ready_callback or self.on_ready
137
-
138
- self._version: Optional[str] = None
139
- self._python_version: Optional[str] = None
140
-
141
- def _get_process_info(
142
- self, args: argparse.Namespace, austin_pid: int
143
- ) -> Tuple[psutil.Process, psutil.Process, str]:
144
- try:
145
- self._proc = psutil.Process(austin_pid)
146
- except psutil.NoSuchProcess:
147
- raise AustinError("Cannot find Austin process.") from None
148
-
149
- if not args.pid: # Austin is forking
150
- try:
151
- self._child_proc = self._proc.children()[0]
152
- if self._child_proc is None:
153
- raise IndexError
154
- except IndexError:
155
- raise AustinError("Cannot find Austin child process.") from None
156
- else: # Austin is attaching
157
- try:
158
- self._child_proc = psutil.Process(args.pid)
159
- except psutil.NoSuchProcess:
160
- raise AustinError(
161
- f"Cannot attach to process with invalid PID {args.pid}."
162
- ) from None
163
-
164
- self._cmd_line = " ".join(self._child_proc.cmdline())
165
-
166
- self._running = True
167
-
168
- return self._proc, self._child_proc, self._cmd_line
169
-
170
- @abstractmethod
171
- def start(self, args: Optional[List[str]] = None) -> Any:
172
- """Start Austin.
173
-
174
- Every subclass should implement this method and ensure that it spawns
175
- a new Austin process.
176
- """
177
- ...
178
-
179
- def get_process(self) -> psutil.Process:
180
- """Get the underlying Austin process.
181
-
182
- Return an instance of :class:`psuitl.Process` that can be used to
183
- control the underlying Austin process at the OS level.
184
- """
185
- return self._proc
186
-
187
- def get_command_line(self) -> Optional[str]:
188
- """Get the inferred command line.
189
-
190
- Return the command line of the (main) process that is being profiled
191
- by Austin.
192
- """
193
- return self._cmd_line
194
-
195
- def is_running(self) -> bool:
196
- """Determine whether Austin is running."""
197
- return self._running
198
-
199
- def terminate(self, wait: bool = False) -> None:
200
- """Terminate Austin.
201
-
202
- Stop the underlying Austin process by sending a termination signal.
203
- """
204
- if not self._proc:
205
- raise AustinError("Austin has not been started yet")
206
-
207
- try:
208
- self._proc.terminate()
209
- if wait:
210
- self._proc.wait()
211
- except psutil.NoSuchProcess:
212
- raise AustinError("Austin has already terminated") from None
213
-
214
- self._running = False
215
- self._proc = None
216
- self._child_proc = None
217
-
218
- def get_child_process(self) -> psutil.Process:
219
- """Get the child process.
220
-
221
- Return an instalce of :class:`psutil.Process` representing the (main)
222
- process being profiled by Austin at the OS level.
223
- """
224
- return self._child_proc
225
-
226
- def submit_sample(self, data: bytes) -> None:
227
- """Submit a sample to the sample callback.
228
-
229
- This method takes care of converting the raw binary data retrieved from
230
- Austin into a Python string.
231
- """
232
- try:
233
- sample = data.decode()
234
- except UnicodeDecodeError:
235
- try:
236
- sample = data.decode("ascii")
237
- except UnicodeDecodeError:
238
- return
239
-
240
- self._sample_callback(sample.rstrip())
241
-
242
- def check_exit(self, rcode: int, stderr: Optional[str]) -> None:
243
- """Check Austin exit status."""
244
- if rcode:
245
- if rcode in {-15, 15, 241}:
246
- raise AustinTerminated()
247
- raise AustinError(f"({rcode}) {stderr}")
248
-
249
- def check_version(self) -> None:
250
- """Check for the minimum Austin binary version."""
251
- austin_version = self.version
252
- if austin_version is None:
253
- raise AustinError("Cannot determine Austin version")
254
- if austin_version < self.BINARY_VERSION:
255
- raise AustinError(
256
- f"Incompatible Austin version (got {austin_version}, expected >= {self.BINARY_VERSION})"
257
- )
258
-
259
- # ---- Default callbacks ----
260
-
261
- def on_terminate(self, stats: Dict[str, str]) -> Any:
262
- """Terminate event callback.
263
-
264
- Implement to be notified when Austin has terminated gracefully. The
265
- callback accepts an argument that will receive the global statistics.
266
- """
267
- pass
268
-
269
- def on_ready(
270
- self,
271
- process: psutil.Process,
272
- child_process: psutil.Process,
273
- command_line: str,
274
- data: Any = None,
275
- ) -> Any:
276
- """Ready event callback.
277
-
278
- Implement to get notified when Austin has successfully started or
279
- attached the Python process to profile and the first sample has been
280
- produced. This callback receives the Austin process and it's (main)
281
- profiled process as instances of :class:`psutil.Process`, along with
282
- the command line of the latter.
283
- """
284
- pass
285
-
286
- # ---- Properties ----
287
-
288
- @_cached_property
289
- def binary_path(self) -> str:
290
- """Discover the path of the Austin binary.
291
-
292
- Lookup order is:
293
- - current working directory
294
- - ``AUSTINPATH`` variable
295
- - ``~/.austinrc`` file
296
- - ``PATH`` variable.
297
- """
298
- binary_name = self.BINARY
299
-
300
- # Try CWD
301
- binary_path = Path.cwd() / binary_name
302
- if binary_path.is_file():
303
- return str(binary_path)
304
-
305
- # Try with AUSTINPATH variable
306
- austin_path = os.environ.get("AUSTINPATH", None)
307
- if austin_path:
308
- binary_path = Path(austin_path).expanduser() / binary_name
309
- if binary_path.is_file():
310
- return str(binary_path)
311
-
312
- # Try with .austinrc file
313
- if AustinConfiguration().binary is not None:
314
- binary_path = Path(AustinConfiguration().binary).expanduser()
315
- if binary_path.is_file():
316
- return str(binary_path)
317
-
318
- # Try from PATH
319
- return self.BINARY
320
-
321
- @property
322
- def version(self) -> SemVer:
323
- """Austin version."""
324
- return _to_semver(self._meta.get("austin"))
325
-
326
- @property
327
- def python_version(self) -> SemVer:
328
- """The version of the detected Python interpreter."""
329
- return _to_semver(self._meta.get("python"))
austin/aio.py CHANGED
@@ -23,13 +23,15 @@
23
23
 
24
24
  import asyncio
25
25
  import sys
26
- from typing import Dict
27
- from typing import List
28
- from typing import Optional
26
+ import typing as t
29
27
 
30
- from austin import AustinError
31
- from austin import BaseAustin
28
+ from austin.base import AustinState
29
+ from austin.base import BaseAustin
32
30
  from austin.cli import AustinArgumentParser
31
+ from austin.errors import AustinError
32
+ from austin.events import AustinMetadata
33
+ from austin.events import AustinSample
34
+ from austin.format.mojo import AsyncMojoStreamReader
33
35
 
34
36
 
35
37
  class AsyncAustin(BaseAustin):
@@ -38,69 +40,89 @@ class AsyncAustin(BaseAustin):
38
40
  Implements an ``asyncio`` API for Austin so that it can be used alongside
39
41
  other asynchronous tasks.
40
42
 
41
- The following example shows how to make a simple asynchronous echo
42
- implementation of Austin that behaves exactly just like Austin.
43
+ The following example shows how to make a simple asynchronous implementation
44
+ of Austin that returns stack in the collapsed format
43
45
 
44
46
  Example::
45
47
 
46
- class EchoAsyncAustin(AsyncAustin):
47
- def on_ready(self, process, child_process, command_line):
48
- print(f"Austin PID: {process.pid}")
49
- print(f"Python PID: {child_process.pid}")
50
- print(f"Command Line: {command_line}")
48
+ import asyncio
49
+ import sys
51
50
 
52
- def on_sample_received(self, line):
53
- print(line)
51
+ from austin.aio import AsyncAustin
52
+ from austin.format.collapsed_stack import AustinEventCollapsedStackFormatter
53
+
54
+ FORMATTER = AustinEventCollapsedStackFormatter()
55
+
56
+
57
+ class CollapsedStackAsyncAustin(AsyncAustin):
58
+ async def on_sample(self, sample):
59
+ print(FORMATTER.format(sample))
60
+
61
+ async def on_metadata(self, metadata):
62
+ print(FORMATTER.format(metadata))
54
63
 
55
- def on_terminate(self, data):
56
- print(data)
57
64
 
58
65
  if sys.platform == "win32":
59
66
  asyncio.set_event_loop(asyncio.ProactorEventLoop())
60
67
 
61
- try:
62
- austin = EchoAsyncAustin()
63
- asyncio.get_event_loop().run_until_complete(
64
- austin.start(["-i", "10000", "python3", "myscript.py"])
65
- )
66
- except (KeyboardInterrupt, asyncio.CancelledError):
67
- pass
68
- """
69
68
 
70
- async def _read_stderr(self) -> Optional[str]:
71
- assert self.proc.stderr is not None
69
+ async def main():
70
+ austin = CollapsedStackAsyncAustin()
71
+ await austin.start(["-i", "10ms", "python", "myscript.py"])
72
+ await austin.wait()
72
73
 
73
- try:
74
- return (
75
- (await asyncio.wait_for(self.proc.stderr.read(), 0.1)).decode().rstrip()
76
- )
77
- except asyncio.TimeoutError:
78
- return None
79
74
 
80
- async def _read_meta(self) -> Dict[str, str]:
81
- assert self.proc.stdout is not None
82
-
83
- meta = {}
75
+ asyncio.run(main())
76
+ """
84
77
 
85
- while True:
86
- line = (await self.proc.stdout.readline()).decode().rstrip()
87
- if not (line and line.startswith("# ")):
88
- break
89
- key, _, value = line[2:].partition(": ")
90
- meta[key] = value
78
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
79
+ super().__init__(*args, **kwargs)
80
+
81
+ self._run_task: t.Optional[asyncio.Task] = None
82
+ self._proc: t.Optional[asyncio.subprocess.Process] = None
83
+
84
+ async def _run(self, mojo: AsyncMojoStreamReader) -> None:
85
+ # Start collecting samples
86
+ self._state = AustinState.RUNNING
87
+
88
+ async for e in mojo:
89
+ if isinstance(e, AustinSample):
90
+ try:
91
+ await t.cast(t.Awaitable[None], self._sample_callback(e))
92
+ except Exception as exc:
93
+ raise AustinError("Error in call to sample callback") from exc
94
+ elif isinstance(e, AustinMetadata):
95
+ self._meta[e.name] = e.value
96
+ if self._metadata_callback is not None:
97
+ try:
98
+ await t.cast(t.Awaitable[None], self._metadata_callback(e))
99
+ except Exception as exc:
100
+ raise AustinError("Error in call to metadata callback") from exc
101
+
102
+ self._state = AustinState.TERMINATING
103
+
104
+ # Call the terminate callback
105
+ try:
106
+ if self._terminate_callback is not None:
107
+ await t.cast(t.Awaitable[None], self._terminate_callback())
108
+ except Exception as exc:
109
+ raise AustinError("Error in call to terminate callback") from exc
91
110
 
92
- self._meta.update(meta)
93
- return meta
111
+ self._state = AustinState.TERMINATED
94
112
 
95
- async def start(self, args: Optional[List[str]] = None) -> None:
113
+ async def start(self, args: t.Optional[t.Sequence[str]] = None) -> None:
96
114
  """Create the start coroutine.
97
115
 
98
116
  Use with the ``asyncio`` event loop.
99
117
  """
118
+ self._args = AustinArgumentParser().parse_args(args)
119
+
120
+ self._state = AustinState.STARTING
121
+
100
122
  try:
101
123
  _args = list(args if args is not None else sys.argv[1:]) # Make a copy
102
124
  _args.insert(0, "-P")
103
- self.proc = await asyncio.create_subprocess_exec(
125
+ self._proc = await asyncio.create_subprocess_exec(
104
126
  self.binary_path,
105
127
  *_args,
106
128
  stdin=asyncio.subprocess.PIPE,
@@ -110,44 +132,53 @@ class AsyncAustin(BaseAustin):
110
132
  except FileNotFoundError:
111
133
  raise AustinError("Austin executable not found.") from None
112
134
 
113
- if not self.proc.stdout:
135
+ if not self._proc.stdout:
114
136
  raise AustinError("Standard output stream is unexpectedly missing")
115
- if not self.proc.stderr:
137
+ if not self._proc.stderr:
116
138
  raise AustinError("Standard error stream is unexpectedly missing")
117
139
 
118
- self._running = True
140
+ mojo = AsyncMojoStreamReader(self._proc.stdout)
119
141
 
120
- try:
121
- if not await self._read_meta():
122
- raise AustinError("Austin did not start properly")
123
-
124
- self.check_version()
142
+ # Retrieve the Austin version, then call the ready callback
143
+ async for e in mojo:
144
+ if isinstance(e, AustinMetadata):
145
+ self._meta[e.name] = e.value
125
146
 
126
- self._ready_callback(
127
- *self._get_process_info(
128
- AustinArgumentParser().parse_args(args), self.proc.pid
129
- )
130
- )
147
+ try:
148
+ if self._metadata_callback is not None:
149
+ await t.cast(t.Awaitable[None], self._metadata_callback(e))
150
+ except Exception as exc:
151
+ raise AustinError("Error in call to metadata callback") from exc
131
152
 
132
- # Start readline loop
133
- while self._running:
134
- data = (await self.proc.stdout.readline()).rstrip()
135
- if not data:
153
+ if e.name == "austin":
154
+ self._check_version()
136
155
  break
156
+ else:
157
+ raise AustinError("Cannot determine Austin version from output")
137
158
 
138
- self.submit_sample(data)
159
+ # Start the run task where we collect the samples
160
+ self._run_task = asyncio.create_task(self._run(mojo))
161
+
162
+ def terminate(self) -> None:
163
+ """Terminate Austin.
164
+
165
+ Stop the underlying Austin process by sending a termination signal.
166
+ """
167
+ if self._proc is None:
168
+ raise AustinError("Austin is not running")
169
+
170
+ try:
171
+ self._proc.terminate()
172
+ except ProcessLookupError as exc:
173
+ raise AustinError("Austin process already terminated") from exc
139
174
 
140
- self._terminate_callback(await self._read_meta())
141
- self.check_exit(await self.proc.wait(), await self._read_stderr())
175
+ async def wait(self) -> int:
176
+ if self._proc is None or self._run_task is None:
177
+ raise AustinError("Austin process is not running")
142
178
 
143
- except Exception:
144
- try:
145
- self.proc.terminate()
146
- await self.proc.wait()
147
- except Exception:
148
- # best effort
149
- pass
150
- raise
179
+ # Await the run task. This will terminate naturally when the Austin
180
+ # process has terminated.
181
+ await self._run_task
151
182
 
152
- finally:
153
- self._running = False
183
+ # Wait for the Austin process to terminate and return the exit code
184
+ return await self._proc.wait()