Skip to content

Commit 4a00945

Browse files
committed
Fix heap stats and snapshots
1 parent 2423977 commit 4a00945

9 files changed

Lines changed: 103 additions & 81 deletions

File tree

src/py_mini_racer/_context.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import TYPE_CHECKING, Any, NewType, Protocol, cast
99

1010
from py_mini_racer._dll import init_mini_racer
11-
from py_mini_racer._exc import JSEvalException, JSPromiseError, JSTimeoutException
11+
from py_mini_racer._exc import JSEvalException, JSPromiseError
1212
from py_mini_racer._types import (
1313
CancelableJSFunction,
1414
JSArray,
@@ -93,7 +93,7 @@ class Context:
9393

9494
_dll: ctypes.CDLL
9595
_ctx: ContextType
96-
_event_loop: asyncio.AbstractEventLoop
96+
event_loop: asyncio.AbstractEventLoop
9797
_object_factory: ObjectFactory
9898
_next_async_callback_id: Iterator[int]
9999
_active_cancelable_mr_task_callbacks: dict[int, Callable[[ValueHandle], None]] = (
@@ -126,7 +126,7 @@ def handle_callback_from_v8(
126126
if callback_id == _UNCANCELABLE_TASK_CALLBACK_ID:
127127
self._non_cancelable_mr_task_results_queue.put(val_handle)
128128
else:
129-
self._event_loop.call_soon_threadsafe(
129+
self.event_loop.call_soon_threadsafe(
130130
self._handle_callback_from_v8_on_event_loop, callback_id, val_handle
131131
)
132132

@@ -179,9 +179,7 @@ async def await_promise(self, promise: JSPromise) -> PythonJSConvertedTypes:
179179
),
180180
)
181181

182-
future: asyncio.Future[PythonJSConvertedTypes] = (
183-
self._event_loop.create_future()
184-
)
182+
future: asyncio.Future[PythonJSConvertedTypes] = self.event_loop.create_future()
185183

186184
def on_resolved(val_handle: ValueHandle) -> None:
187185
if future.cancelled():
@@ -315,24 +313,7 @@ def array_push(self, arr: JSArray, new_val: PythonJSConvertedTypes) -> None:
315313
)
316314

317315
def are_we_running_on_the_mini_racer_event_loop(self) -> bool:
318-
return get_running_loop_or_none() is self._event_loop
319-
320-
def run_coro(
321-
self,
322-
coro: Coroutine[Any, Any, PythonJSConvertedTypes],
323-
timeout_sec: float | None = None,
324-
) -> PythonJSConvertedTypes:
325-
assert not self.are_we_running_on_the_mini_racer_event_loop(), (
326-
"Cannot run a blocking operation from our own event loop"
327-
)
328-
329-
async def run() -> PythonJSConvertedTypes:
330-
try:
331-
return await asyncio.wait_for(coro, timeout=timeout_sec)
332-
except asyncio.TimeoutError as e:
333-
raise JSTimeoutException from e
334-
335-
return asyncio.run_coroutine_threadsafe(run(), self._event_loop).result()
316+
return get_running_loop_or_none() is self.event_loop
336317

337318
async def call_function_cancelable(
338319
self,
@@ -391,14 +372,22 @@ def was_soft_memory_limit_reached(self) -> bool:
391372
def low_memory_notification(self) -> None:
392373
self._dll.mr_low_memory_notification(self._ctx)
393374

394-
async def heap_stats(self) -> str:
395-
return cast("str", await self._run_cancelable_mr_task(self._dll.mr_heap_stats))
375+
def heap_stats(self) -> str:
376+
return cast(
377+
"str",
378+
self._value_handle_to_python(
379+
self._wrap_raw_handle(self._dll.mr_heap_stats(self._ctx))
380+
),
381+
)
396382

397-
async def heap_snapshot(self) -> str:
383+
def heap_snapshot(self) -> str:
398384
"""Return a snapshot of the V8 isolate heap."""
399385

400386
return cast(
401-
"str", await self._run_cancelable_mr_task(self._dll.mr_heap_snapshot)
387+
"str",
388+
self._value_handle_to_python(
389+
self._wrap_raw_handle(self._dll.mr_heap_snapshot(self._ctx))
390+
),
402391
)
403392

404393
def value_count(self) -> int:
@@ -444,7 +433,7 @@ async def await_into_js_promise_resolvers(val_handle: ValueHandle) -> None:
444433
err_maker(f"Error running Python function:\n{format_exc()}")
445434
)
446435

447-
async with _make_task_set(self._event_loop) as task_set:
436+
async with _make_task_set(self.event_loop) as task_set:
448437
with self._register_js_notification(
449438
lambda val_handle: task_set.start_task(
450439
await_into_js_promise_resolvers(val_handle)

src/py_mini_racer/_dll.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,16 @@ def _build_dll_handle(dll_path: Path) -> ctypes.CDLL: # noqa: PLR0915
108108

109109
handle.mr_cancel_task.argtypes = [ctypes.c_uint64, ctypes.c_uint64]
110110

111-
handle.mr_heap_stats.argtypes = [ctypes.c_uint64, ctypes.c_uint64]
112-
handle.mr_heap_stats.restype = ctypes.c_uint64
111+
handle.mr_heap_stats.argtypes = [ctypes.c_uint64]
112+
handle.mr_heap_stats.restype = RawValueHandle
113113

114114
handle.mr_low_memory_notification.argtypes = [ctypes.c_uint64]
115115

116116
handle.mr_make_js_callback.argtypes = [ctypes.c_uint64, ctypes.c_uint64]
117117
handle.mr_make_js_callback.restype = RawValueHandle
118118

119-
handle.mr_heap_snapshot.argtypes = [ctypes.c_uint64, ctypes.c_uint64]
120-
handle.mr_heap_snapshot.restype = ctypes.c_uint64
119+
handle.mr_heap_snapshot.argtypes = [ctypes.c_uint64]
120+
handle.mr_heap_snapshot.restype = RawValueHandle
121121

122122
handle.mr_get_identity_hash.argtypes = [ctypes.c_uint64, RawValueHandle]
123123
handle.mr_get_identity_hash.restype = RawValueHandle

src/py_mini_racer/_mini_racer.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from py_mini_racer._context import Context, ContextType, get_running_loop_or_none
1717
from py_mini_racer._dll import init_mini_racer, mr_callback_func
18-
from py_mini_racer._exc import WrongReturnTypeException
18+
from py_mini_racer._exc import JSTimeoutException, WrongReturnTypeException
1919
from py_mini_racer._objects import ObjectFactoryImpl
2020
from py_mini_racer._set_timeout import INSTALL_SET_TIMEOUT
2121

@@ -146,20 +146,28 @@ def eval(
146146
# Système international d'unités use seconds.
147147
timeout_sec = timeout / 1000
148148

149-
assert self._ctx is not None
149+
ctx = self._ctx
150+
assert ctx is not None
150151

151-
if not self._ctx.are_we_running_on_the_mini_racer_event_loop():
152-
return self._ctx.run_coro(
153-
self._ctx.eval_cancelable(code), timeout_sec=timeout_sec
154-
)
152+
if not ctx.are_we_running_on_the_mini_racer_event_loop():
153+
154+
async def run() -> PythonJSConvertedTypes:
155+
try:
156+
return await asyncio.wait_for(
157+
ctx.eval_cancelable(code), timeout=timeout_sec
158+
)
159+
except asyncio.TimeoutError as e:
160+
raise JSTimeoutException from e
161+
162+
return asyncio.run_coroutine_threadsafe(run(), ctx.event_loop).result()
155163

156164
assert timeout_sec is None, (
157165
"To apply a timeout in an async context, use "
158166
"`await asyncio.wait_for(mr.eval_cancelable(your_params), "
159167
"timeout=your_timeout)`"
160168
)
161169

162-
return self._ctx.eval(code)
170+
return ctx.eval(code)
163171

164172
async def eval_cancelable(self, code: str) -> PythonJSConvertedTypes:
165173
"""Evaluate JavaScript code in the V8 isolate.
@@ -316,7 +324,13 @@ def heap_stats(self) -> Any: # noqa: ANN401
316324
"""Return the V8 isolate heap statistics."""
317325

318326
assert self._ctx is not None
319-
return self.json_impl.loads(self._ctx.run_coro(self._ctx.heap_stats()))
327+
return self.json_impl.loads(self._ctx.heap_stats())
328+
329+
def heap_snapshot(self) -> Any: # noqa: ANN401
330+
"""Return a snapshot of the V8 isolate heap."""
331+
332+
assert self._ctx is not None
333+
return self.json_impl.loads(self._ctx.heap_snapshot())
320334

321335

322336
@contextmanager

src/py_mini_racer/_objects.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import ctypes
67
from datetime import datetime, timezone
78
from operator import index as op_index
@@ -15,6 +16,7 @@
1516
JSOOMException,
1617
JSParseException,
1718
JSTerminatedException,
19+
JSTimeoutException,
1820
JSValueError,
1921
)
2022
from py_mini_racer._types import (
@@ -131,9 +133,19 @@ def __call__(
131133
timeout_sec: float | None = None,
132134
) -> PythonJSConvertedTypes:
133135
if not self._ctx.are_we_running_on_the_mini_racer_event_loop():
134-
return self._ctx.run_coro(
135-
self._ctx.call_function_cancelable(self, *args, this=this), timeout_sec
136-
)
136+
137+
async def run() -> PythonJSConvertedTypes:
138+
try:
139+
return await asyncio.wait_for(
140+
self._ctx.call_function_cancelable(self, *args, this=this),
141+
timeout=timeout_sec,
142+
)
143+
except asyncio.TimeoutError as e:
144+
raise JSTimeoutException from e
145+
146+
return asyncio.run_coroutine_threadsafe(
147+
run(), self._ctx.event_loop
148+
).result()
137149

138150
assert timeout_sec is None, (
139151
"To apply a timeout in an async context, use "
@@ -166,7 +178,15 @@ def get(self, *, timeout: float | None = None) -> PythonJSConvertedTypes:
166178
"In an async context, call `await promise` instead of promise.get()"
167179
)
168180

169-
return self._ctx.run_coro(self._ctx.await_promise(self), timeout_sec=timeout)
181+
async def run() -> PythonJSConvertedTypes:
182+
try:
183+
return await asyncio.wait_for(
184+
self._ctx.await_promise(self), timeout=timeout
185+
)
186+
except asyncio.TimeoutError as e:
187+
raise JSTimeoutException from e
188+
189+
return asyncio.run_coroutine_threadsafe(run(), self._ctx.event_loop).result()
170190

171191
def __await__(self) -> Generator[Any, None, Any]:
172192
return self._ctx.await_promise(self).__await__()

src/v8_py_frontend/context.cc

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,21 @@ void Context::CancelTask(uint64_t task_id) {
125125
cancelable_task_manager_.Cancel(task_id);
126126
}
127127

128-
auto Context::HeapSnapshot(uint64_t callback_id) -> uint64_t {
129-
return RunTask(
130-
[this](v8::Isolate* isolate) {
131-
return heap_reporter_.HeapSnapshot(isolate);
132-
},
133-
callback_id);
128+
auto Context::HeapSnapshot() -> BinaryValueHandle* {
129+
return bv_registry_.Remember(isolate_manager_
130+
.Run([this](v8::Isolate* isolate) mutable {
131+
return heap_reporter_.HeapSnapshot(
132+
isolate);
133+
})
134+
.get());
134135
}
135136

136-
auto Context::HeapStats(uint64_t callback_id) -> uint64_t {
137-
return RunTask(
138-
[this](v8::Isolate* isolate) {
139-
return heap_reporter_.HeapStats(isolate);
140-
},
141-
callback_id);
137+
auto Context::HeapStats() -> BinaryValueHandle* {
138+
return bv_registry_.Remember(isolate_manager_
139+
.Run([this](v8::Isolate* isolate) mutable {
140+
return heap_reporter_.HeapStats(isolate);
141+
})
142+
.get());
142143
}
143144

144145
auto Context::GetIdentityHash(BinaryValueHandle* obj_handle)

src/v8_py_frontend/context.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class Context {
4141
template <typename... Params>
4242
auto AllocBinaryValue(Params&&... params) -> BinaryValueHandle*;
4343
void CancelTask(uint64_t task_id);
44-
auto HeapSnapshot(uint64_t callback_id) -> uint64_t;
45-
auto HeapStats(uint64_t callback_id) -> uint64_t;
44+
auto HeapSnapshot() -> BinaryValueHandle*;
45+
auto HeapStats() -> BinaryValueHandle*;
4646
auto Eval(BinaryValueHandle* code_handle,
4747

4848
uint64_t callback_id) -> uint64_t;

src/v8_py_frontend/exports.cc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ LIB_EXPORT void mr_cancel_task(uint64_t context_id, uint64_t task_id) {
115115
context->CancelTask(task_id);
116116
}
117117

118-
LIB_EXPORT auto mr_heap_stats(uint64_t context_id,
119-
uint64_t callback_id) -> uint64_t {
118+
LIB_EXPORT auto mr_heap_stats(uint64_t context_id)
119+
-> MiniRacer::BinaryValueHandle* {
120120
auto context = GetContext(context_id);
121121
if (!context) {
122122
return 0;
123123
}
124-
return context->HeapStats(callback_id);
124+
return context->HeapStats();
125125
}
126126

127127
LIB_EXPORT void mr_set_hard_memory_limit(uint64_t context_id, size_t limit) {
@@ -273,13 +273,13 @@ LIB_EXPORT auto mr_call_function(uint64_t context_id,
273273
callback_id);
274274
}
275275

276-
LIB_EXPORT auto mr_heap_snapshot(uint64_t context_id,
277-
uint64_t callback_id) -> uint64_t {
276+
LIB_EXPORT auto mr_heap_snapshot(uint64_t context_id)
277+
-> MiniRacer::BinaryValueHandle* {
278278
auto context = GetContext(context_id);
279279
if (!context) {
280280
return 0;
281281
}
282-
return context->HeapSnapshot(callback_id);
282+
return context->HeapSnapshot();
283283
}
284284

285285
LIB_EXPORT auto mr_value_count(uint64_t context_id) -> size_t {

src/v8_py_frontend/exports.h

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,7 @@ LIB_EXPORT auto mr_array_push(uint64_t context_id,
209209

210210
/** Cancel the given asynchronous task.
211211
*
212-
* (Such tasks are started by mr_eval, mr_call_function, mr_heap_stats, and
213-
* mr_heap_snapshot).
212+
* (Such tasks are started by mr_eval and mr_call_function).
214213
**/
215214
LIB_EXPORT void mr_cancel_task(uint64_t context_id, uint64_t task_id);
216215

@@ -248,26 +247,16 @@ LIB_EXPORT auto mr_call_function(uint64_t context_id,
248247
/** Get stats for the V8 heap.
249248
*
250249
* This function is intended for use in debugging only.
251-
*
252-
* This call is processed asynchronously and as such accepts a callback ID.
253-
* The callback ID and a MiniRacer::BinaryValueHandle* containing the
254-
* evaluation result are passed back to the callback upon completion. A task ID
255-
* is returned which can be passed back to mr_cancel_task to cancel evaluation.
256250
**/
257-
LIB_EXPORT auto mr_heap_stats(uint64_t context_id,
258-
uint64_t callback_id) -> uint64_t;
251+
LIB_EXPORT auto mr_heap_stats(uint64_t context_id)
252+
-> MiniRacer::BinaryValueHandle*;
259253

260254
/** Get a snapshot of the V8 heap.
261255
*
262256
* This function is intended for use in debugging only.
263-
*
264-
* This call is processed asynchronously and as such accepts a callback ID.
265-
* The callback ID and a MiniRacer::BinaryValueHandle* containing the
266-
* evaluation result are passed back to the callback upon completion. A task ID
267-
* is returned which can be passed back to mr_cancel_task to cancel evaluation.
268257
**/
269-
LIB_EXPORT auto mr_heap_snapshot(uint64_t context_id,
270-
uint64_t callback_id) -> uint64_t;
258+
LIB_EXPORT auto mr_heap_snapshot(uint64_t context_id)
259+
-> MiniRacer::BinaryValueHandle*;
271260

272261
// NOLINTEND(bugprone-easily-swappable-parameters)
273262

tests/test_heap.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,12 @@ def test_heap_stats() -> None:
1111
assert mr.heap_stats()["total_heap_size"] > 0
1212

1313
assert_no_v8_objects(mr)
14+
15+
16+
def test_heap_snapshot() -> None:
17+
mr = MiniRacer()
18+
19+
assert mr.heap_snapshot()["edges"]
20+
assert mr.heap_snapshot()["strings"]
21+
22+
assert_no_v8_objects(mr)

0 commit comments

Comments
 (0)