[pypy-commit] pypy default: merge the gc-hooks branch: it is now possible to install app-level hooks which are triggered whenever a specific GC activity occurs
antocuni
pypy.commits at gmail.com
Tue Apr 17 06:09:49 EDT 2018
Author: Antonio Cuni <anto.cuni at gmail.com>
Branch:
Changeset: r94361:5bd740a5496c
Date: 2018-04-17 12:09 +0200
http://bitbucket.org/pypy/pypy/changeset/5bd740a5496c/
Log: merge the gc-hooks branch: it is now possible to install app-level
hooks which are triggered whenever a specific GC activity occurs
diff --git a/pypy/doc/gc_info.rst b/pypy/doc/gc_info.rst
--- a/pypy/doc/gc_info.rst
+++ b/pypy/doc/gc_info.rst
@@ -121,6 +121,160 @@
alive by GC objects, but not accounted in the GC
+GC Hooks
+--------
+
+GC hooks are user-defined functions which are called whenever a specific GC
+event occur, and can be used to monitor GC activity and pauses. You can
+install the hooks by setting the following attributes:
+
+``gc.hook.on_gc_minor``
+ Called whenever a minor collection occurs. It corresponds to
+ ``gc-minor`` sections inside ``PYPYLOG``.
+
+``gc.hook.on_gc_collect_step``
+ Called whenever an incremental step of a major collection occurs. It
+ corresponds to ``gc-collect-step`` sections inside ``PYPYLOG``.
+
+``gc.hook.on_gc_collect``
+ Called after the last incremental step, when a major collection is fully
+ done. It corresponds to ``gc-collect-done`` sections inside ``PYPYLOG``.
+
+To uninstall a hook, simply set the corresponding attribute to ``None``. To
+install all hooks at once, you can call ``gc.hooks.set(obj)``, which will look
+for methods ``on_gc_*`` on ``obj``. To uninstall all the hooks at once, you
+can call ``gc.hooks.reset()``.
+
+The functions called by the hooks receive a single ``stats`` argument, which
+contains various statistics about the event.
+
+Note that PyPy cannot call the hooks immediately after a GC event, but it has
+to wait until it reaches a point in which the interpreter is in a known state
+and calling user-defined code is harmless. It might happen that multiple
+events occur before the hook is invoked: in this case, you can inspect the
+value ``stats.count`` to know how many times the event occured since the last
+time the hook was called. Similarly, ``stats.duration`` contains the
+**total** time spent by the GC for this specific event since the last time the
+hook was called.
+
+On the other hand, all the other fields of the ``stats`` object are relative
+only to the **last** event of the series.
+
+The attributes for ``GcMinorStats`` are:
+
+``count``
+ The number of minor collections occured since the last hook call.
+
+``duration``
+ The total time spent inside minor collections since the last hook
+ call. See below for more information on the unit.
+
+ ``total_memory_used``
+ The amount of memory used at the end of the minor collection, in
+ bytes. This include the memory used in arenas (for GC-managed memory) and
+ raw-malloced memory (e.g., the content of numpy arrays).
+
+``pinned_objects``
+ the number of pinned objects.
+
+
+The attributes for ``GcCollectStepStats`` are:
+
+``count``, ``duration``
+ See above.
+
+``oldstate``, ``newstate``
+ Integers which indicate the state of the GC before and after the step.
+
+The value of ``oldstate`` and ``newstate`` is one of these constants, defined
+inside ``gc.GcCollectStepStats``: ``STATE_SCANNING``, ``STATE_MARKING``,
+``STATE_SWEEPING``, ``STATE_FINALIZING``. It is possible to get a string
+representation of it by indexing the ``GC_STATS`` tuple.
+
+
+The attributes for ``GcCollectStats`` are:
+
+``count``
+ See above.
+
+``num_major_collects``
+ The total number of major collections which have been done since the
+ start. Contrarily to ``count``, this is an always-growing counter and it's
+ not reset between invocations.
+
+``arenas_count_before``, ``arenas_count_after``
+ Number of arenas used before and after the major collection.
+
+``arenas_bytes``
+ Total number of bytes used by GC-managed objects.
+
+``rawmalloc_bytes_before``, ``rawmalloc_bytes_after``
+ Total number of bytes used by raw-malloced objects, before and after the
+ major collection.
+
+Note that ``GcCollectStats`` has **not** got a ``duration`` field. This is
+because all the GC work is done inside ``gc-collect-step``:
+``gc-collect-done`` is used only to give additional stats, but doesn't do any
+actual work.
+
+A note about the ``duration`` field: depending on the architecture and
+operating system, PyPy uses different ways to read timestamps, so ``duration``
+is expressed in varying units. It is possible to know which by calling
+``__pypy__.debug_get_timestamp_unit()``, which can be one of the following
+values:
+
+``tsc``
+ The default on ``x86`` machines: timestamps are expressed in CPU ticks, as
+ read by the `Time Stamp Counter`_.
+
+``ns``
+ Timestamps are expressed in nanoseconds.
+
+``QueryPerformanceCounter``
+ On Windows, in case for some reason ``tsc`` is not available: timestamps
+ are read using the win API ``QueryPerformanceCounter()``.
+
+
+Unfortunately, there does not seem to be a reliable standard way for
+converting ``tsc`` ticks into nanoseconds, although in practice on modern CPUs
+it is enough to divide the ticks by the maximum nominal frequency of the CPU.
+For this reason, PyPy gives the raw value, and leaves the job of doing the
+conversion to external libraries.
+
+Here is an example of GC hooks in use::
+
+ import sys
+ import gc
+
+ class MyHooks(object):
+ done = False
+
+ def on_gc_minor(self, stats):
+ print 'gc-minor: count = %02d, duration = %d' % (stats.count,
+ stats.duration)
+
+ def on_gc_collect_step(self, stats):
+ old = gc.GcCollectStepStats.GC_STATES[stats.oldstate]
+ new = gc.GcCollectStepStats.GC_STATES[stats.newstate]
+ print 'gc-collect-step: %s --> %s' % (old, new)
+ print ' count = %02d, duration = %d' % (stats.count,
+ stats.duration)
+
+ def on_gc_collect(self, stats):
+ print 'gc-collect-done: count = %02d' % stats.count
+ self.done = True
+
+ hooks = MyHooks()
+ gc.hooks.set(hooks)
+
+ # simulate some GC activity
+ lst = []
+ while not hooks.done:
+ lst = [lst, 1, 2, 3]
+
+
+.. _`Time Stamp Counter`: https://en.wikipedia.org/wiki/Time_Stamp_Counter
+
.. _minimark-environment-variables:
Environment variables
diff --git a/pypy/doc/whatsnew-head.rst b/pypy/doc/whatsnew-head.rst
--- a/pypy/doc/whatsnew-head.rst
+++ b/pypy/doc/whatsnew-head.rst
@@ -10,3 +10,7 @@
Fix a rare GC bug that was introduced more than one year ago, but was
not diagnosed before issue #2752.
+
+.. branch: gc-hooks
+
+Introduce GC hooks, as documented in doc/gc_info.rst
diff --git a/pypy/goal/targetpypystandalone.py b/pypy/goal/targetpypystandalone.py
--- a/pypy/goal/targetpypystandalone.py
+++ b/pypy/goal/targetpypystandalone.py
@@ -215,6 +215,7 @@
usage = SUPPRESS_USAGE
take_options = True
+ space = None
def opt_parser(self, config):
parser = to_optparse(config, useoptions=["objspace.*"],
@@ -364,15 +365,21 @@
from pypy.module.pypyjit.hooks import pypy_hooks
return PyPyJitPolicy(pypy_hooks)
+ def get_gchooks(self):
+ from pypy.module.gc.hook import LowLevelGcHooks
+ if self.space is None:
+ raise Exception("get_gchooks must be called afeter get_entry_point")
+ return self.space.fromcache(LowLevelGcHooks)
+
def get_entry_point(self, config):
- space = make_objspace(config)
+ self.space = make_objspace(config)
# manually imports app_main.py
filename = os.path.join(pypydir, 'interpreter', 'app_main.py')
app = gateway.applevel(open(filename).read(), 'app_main.py', 'app_main')
app.hidden_applevel = False
- w_dict = app.getwdict(space)
- entry_point, _ = create_entry_point(space, w_dict)
+ w_dict = app.getwdict(self.space)
+ entry_point, _ = create_entry_point(self.space, w_dict)
return entry_point, None, PyPyAnnotatorPolicy()
@@ -381,7 +388,7 @@
'jitpolicy', 'get_entry_point',
'get_additional_config_options']:
ns[name] = getattr(self, name)
-
+ ns['get_gchooks'] = self.get_gchooks
PyPyTarget().interface(globals())
diff --git a/pypy/interpreter/executioncontext.py b/pypy/interpreter/executioncontext.py
--- a/pypy/interpreter/executioncontext.py
+++ b/pypy/interpreter/executioncontext.py
@@ -404,7 +404,7 @@
self._periodic_actions = []
self._nonperiodic_actions = []
self.has_bytecode_counter = False
- self.fired_actions = None
+ self._fired_actions_reset()
# the default value is not 100, unlike CPython 2.7, but a much
# larger value, because we use a technique that not only allows
# but actually *forces* another thread to run whenever the counter
@@ -416,13 +416,28 @@
"""Request for the action to be run before the next opcode."""
if not action._fired:
action._fired = True
- if self.fired_actions is None:
- self.fired_actions = []
- self.fired_actions.append(action)
+ self._fired_actions_append(action)
# set the ticker to -1 in order to force action_dispatcher()
# to run at the next possible bytecode
self.reset_ticker(-1)
+ def _fired_actions_reset(self):
+ # linked list of actions. We cannot use a normal RPython list because
+ # we want AsyncAction.fire() to be marked as @rgc.collect: this way,
+ # we can call it from e.g. GcHooks or cpyext's dealloc_trigger.
+ self._fired_actions_first = None
+ self._fired_actions_last = None
+
+ @rgc.no_collect
+ def _fired_actions_append(self, action):
+ assert action._next is None
+ if self._fired_actions_first is None:
+ self._fired_actions_first = action
+ self._fired_actions_last = action
+ else:
+ self._fired_actions_last._next = action
+ self._fired_actions_last = action
+
@not_rpython
def register_periodic_action(self, action, use_bytecode_counter):
"""
@@ -467,9 +482,9 @@
action.perform(ec, frame)
# nonperiodic actions
- list = self.fired_actions
- if list is not None:
- self.fired_actions = None
+ action = self._fired_actions_first
+ if action:
+ self._fired_actions_reset()
# NB. in case there are several actions, we reset each
# 'action._fired' to false only when we're about to call
# 'action.perform()'. This means that if
@@ -477,9 +492,10 @@
# the corresponding perform(), the fire() has no
# effect---which is the effect we want, because
# perform() will be called anyway.
- for action in list:
+ while action is not None:
action._fired = False
action.perform(ec, frame)
+ action._next, action = None, action._next
self.action_dispatcher = action_dispatcher
@@ -512,10 +528,12 @@
to occur between two opcodes, not at a completely random time.
"""
_fired = False
+ _next = None
def __init__(self, space):
self.space = space
+ @rgc.no_collect
def fire(self):
"""Request for the action to be run before the next opcode.
The action must have been registered at space initalization time."""
diff --git a/pypy/interpreter/test/test_executioncontext.py b/pypy/interpreter/test/test_executioncontext.py
--- a/pypy/interpreter/test/test_executioncontext.py
+++ b/pypy/interpreter/test/test_executioncontext.py
@@ -37,6 +37,37 @@
pass
assert i == 9
+ def test_action_queue(self):
+ events = []
+
+ class Action1(executioncontext.AsyncAction):
+ def perform(self, ec, frame):
+ events.append('one')
+
+ class Action2(executioncontext.AsyncAction):
+ def perform(self, ec, frame):
+ events.append('two')
+
+ space = self.space
+ a1 = Action1(space)
+ a2 = Action2(space)
+ a1.fire()
+ a2.fire()
+ space.appexec([], """():
+ n = 5
+ return n + 2
+ """)
+ assert events == ['one', 'two']
+ #
+ events[:] = []
+ a1.fire()
+ space.appexec([], """():
+ n = 5
+ return n + 2
+ """)
+ assert events == ['one']
+
+
def test_periodic_action(self):
from pypy.interpreter.executioncontext import ActionFlag
diff --git a/pypy/module/__pypy__/__init__.py b/pypy/module/__pypy__/__init__.py
--- a/pypy/module/__pypy__/__init__.py
+++ b/pypy/module/__pypy__/__init__.py
@@ -82,6 +82,8 @@
'debug_stop' : 'interp_debug.debug_stop',
'debug_print_once' : 'interp_debug.debug_print_once',
'debug_flush' : 'interp_debug.debug_flush',
+ 'debug_read_timestamp' : 'interp_debug.debug_read_timestamp',
+ 'debug_get_timestamp_unit' : 'interp_debug.debug_get_timestamp_unit',
'builtinify' : 'interp_magic.builtinify',
'hidden_applevel' : 'interp_magic.hidden_applevel',
'get_hidden_tb' : 'interp_magic.get_hidden_tb',
diff --git a/pypy/module/__pypy__/interp_debug.py b/pypy/module/__pypy__/interp_debug.py
--- a/pypy/module/__pypy__/interp_debug.py
+++ b/pypy/module/__pypy__/interp_debug.py
@@ -1,6 +1,6 @@
from pypy.interpreter.gateway import unwrap_spec
from rpython.rlib import debug, jit
-
+from rpython.rlib import rtimer
@jit.dont_look_inside
@unwrap_spec(category='text')
@@ -28,3 +28,18 @@
@jit.dont_look_inside
def debug_flush(space):
debug.debug_flush()
+
+def debug_read_timestamp(space):
+ return space.newint(rtimer.read_timestamp())
+
+def debug_get_timestamp_unit(space):
+ unit = rtimer.get_timestamp_unit()
+ if unit == rtimer.UNIT_TSC:
+ unit_str = 'tsc'
+ elif unit == rtimer.UNIT_NS:
+ unit_str = 'ns'
+ elif unit == rtimer.UNIT_QUERY_PERFORMANCE_COUNTER:
+ unit_str = 'QueryPerformanceCounter'
+ else:
+ unit_str = 'UNKNOWN(%d)' % unit
+ return space.newtext(unit_str)
diff --git a/pypy/module/__pypy__/test/test_debug.py b/pypy/module/__pypy__/test/test_debug.py
--- a/pypy/module/__pypy__/test/test_debug.py
+++ b/pypy/module/__pypy__/test/test_debug.py
@@ -48,3 +48,14 @@
from __pypy__ import debug_flush
debug_flush()
# assert did not crash
+
+ def test_debug_read_timestamp(self):
+ from __pypy__ import debug_read_timestamp
+ a = debug_read_timestamp()
+ b = debug_read_timestamp()
+ assert b > a
+
+ def test_debug_get_timestamp_unit(self):
+ from __pypy__ import debug_get_timestamp_unit
+ unit = debug_get_timestamp_unit()
+ assert unit in ('tsc', 'ns', 'QueryPerformanceCounter')
diff --git a/pypy/module/gc/__init__.py b/pypy/module/gc/__init__.py
--- a/pypy/module/gc/__init__.py
+++ b/pypy/module/gc/__init__.py
@@ -34,5 +34,7 @@
'get_typeids_z': 'referents.get_typeids_z',
'get_typeids_list': 'referents.get_typeids_list',
'GcRef': 'referents.W_GcRef',
+ 'hooks': 'space.fromcache(hook.W_AppLevelHooks)',
+ 'GcCollectStepStats': 'hook.W_GcCollectStepStats',
})
MixedModule.__init__(self, space, w_name)
diff --git a/pypy/module/gc/hook.py b/pypy/module/gc/hook.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/gc/hook.py
@@ -0,0 +1,317 @@
+from rpython.memory.gc.hook import GcHooks
+from rpython.memory.gc import incminimark
+from rpython.rlib.nonconst import NonConstant
+from rpython.rlib.rarithmetic import r_uint, r_longlong
+from pypy.interpreter.gateway import interp2app, unwrap_spec, WrappedDefault
+from pypy.interpreter.baseobjspace import W_Root
+from pypy.interpreter.typedef import TypeDef, interp_attrproperty, GetSetProperty
+from pypy.interpreter.executioncontext import AsyncAction
+
+class LowLevelGcHooks(GcHooks):
+ """
+ These are the low-level hooks which are called directly from the GC.
+
+ They can't do much, because the base class marks the methods as
+ @rgc.no_collect.
+
+ This is expected to be a singleton, created by space.fromcache, and it is
+ integrated with the translation by targetpypystandalone.get_gchooks
+ """
+
+ def __init__(self, space):
+ self.space = space
+ self.w_hooks = space.fromcache(W_AppLevelHooks)
+
+ def is_gc_minor_enabled(self):
+ return self.w_hooks.gc_minor_enabled
+
+ def is_gc_collect_step_enabled(self):
+ return self.w_hooks.gc_collect_step_enabled
+
+ def is_gc_collect_enabled(self):
+ return self.w_hooks.gc_collect_enabled
+
+ def on_gc_minor(self, duration, total_memory_used, pinned_objects):
+ action = self.w_hooks.gc_minor
+ action.count += 1
+ action.duration += duration
+ action.total_memory_used = total_memory_used
+ action.pinned_objects = pinned_objects
+ action.fire()
+
+ def on_gc_collect_step(self, duration, oldstate, newstate):
+ action = self.w_hooks.gc_collect_step
+ action.count += 1
+ action.duration += duration
+ action.oldstate = oldstate
+ action.newstate = newstate
+ action.fire()
+
+ def on_gc_collect(self, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ action = self.w_hooks.gc_collect
+ action.count += 1
+ action.num_major_collects = num_major_collects
+ action.arenas_count_before = arenas_count_before
+ action.arenas_count_after = arenas_count_after
+ action.arenas_bytes = arenas_bytes
+ action.rawmalloc_bytes_before = rawmalloc_bytes_before
+ action.rawmalloc_bytes_after = rawmalloc_bytes_after
+ action.fire()
+
+
+class W_AppLevelHooks(W_Root):
+
+ def __init__(self, space):
+ self.space = space
+ self.gc_minor_enabled = False
+ self.gc_collect_step_enabled = False
+ self.gc_collect_enabled = False
+ self.gc_minor = GcMinorHookAction(space)
+ self.gc_collect_step = GcCollectStepHookAction(space)
+ self.gc_collect = GcCollectHookAction(space)
+
+ def descr_get_on_gc_minor(self, space):
+ return self.gc_minor.w_callable
+
+ def descr_set_on_gc_minor(self, space, w_obj):
+ self.gc_minor_enabled = not space.is_none(w_obj)
+ self.gc_minor.w_callable = w_obj
+ self.gc_minor.fix_annotation()
+
+ def descr_get_on_gc_collect_step(self, space):
+ return self.gc_collect_step.w_callable
+
+ def descr_set_on_gc_collect_step(self, space, w_obj):
+ self.gc_collect_step_enabled = not space.is_none(w_obj)
+ self.gc_collect_step.w_callable = w_obj
+ self.gc_collect_step.fix_annotation()
+
+ def descr_get_on_gc_collect(self, space):
+ return self.gc_collect.w_callable
+
+ def descr_set_on_gc_collect(self, space, w_obj):
+ self.gc_collect_enabled = not space.is_none(w_obj)
+ self.gc_collect.w_callable = w_obj
+ self.gc_collect.fix_annotation()
+
+ def descr_set(self, space, w_obj):
+ w_a = space.getattr(w_obj, space.newtext('on_gc_minor'))
+ w_b = space.getattr(w_obj, space.newtext('on_gc_collect_step'))
+ w_c = space.getattr(w_obj, space.newtext('on_gc_collect'))
+ self.descr_set_on_gc_minor(space, w_a)
+ self.descr_set_on_gc_collect_step(space, w_b)
+ self.descr_set_on_gc_collect(space, w_c)
+
+ def descr_reset(self, space):
+ self.descr_set_on_gc_minor(space, space.w_None)
+ self.descr_set_on_gc_collect_step(space, space.w_None)
+ self.descr_set_on_gc_collect(space, space.w_None)
+
+
+class GcMinorHookAction(AsyncAction):
+ count = 0
+ duration = r_longlong(0)
+ total_memory_used = 0
+ pinned_objects = 0
+
+ def __init__(self, space):
+ AsyncAction.__init__(self, space)
+ self.w_callable = space.w_None
+
+ def reset(self):
+ self.count = 0
+ self.duration = r_longlong(0)
+
+ def fix_annotation(self):
+ # the annotation of the class and its attributes must be completed
+ # BEFORE we do the gc transform; this makes sure that everything is
+ # annotated with the correct types
+ if NonConstant(False):
+ self.count = NonConstant(-42)
+ self.duration = NonConstant(r_longlong(-42))
+ self.total_memory_used = NonConstant(r_uint(42))
+ self.pinned_objects = NonConstant(-42)
+ self.fire()
+
+ def perform(self, ec, frame):
+ w_stats = W_GcMinorStats(
+ self.count,
+ self.duration,
+ self.total_memory_used,
+ self.pinned_objects)
+ self.reset()
+ self.space.call_function(self.w_callable, w_stats)
+
+
+class GcCollectStepHookAction(AsyncAction):
+ count = 0
+ duration = r_longlong(0)
+ oldstate = 0
+ newstate = 0
+
+ def __init__(self, space):
+ AsyncAction.__init__(self, space)
+ self.w_callable = space.w_None
+
+ def reset(self):
+ self.count = 0
+ self.duration = r_longlong(0)
+
+ def fix_annotation(self):
+ # the annotation of the class and its attributes must be completed
+ # BEFORE we do the gc transform; this makes sure that everything is
+ # annotated with the correct types
+ if NonConstant(False):
+ self.count = NonConstant(-42)
+ self.duration = NonConstant(r_longlong(-42))
+ self.oldstate = NonConstant(-42)
+ self.newstate = NonConstant(-42)
+ self.fire()
+
+ def perform(self, ec, frame):
+ w_stats = W_GcCollectStepStats(
+ self.count,
+ self.duration,
+ self.oldstate,
+ self.newstate)
+ self.reset()
+ self.space.call_function(self.w_callable, w_stats)
+
+
+class GcCollectHookAction(AsyncAction):
+ count = 0
+ num_major_collects = 0
+ arenas_count_before = 0
+ arenas_count_after = 0
+ arenas_bytes = 0
+ rawmalloc_bytes_before = 0
+ rawmalloc_bytes_after = 0
+
+ def __init__(self, space):
+ AsyncAction.__init__(self, space)
+ self.w_callable = space.w_None
+
+ def reset(self):
+ self.count = 0
+
+ def fix_annotation(self):
+ # the annotation of the class and its attributes must be completed
+ # BEFORE we do the gc transform; this makes sure that everything is
+ # annotated with the correct types
+ if NonConstant(False):
+ self.count = NonConstant(-42)
+ self.num_major_collects = NonConstant(-42)
+ self.arenas_count_before = NonConstant(-42)
+ self.arenas_count_after = NonConstant(-42)
+ self.arenas_bytes = NonConstant(r_uint(42))
+ self.rawmalloc_bytes_before = NonConstant(r_uint(42))
+ self.rawmalloc_bytes_after = NonConstant(r_uint(42))
+ self.fire()
+
+ def perform(self, ec, frame):
+ w_stats = W_GcCollectStats(self.count,
+ self.num_major_collects,
+ self.arenas_count_before,
+ self.arenas_count_after,
+ self.arenas_bytes,
+ self.rawmalloc_bytes_before,
+ self.rawmalloc_bytes_after)
+ self.reset()
+ self.space.call_function(self.w_callable, w_stats)
+
+
+class W_GcMinorStats(W_Root):
+
+ def __init__(self, count, duration, total_memory_used, pinned_objects):
+ self.count = count
+ self.duration = duration
+ self.total_memory_used = total_memory_used
+ self.pinned_objects = pinned_objects
+
+
+class W_GcCollectStepStats(W_Root):
+
+ def __init__(self, count, duration, oldstate, newstate):
+ self.count = count
+ self.duration = duration
+ self.oldstate = oldstate
+ self.newstate = newstate
+
+
+class W_GcCollectStats(W_Root):
+ def __init__(self, count, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ self.count = count
+ self.num_major_collects = num_major_collects
+ self.arenas_count_before = arenas_count_before
+ self.arenas_count_after = arenas_count_after
+ self.arenas_bytes = arenas_bytes
+ self.rawmalloc_bytes_before = rawmalloc_bytes_before
+ self.rawmalloc_bytes_after = rawmalloc_bytes_after
+
+
+# just a shortcut to make the typedefs shorter
+def wrap_many_ints(cls, names):
+ d = {}
+ for name in names:
+ d[name] = interp_attrproperty(name, cls=cls, wrapfn="newint")
+ return d
+
+
+W_AppLevelHooks.typedef = TypeDef(
+ "GcHooks",
+ on_gc_minor = GetSetProperty(
+ W_AppLevelHooks.descr_get_on_gc_minor,
+ W_AppLevelHooks.descr_set_on_gc_minor),
+
+ on_gc_collect_step = GetSetProperty(
+ W_AppLevelHooks.descr_get_on_gc_collect_step,
+ W_AppLevelHooks.descr_set_on_gc_collect_step),
+
+ on_gc_collect = GetSetProperty(
+ W_AppLevelHooks.descr_get_on_gc_collect,
+ W_AppLevelHooks.descr_set_on_gc_collect),
+
+ set = interp2app(W_AppLevelHooks.descr_set),
+ reset = interp2app(W_AppLevelHooks.descr_reset),
+ )
+
+W_GcMinorStats.typedef = TypeDef(
+ "GcMinorStats",
+ **wrap_many_ints(W_GcMinorStats, (
+ "count",
+ "duration",
+ "total_memory_used",
+ "pinned_objects"))
+ )
+
+W_GcCollectStepStats.typedef = TypeDef(
+ "GcCollectStepStats",
+ STATE_SCANNING = incminimark.STATE_SCANNING,
+ STATE_MARKING = incminimark.STATE_MARKING,
+ STATE_SWEEPING = incminimark.STATE_SWEEPING,
+ STATE_FINALIZING = incminimark.STATE_FINALIZING,
+ GC_STATES = tuple(incminimark.GC_STATES),
+ **wrap_many_ints(W_GcCollectStepStats, (
+ "count",
+ "duration",
+ "oldstate",
+ "newstate"))
+ )
+
+W_GcCollectStats.typedef = TypeDef(
+ "GcCollectStats",
+ **wrap_many_ints(W_GcCollectStats, (
+ "count",
+ "num_major_collects",
+ "arenas_count_before",
+ "arenas_count_after",
+ "arenas_bytes",
+ "rawmalloc_bytes_before",
+ "rawmalloc_bytes_after"))
+ )
diff --git a/pypy/module/gc/test/test_hook.py b/pypy/module/gc/test/test_hook.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/gc/test/test_hook.py
@@ -0,0 +1,176 @@
+import pytest
+from rpython.rlib.rarithmetic import r_uint
+from pypy.module.gc.hook import LowLevelGcHooks
+from pypy.interpreter.baseobjspace import ObjSpace
+from pypy.interpreter.gateway import interp2app, unwrap_spec
+
+class AppTestGcHooks(object):
+
+ def setup_class(cls):
+ if cls.runappdirect:
+ pytest.skip("these tests cannot work with -A")
+ space = cls.space
+ gchooks = space.fromcache(LowLevelGcHooks)
+
+ @unwrap_spec(ObjSpace, int, r_uint, int)
+ def fire_gc_minor(space, duration, total_memory_used, pinned_objects):
+ gchooks.fire_gc_minor(duration, total_memory_used, pinned_objects)
+
+ @unwrap_spec(ObjSpace, int, int, int)
+ def fire_gc_collect_step(space, duration, oldstate, newstate):
+ gchooks.fire_gc_collect_step(duration, oldstate, newstate)
+
+ @unwrap_spec(ObjSpace, int, int, int, r_uint, r_uint, r_uint)
+ def fire_gc_collect(space, a, b, c, d, e, f):
+ gchooks.fire_gc_collect(a, b, c, d, e, f)
+
+ @unwrap_spec(ObjSpace)
+ def fire_many(space):
+ gchooks.fire_gc_minor(5, 0, 0)
+ gchooks.fire_gc_minor(7, 0, 0)
+ gchooks.fire_gc_collect_step(5, 0, 0)
+ gchooks.fire_gc_collect_step(15, 0, 0)
+ gchooks.fire_gc_collect_step(22, 0, 0)
+ gchooks.fire_gc_collect(1, 2, 3, 4, 5, 6)
+
+ cls.w_fire_gc_minor = space.wrap(interp2app(fire_gc_minor))
+ cls.w_fire_gc_collect_step = space.wrap(interp2app(fire_gc_collect_step))
+ cls.w_fire_gc_collect = space.wrap(interp2app(fire_gc_collect))
+ cls.w_fire_many = space.wrap(interp2app(fire_many))
+
+ def test_default(self):
+ import gc
+ assert gc.hooks.on_gc_minor is None
+ assert gc.hooks.on_gc_collect_step is None
+ assert gc.hooks.on_gc_collect is None
+
+ def test_on_gc_minor(self):
+ import gc
+ lst = []
+ def on_gc_minor(stats):
+ lst.append((stats.count,
+ stats.duration,
+ stats.total_memory_used,
+ stats.pinned_objects))
+ gc.hooks.on_gc_minor = on_gc_minor
+ self.fire_gc_minor(10, 20, 30)
+ self.fire_gc_minor(40, 50, 60)
+ assert lst == [
+ (1, 10, 20, 30),
+ (1, 40, 50, 60),
+ ]
+ #
+ gc.hooks.on_gc_minor = None
+ self.fire_gc_minor(70, 80, 90) # won't fire because the hooks is disabled
+ assert lst == [
+ (1, 10, 20, 30),
+ (1, 40, 50, 60),
+ ]
+
+ def test_on_gc_collect_step(self):
+ import gc
+ lst = []
+ def on_gc_collect_step(stats):
+ lst.append((stats.count,
+ stats.duration,
+ stats.oldstate,
+ stats.newstate))
+ gc.hooks.on_gc_collect_step = on_gc_collect_step
+ self.fire_gc_collect_step(10, 20, 30)
+ self.fire_gc_collect_step(40, 50, 60)
+ assert lst == [
+ (1, 10, 20, 30),
+ (1, 40, 50, 60),
+ ]
+ #
+ gc.hooks.on_gc_collect_step = None
+ self.fire_gc_collect_step(70, 80, 90) # won't fire
+ assert lst == [
+ (1, 10, 20, 30),
+ (1, 40, 50, 60),
+ ]
+
+ def test_on_gc_collect(self):
+ import gc
+ lst = []
+ def on_gc_collect(stats):
+ lst.append((stats.count,
+ stats.num_major_collects,
+ stats.arenas_count_before,
+ stats.arenas_count_after,
+ stats.arenas_bytes,
+ stats.rawmalloc_bytes_before,
+ stats.rawmalloc_bytes_after))
+ gc.hooks.on_gc_collect = on_gc_collect
+ self.fire_gc_collect(1, 2, 3, 4, 5, 6)
+ self.fire_gc_collect(7, 8, 9, 10, 11, 12)
+ assert lst == [
+ (1, 1, 2, 3, 4, 5, 6),
+ (1, 7, 8, 9, 10, 11, 12),
+ ]
+ #
+ gc.hooks.on_gc_collect = None
+ self.fire_gc_collect(42, 42, 42, 42, 42, 42) # won't fire
+ assert lst == [
+ (1, 1, 2, 3, 4, 5, 6),
+ (1, 7, 8, 9, 10, 11, 12),
+ ]
+
+ def test_consts(self):
+ import gc
+ S = gc.GcCollectStepStats
+ assert S.STATE_SCANNING == 0
+ assert S.STATE_MARKING == 1
+ assert S.STATE_SWEEPING == 2
+ assert S.STATE_FINALIZING == 3
+ assert S.GC_STATES == ('SCANNING', 'MARKING', 'SWEEPING', 'FINALIZING')
+
+ def test_cumulative(self):
+ import gc
+ class MyHooks(object):
+
+ def __init__(self):
+ self.minors = []
+ self.steps = []
+
+ def on_gc_minor(self, stats):
+ self.minors.append((stats.count, stats.duration))
+
+ def on_gc_collect_step(self, stats):
+ self.steps.append((stats.count, stats.duration))
+
+ on_gc_collect = None
+
+ myhooks = MyHooks()
+ gc.hooks.set(myhooks)
+ self.fire_many()
+ assert myhooks.minors == [(2, 12)]
+ assert myhooks.steps == [(3, 42)]
+
+ def test_clear_queue(self):
+ import gc
+ class MyHooks(object):
+
+ def __init__(self):
+ self.lst = []
+
+ def on_gc_minor(self, stats):
+ self.lst.append('minor')
+
+ def on_gc_collect_step(self, stats):
+ self.lst.append('step')
+
+ def on_gc_collect(self, stats):
+ self.lst.append('collect')
+
+ myhooks = MyHooks()
+ gc.hooks.set(myhooks)
+ self.fire_many()
+ assert myhooks.lst == ['minor', 'step', 'collect']
+ myhooks.lst[:] = []
+ self.fire_gc_minor(0, 0, 0)
+ assert myhooks.lst == ['minor']
+ gc.hooks.reset()
+ assert gc.hooks.on_gc_minor is None
+ assert gc.hooks.on_gc_collect_step is None
+ assert gc.hooks.on_gc_collect is None
diff --git a/pypy/module/gc/test/test_ztranslation.py b/pypy/module/gc/test/test_ztranslation.py
--- a/pypy/module/gc/test/test_ztranslation.py
+++ b/pypy/module/gc/test/test_ztranslation.py
@@ -1,4 +1,9 @@
from pypy.objspace.fake.checkmodule import checkmodule
def test_checkmodule():
- checkmodule('gc')
+ # we need to ignore GcCollectStepStats, else checkmodule fails. I think
+ # this happens because W_GcCollectStepStats.__init__ is only called from
+ # GcCollectStepHookAction.perform() and the fake objspace doesn't know
+ # about those: so, perform() is never annotated and the annotator thinks
+ # W_GcCollectStepStats has no attributes
+ checkmodule('gc', ignore=['GcCollectStepStats'])
diff --git a/pypy/objspace/fake/checkmodule.py b/pypy/objspace/fake/checkmodule.py
--- a/pypy/objspace/fake/checkmodule.py
+++ b/pypy/objspace/fake/checkmodule.py
@@ -4,6 +4,7 @@
def checkmodule(*modnames, **kwds):
translate_startup = kwds.pop('translate_startup', True)
+ ignore = set(kwds.pop('ignore', ()))
assert not kwds
config = get_pypy_config(translating=True)
space = FakeObjSpace(config)
@@ -17,6 +18,8 @@
module.init(space)
modules.append(module)
for name in module.loaders:
+ if name in ignore:
+ continue
seeobj_w.append(module._load_lazily(space, name))
if hasattr(module, 'submodules'):
for cls in module.submodules.itervalues():
diff --git a/rpython/jit/codewriter/jtransform.py b/rpython/jit/codewriter/jtransform.py
--- a/rpython/jit/codewriter/jtransform.py
+++ b/rpython/jit/codewriter/jtransform.py
@@ -2164,6 +2164,11 @@
oopspecindex=EffectInfo.OS_MATH_READ_TIMESTAMP,
extraeffect=EffectInfo.EF_CANNOT_RAISE)
+ def rewrite_op_ll_get_timestamp_unit(self, op):
+ op1 = self.prepare_builtin_call(op, "ll_get_timestamp_unit", [])
+ return self.handle_residual_call(op1,
+ extraeffect=EffectInfo.EF_CANNOT_RAISE)
+
def rewrite_op_jit_force_quasi_immutable(self, op):
v_inst, c_fieldname = op.args
descr1 = self.cpu.fielddescrof(v_inst.concretetype.TO,
diff --git a/rpython/jit/codewriter/support.py b/rpython/jit/codewriter/support.py
--- a/rpython/jit/codewriter/support.py
+++ b/rpython/jit/codewriter/support.py
@@ -285,6 +285,9 @@
from rpython.rlib import rtimer
return rtimer.read_timestamp()
+def _ll_0_ll_get_timestamp_unit():
+ from rpython.rlib import rtimer
+ return rtimer.get_timestamp_unit()
# math support
# ------------
diff --git a/rpython/jit/metainterp/test/test_ajit.py b/rpython/jit/metainterp/test/test_ajit.py
--- a/rpython/jit/metainterp/test/test_ajit.py
+++ b/rpython/jit/metainterp/test/test_ajit.py
@@ -2577,6 +2577,14 @@
res = self.interp_operations(f, [])
assert res
+ def test_get_timestamp_unit(self):
+ import time
+ from rpython.rlib import rtimer
+ def f():
+ return rtimer.get_timestamp_unit()
+ unit = self.interp_operations(f, [])
+ assert unit == rtimer.UNIT_NS
+
def test_bug688_multiple_immutable_fields(self):
myjitdriver = JitDriver(greens=[], reds=['counter','context'])
diff --git a/rpython/memory/gc/base.py b/rpython/memory/gc/base.py
--- a/rpython/memory/gc/base.py
+++ b/rpython/memory/gc/base.py
@@ -5,6 +5,7 @@
from rpython.memory.support import DEFAULT_CHUNK_SIZE
from rpython.memory.support import get_address_stack, get_address_deque
from rpython.memory.support import AddressDict, null_address_dict
+from rpython.memory.gc.hook import GcHooks
from rpython.rtyper.lltypesystem.llmemory import NULL, raw_malloc_usage
from rpython.rtyper.annlowlevel import cast_adr_to_nongc_instance
@@ -25,7 +26,7 @@
_totalroots_rpy = 0 # for inspector.py
def __init__(self, config, chunk_size=DEFAULT_CHUNK_SIZE,
- translated_to_c=True):
+ translated_to_c=True, hooks=None):
self.gcheaderbuilder = GCHeaderBuilder(self.HDR)
self.AddressStack = get_address_stack(chunk_size)
self.AddressDeque = get_address_deque(chunk_size)
@@ -34,6 +35,9 @@
self.config = config
assert isinstance(translated_to_c, bool)
self.translated_to_c = translated_to_c
+ if hooks is None:
+ hooks = GcHooks() # the default hooks are empty
+ self.hooks = hooks
def setup(self):
# all runtime mutable values' setup should happen here
diff --git a/rpython/memory/gc/hook.py b/rpython/memory/gc/hook.py
new file mode 100644
--- /dev/null
+++ b/rpython/memory/gc/hook.py
@@ -0,0 +1,70 @@
+from rpython.rlib import rgc
+
+# WARNING: at the moment of writing, gc hooks are implemented only for
+# incminimark. Please add calls to hooks to the other GCs if you need it.
+class GcHooks(object):
+ """
+ Base class to write your own GC hooks.
+
+ Subclasses are expected to override the on_* methods. Note that such
+ methods can do only simple stuff such as updating statistics and/or
+ setting a flag: in particular, they cannot do anything which can possibly
+ trigger a GC collection.
+ """
+
+ def is_gc_minor_enabled(self):
+ return False
+
+ def is_gc_collect_step_enabled(self):
+ return False
+
+ def is_gc_collect_enabled(self):
+ return False
+
+ def on_gc_minor(self, duration, total_memory_used, pinned_objects):
+ """
+ Called after a minor collection
+ """
+
+ def on_gc_collect_step(self, duration, oldstate, newstate):
+ """
+ Called after each individual step of a major collection, in case the GC is
+ incremental.
+
+ ``oldstate`` and ``newstate`` are integers which indicate the GC
+ state; for incminimark, see incminimark.STATE_* and
+ incminimark.GC_STATES.
+ """
+
+
+ def on_gc_collect(self, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ """
+ Called after a major collection is fully done
+ """
+
+ # the fire_* methods are meant to be called from the GC are should NOT be
+ # overridden
+
+ @rgc.no_collect
+ def fire_gc_minor(self, duration, total_memory_used, pinned_objects):
+ if self.is_gc_minor_enabled():
+ self.on_gc_minor(duration, total_memory_used, pinned_objects)
+
+ @rgc.no_collect
+ def fire_gc_collect_step(self, duration, oldstate, newstate):
+ if self.is_gc_collect_step_enabled():
+ self.on_gc_collect_step(duration, oldstate, newstate)
+
+ @rgc.no_collect
+ def fire_gc_collect(self, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ if self.is_gc_collect_enabled():
+ self.on_gc_collect(num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after)
diff --git a/rpython/memory/gc/incminimark.py b/rpython/memory/gc/incminimark.py
--- a/rpython/memory/gc/incminimark.py
+++ b/rpython/memory/gc/incminimark.py
@@ -73,6 +73,7 @@
from rpython.rlib.debug import ll_assert, debug_print, debug_start, debug_stop
from rpython.rlib.objectmodel import specialize
from rpython.rlib import rgc
+from rpython.rlib.rtimer import read_timestamp
from rpython.memory.gc.minimarkpage import out_of_memory
#
@@ -1643,6 +1644,7 @@
"""Perform a minor collection: find the objects from the nursery
that remain alive and move them out."""
#
+ start = read_timestamp()
debug_start("gc-minor")
#
# All nursery barriers are invalid from this point on. They
@@ -1830,8 +1832,8 @@
# from the nursery that we just moved out.
self.size_objects_made_old += r_uint(self.nursery_surviving_size)
#
- debug_print("minor collect, total memory used:",
- self.get_total_memory_used())
+ total_memory_used = self.get_total_memory_used()
+ debug_print("minor collect, total memory used:", total_memory_used)
debug_print("number of pinned objects:",
self.pinned_objects_in_nursery)
if self.DEBUG >= 2:
@@ -1840,6 +1842,11 @@
self.root_walker.finished_minor_collection()
#
debug_stop("gc-minor")
+ duration = read_timestamp() - start
+ self.hooks.fire_gc_minor(
+ duration=duration,
+ total_memory_used=total_memory_used,
+ pinned_objects=self.pinned_objects_in_nursery)
def _reset_flag_old_objects_pointing_to_pinned(self, obj, ignore):
ll_assert(self.header(obj).tid & GCFLAG_PINNED_OBJECT_PARENT_KNOWN != 0,
@@ -2241,7 +2248,9 @@
# Note - minor collections seem fast enough so that one
# is done before every major collection step
def major_collection_step(self, reserving_size=0):
+ start = read_timestamp()
debug_start("gc-collect-step")
+ oldstate = self.gc_state
debug_print("starting gc state: ", GC_STATES[self.gc_state])
# Debugging checks
if self.pinned_objects_in_nursery == 0:
@@ -2421,6 +2430,13 @@
self.stat_rawmalloced_total_size, " => ",
self.rawmalloced_total_size)
debug_stop("gc-collect-done")
+ self.hooks.fire_gc_collect(
+ num_major_collects=self.num_major_collects,
+ arenas_count_before=self.stat_ac_arenas_count,
+ arenas_count_after=self.ac.arenas_count,
+ arenas_bytes=self.ac.total_memory_used,
+ rawmalloc_bytes_before=self.stat_rawmalloced_total_size,
+ rawmalloc_bytes_after=self.rawmalloced_total_size)
#
# Set the threshold for the next major collection to be when we
# have allocated 'major_collection_threshold' times more than
@@ -2472,6 +2488,11 @@
debug_print("stopping, now in gc state: ", GC_STATES[self.gc_state])
debug_stop("gc-collect-step")
+ duration = read_timestamp() - start
+ self.hooks.fire_gc_collect_step(
+ duration=duration,
+ oldstate=oldstate,
+ newstate=self.gc_state)
def _sweep_old_objects_pointing_to_pinned(self, obj, new_list):
if self.header(obj).tid & GCFLAG_VISITED:
diff --git a/rpython/memory/gc/test/test_direct.py b/rpython/memory/gc/test/test_direct.py
--- a/rpython/memory/gc/test/test_direct.py
+++ b/rpython/memory/gc/test/test_direct.py
@@ -70,6 +70,9 @@
class BaseDirectGCTest(object):
GC_PARAMS = {}
+ def get_extra_gc_params(self):
+ return {}
+
def setup_method(self, meth):
from rpython.config.translationoption import get_combined_translation_config
config = get_combined_translation_config(translating=True).translation
@@ -78,6 +81,7 @@
if hasattr(meth, 'GC_PARAMS'):
GC_PARAMS.update(meth.GC_PARAMS)
GC_PARAMS['translated_to_c'] = False
+ GC_PARAMS.update(self.get_extra_gc_params())
self.gc = self.GCClass(config, **GC_PARAMS)
self.gc.DEBUG = True
self.rootwalker = DirectRootWalker(self)
diff --git a/rpython/memory/gc/test/test_hook.py b/rpython/memory/gc/test/test_hook.py
new file mode 100644
--- /dev/null
+++ b/rpython/memory/gc/test/test_hook.py
@@ -0,0 +1,125 @@
+from rpython.rtyper.lltypesystem import lltype, llmemory
+from rpython.memory.gc.hook import GcHooks
+from rpython.memory.gc.test.test_direct import BaseDirectGCTest, S
+
+
+class MyGcHooks(GcHooks):
+
+ def __init__(self):
+ GcHooks.__init__(self)
+ self._gc_minor_enabled = False
+ self._gc_collect_step_enabled = False
+ self._gc_collect_enabled = False
+ self.reset()
+
+ def is_gc_minor_enabled(self):
+ return self._gc_minor_enabled
+
+ def is_gc_collect_step_enabled(self):
+ return self._gc_collect_step_enabled
+
+ def is_gc_collect_enabled(self):
+ return self._gc_collect_enabled
+
+ def reset(self):
+ self.minors = []
+ self.steps = []
+ self.collects = []
+ self.durations = []
+
+ def on_gc_minor(self, duration, total_memory_used, pinned_objects):
+ self.durations.append(duration)
+ self.minors.append({
+ 'total_memory_used': total_memory_used,
+ 'pinned_objects': pinned_objects})
+
+ def on_gc_collect_step(self, duration, oldstate, newstate):
+ self.durations.append(duration)
+ self.steps.append({
+ 'oldstate': oldstate,
+ 'newstate': newstate})
+
+ def on_gc_collect(self, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ self.collects.append({
+ 'num_major_collects': num_major_collects,
+ 'arenas_count_before': arenas_count_before,
+ 'arenas_count_after': arenas_count_after,
+ 'arenas_bytes': arenas_bytes,
+ 'rawmalloc_bytes_before': rawmalloc_bytes_before,
+ 'rawmalloc_bytes_after': rawmalloc_bytes_after})
+
+
+class TestIncMiniMarkHooks(BaseDirectGCTest):
+ from rpython.memory.gc.incminimark import IncrementalMiniMarkGC as GCClass
+
+ def get_extra_gc_params(self):
+ return {'hooks': MyGcHooks()}
+
+ def setup_method(self, m):
+ BaseDirectGCTest.setup_method(self, m)
+ size = llmemory.sizeof(S) + self.gc.gcheaderbuilder.size_gc_header
+ self.size_of_S = llmemory.raw_malloc_usage(size)
+
+ def test_on_gc_minor(self):
+ self.gc.hooks._gc_minor_enabled = True
+ self.malloc(S)
+ self.gc._minor_collection()
+ assert self.gc.hooks.minors == [
+ {'total_memory_used': 0, 'pinned_objects': 0}
+ ]
+ assert self.gc.hooks.durations[0] > 0
+ self.gc.hooks.reset()
+ #
+ # these objects survive, so the total_memory_used is > 0
+ self.stackroots.append(self.malloc(S))
+ self.stackroots.append(self.malloc(S))
+ self.gc._minor_collection()
+ assert self.gc.hooks.minors == [
+ {'total_memory_used': self.size_of_S*2, 'pinned_objects': 0}
+ ]
+
+ def test_on_gc_collect(self):
+ from rpython.memory.gc import incminimark as m
+ self.gc.hooks._gc_collect_step_enabled = True
+ self.gc.hooks._gc_collect_enabled = True
+ self.malloc(S)
+ self.gc.collect()
+ assert self.gc.hooks.steps == [
+ {'oldstate': m.STATE_SCANNING, 'newstate': m.STATE_MARKING},
+ {'oldstate': m.STATE_MARKING, 'newstate': m.STATE_SWEEPING},
+ {'oldstate': m.STATE_SWEEPING, 'newstate': m.STATE_FINALIZING},
+ {'oldstate': m.STATE_FINALIZING, 'newstate': m.STATE_SCANNING}
+ ]
+ assert self.gc.hooks.collects == [
+ {'num_major_collects': 1,
+ 'arenas_count_before': 0,
+ 'arenas_count_after': 0,
+ 'arenas_bytes': 0,
+ 'rawmalloc_bytes_after': 0,
+ 'rawmalloc_bytes_before': 0}
+ ]
+ assert len(self.gc.hooks.durations) == 4 # 4 steps
+ for d in self.gc.hooks.durations:
+ assert d > 0
+ self.gc.hooks.reset()
+ #
+ self.stackroots.append(self.malloc(S))
+ self.gc.collect()
+ assert self.gc.hooks.collects == [
+ {'num_major_collects': 2,
+ 'arenas_count_before': 1,
+ 'arenas_count_after': 1,
+ 'arenas_bytes': self.size_of_S,
+ 'rawmalloc_bytes_after': 0,
+ 'rawmalloc_bytes_before': 0}
+ ]
+
+ def test_hook_disabled(self):
+ self.gc._minor_collection()
+ self.gc.collect()
+ assert self.gc.hooks.minors == []
+ assert self.gc.hooks.steps == []
+ assert self.gc.hooks.collects == []
diff --git a/rpython/memory/gctransform/framework.py b/rpython/memory/gctransform/framework.py
--- a/rpython/memory/gctransform/framework.py
+++ b/rpython/memory/gctransform/framework.py
@@ -116,7 +116,7 @@
class BaseFrameworkGCTransformer(GCTransformer):
root_stack_depth = None # for tests to override
- def __init__(self, translator):
+ def __init__(self, translator, gchooks=None):
from rpython.memory.gc.base import choose_gc_from_config
super(BaseFrameworkGCTransformer, self).__init__(translator,
@@ -162,7 +162,8 @@
self.finalizer_queue_indexes = {}
self.finalizer_handlers = []
- gcdata.gc = GCClass(translator.config.translation, **GC_PARAMS)
+ gcdata.gc = GCClass(translator.config.translation, hooks=gchooks,
+ **GC_PARAMS)
root_walker = self.build_root_walker()
root_walker.finished_minor_collection_func = finished_minor_collection
self.root_walker = root_walker
diff --git a/rpython/memory/test/test_transformed_gc.py b/rpython/memory/test/test_transformed_gc.py
--- a/rpython/memory/test/test_transformed_gc.py
+++ b/rpython/memory/test/test_transformed_gc.py
@@ -14,7 +14,9 @@
from rpython.conftest import option
from rpython.rlib.rstring import StringBuilder
from rpython.rlib.rarithmetic import LONG_BIT
+from rpython.rlib.nonconst import NonConstant
from rpython.rtyper.rtyper import llinterp_backend
+from rpython.memory.gc.hook import GcHooks
WORD = LONG_BIT // 8
@@ -48,6 +50,7 @@
gcpolicy = None
GC_CAN_MOVE = False
taggedpointers = False
+ gchooks = None
def setup_class(cls):
cls.marker = lltype.malloc(rffi.CArray(lltype.Signed), 1,
@@ -112,7 +115,8 @@
fixup(t)
cbuild = CStandaloneBuilder(t, entrypoint, config=t.config,
- gcpolicy=cls.gcpolicy)
+ gcpolicy=cls.gcpolicy,
+ gchooks=cls.gchooks)
cbuild.make_entrypoint_wrapper = False
db = cbuild.build_database()
entrypointptr = cbuild.getentrypointptr()
@@ -1388,6 +1392,48 @@
assert res([]) == 0
+class GcHooksStats(object):
+ minors = 0
+ steps = 0
+ collects = 0
+
+ def reset(self):
+ # the NonConstant are needed so that the annotator annotates the
+ # fields as a generic SomeInteger(), instead of a constant 0. A call
+ # to this method MUST be seen during normal annotation, else the class
+ # is annotated only during GC transform, when it's too late
+ self.minors = NonConstant(0)
+ self.steps = NonConstant(0)
+ self.collects = NonConstant(0)
+
+
+class MyGcHooks(GcHooks):
+
+ def __init__(self, stats=None):
+ self.stats = stats or GcHooksStats()
+
+ def is_gc_minor_enabled(self):
+ return True
+
+ def is_gc_collect_step_enabled(self):
+ return True
+
+ def is_gc_collect_enabled(self):
+ return True
+
+ def on_gc_minor(self, duration, total_memory_used, pinned_objects):
+ self.stats.minors += 1
+
+ def on_gc_collect_step(self, duration, oldstate, newstate):
+ self.stats.steps += 1
+
+ def on_gc_collect(self, num_major_collects,
+ arenas_count_before, arenas_count_after,
+ arenas_bytes, rawmalloc_bytes_before,
+ rawmalloc_bytes_after):
+ self.stats.collects += 1
+
+
class TestIncrementalMiniMarkGC(TestMiniMarkGC):
gcname = "incminimark"
@@ -1405,6 +1451,8 @@
}
root_stack_depth = 200
+ gchooks = MyGcHooks()
+
def define_malloc_array_of_gcptr(self):
S = lltype.GcStruct('S', ('x', lltype.Signed))
A = lltype.GcArray(lltype.Ptr(S))
@@ -1438,6 +1486,34 @@
res = run([])
assert res
+ def define_gc_hooks(cls):
+ gchooks = cls.gchooks
+ # it is important that we fish .stats OUTSIDE f(); we cannot see
+ # gchooks from within RPython code
+ stats = gchooks.stats
+ def f():
+ stats.reset()
+ # trigger two major collections
+ llop.gc__collect(lltype.Void)
+ llop.gc__collect(lltype.Void)
+ return (10000 * stats.collects +
+ 100 * stats.steps +
+ 1 * stats.minors)
+ return f
+
+ def test_gc_hooks(self):
+ run = self.runner("gc_hooks")
+ count = run([])
+ collects, count = divmod(count, 10000)
+ steps, minors = divmod(count, 100)
+ #
+ # note: the following asserts are slightly fragile, as they assume
+ # that we do NOT run any minor collection apart the ones triggered by
+ # major_collection_step
+ assert collects == 2 # 2 collections, manually triggered
+ assert steps == 4 * collects # 4 steps for each major collection
+ assert minors == steps # one minor collection for each step
+
# ________________________________________________________________
# tagged pointers
diff --git a/rpython/rlib/rtimer.py b/rpython/rlib/rtimer.py
--- a/rpython/rlib/rtimer.py
+++ b/rpython/rlib/rtimer.py
@@ -7,6 +7,11 @@
_is_64_bit = r_uint.BITS > 32
+# unit of values returned by read_timestamp. Should be in sync with the ones
+# defined in translator/c/debug_print.h
+UNIT_TSC = 0
+UNIT_NS = 1 # nanoseconds
+UNIT_QUERY_PERFORMANCE_COUNTER = 2
def read_timestamp():
# Returns a longlong on 32-bit, and a regular int on 64-bit.
@@ -17,6 +22,11 @@
else:
return longlongmask(x)
+def get_timestamp_unit():
+ # an unit which is as arbitrary as the way we build the result of
+ # read_timestamp :)
+ return UNIT_NS
+
class ReadTimestampEntry(ExtRegistryEntry):
_about_ = read_timestamp
@@ -35,3 +45,15 @@
else:
resulttype = rffi.LONGLONG
return hop.genop("ll_read_timestamp", [], resulttype=resulttype)
+
+
+class ReadTimestampEntry(ExtRegistryEntry):
+ _about_ = get_timestamp_unit
+
+ def compute_result_annotation(self):
+ from rpython.annotator.model import SomeInteger
+ return SomeInteger(nonneg=True)
+
+ def specialize_call(self, hop):
+ hop.exception_cannot_occur()
+ return hop.genop("ll_get_timestamp_unit", [], resulttype=lltype.Signed)
diff --git a/rpython/rlib/test/test_rtimer.py b/rpython/rlib/test/test_rtimer.py
--- a/rpython/rlib/test/test_rtimer.py
+++ b/rpython/rlib/test/test_rtimer.py
@@ -1,28 +1,56 @@
import time
-
-from rpython.rlib.rtimer import read_timestamp
+import platform
+from rpython.rlib import rtimer
from rpython.rtyper.test.test_llinterp import interpret
from rpython.translator.c.test.test_genc import compile
-def timer():
- t1 = read_timestamp()
- start = time.time()
- while time.time() - start < 0.1:
- # busy wait
- pass
- t2 = read_timestamp()
- return t2 - t1
+class TestTimer(object):
-def test_timer():
- diff = timer()
- # We're counting ticks, verify they look correct
- assert diff > 1000
+ @staticmethod
+ def timer():
+ t1 = rtimer.read_timestamp()
+ start = time.time()
+ while time.time() - start < 0.1:
+ # busy wait
+ pass
+ t2 = rtimer.read_timestamp()
+ return t2 - t1
-def test_annotation():
- diff = interpret(timer, [])
- assert diff > 1000
+ def test_direct(self):
+ diff = self.timer()
+ # We're counting ticks, verify they look correct
+ assert diff > 1000
-def test_compile_c():
- function = compile(timer, [])
- diff = function()
- assert diff > 1000
\ No newline at end of file
+ def test_annotation(self):
+ diff = interpret(self.timer, [])
+ assert diff > 1000
+
+ def test_compile_c(self):
+ function = compile(self.timer, [])
+ diff = function()
+ assert diff > 1000
+
+
+class TestGetUnit(object):
+
+ @staticmethod
+ def get_unit():
+ return rtimer.get_timestamp_unit()
+
+ def test_direct(self):
+ unit = self.get_unit()
+ assert unit == rtimer.UNIT_NS
+
+ def test_annotation(self):
+ unit = interpret(self.get_unit, [])
+ assert unit == rtimer.UNIT_NS
+
+ def test_compile_c(self):
+ function = compile(self.get_unit, [])
+ unit = function()
+ if platform.processor() in ('x86', 'x86_64'):
+ assert unit == rtimer.UNIT_TSC
+ else:
+ assert unit in (rtimer.UNIT_TSC,
+ rtimer.UNIT_NS,
+ rtimer.UNIT_QUERY_PERFORMANCE_COUNTER)
diff --git a/rpython/rtyper/lltypesystem/lloperation.py b/rpython/rtyper/lltypesystem/lloperation.py
--- a/rpython/rtyper/lltypesystem/lloperation.py
+++ b/rpython/rtyper/lltypesystem/lloperation.py
@@ -445,6 +445,7 @@
'get_write_barrier_from_array_failing_case': LLOp(sideeffects=False),
'gc_get_type_info_group': LLOp(sideeffects=False),
'll_read_timestamp': LLOp(canrun=True),
+ 'll_get_timestamp_unit': LLOp(canrun=True),
# __________ GC operations __________
diff --git a/rpython/rtyper/lltypesystem/opimpl.py b/rpython/rtyper/lltypesystem/opimpl.py
--- a/rpython/rtyper/lltypesystem/opimpl.py
+++ b/rpython/rtyper/lltypesystem/opimpl.py
@@ -696,6 +696,10 @@
from rpython.rlib.rtimer import read_timestamp
return read_timestamp()
+def op_ll_get_timestamp_unit():
+ from rpython.rlib.rtimer import get_timestamp_unit
+ return get_timestamp_unit()
+
def op_debug_fatalerror(ll_msg):
from rpython.rtyper.lltypesystem import lltype, rstr
from rpython.rtyper.llinterp import LLFatalError
diff --git a/rpython/translator/c/database.py b/rpython/translator/c/database.py
--- a/rpython/translator/c/database.py
+++ b/rpython/translator/c/database.py
@@ -29,6 +29,7 @@
def __init__(self, translator=None, standalone=False,
gcpolicyclass=None,
+ gchooks=None,
exctransformer=None,
thread_enabled=False,
sandbox=False):
@@ -56,7 +57,7 @@
self.namespace = CNameManager()
if translator is not None:
- self.gctransformer = self.gcpolicy.gettransformer(translator)
+ self.gctransformer = self.gcpolicy.gettransformer(translator, gchooks)
self.completed = False
self.instrument_ncounter = 0
diff --git a/rpython/translator/c/gc.py b/rpython/translator/c/gc.py
--- a/rpython/translator/c/gc.py
+++ b/rpython/translator/c/gc.py
@@ -94,7 +94,7 @@
class RefcountingGcPolicy(BasicGcPolicy):
- def gettransformer(self, translator):
+ def gettransformer(self, translator, gchooks):
from rpython.memory.gctransform import refcounting
return refcounting.RefcountingGCTransformer(translator)
@@ -175,7 +175,7 @@
class BoehmGcPolicy(BasicGcPolicy):
- def gettransformer(self, translator):
+ def gettransformer(self, translator, gchooks):
from rpython.memory.gctransform import boehm
return boehm.BoehmGCTransformer(translator)
@@ -302,9 +302,9 @@
class BasicFrameworkGcPolicy(BasicGcPolicy):
- def gettransformer(self, translator):
+ def gettransformer(self, translator, gchooks):
if hasattr(self, 'transformerclass'): # for rpython/memory tests
- return self.transformerclass(translator)
+ return self.transformerclass(translator, gchooks=gchooks)
raise NotImplementedError
def struct_setup(self, structdefnode, rtti):
@@ -439,9 +439,9 @@
class ShadowStackFrameworkGcPolicy(BasicFrameworkGcPolicy):
- def gettransformer(self, translator):
+ def gettransformer(self, translator, gchooks):
from rpython.memory.gctransform import shadowstack
- return shadowstack.ShadowStackFrameworkGCTransformer(translator)
+ return shadowstack.ShadowStackFrameworkGCTransformer(translator, gchooks)
def enter_roots_frame(self, funcgen, (c_gcdata, c_numcolors)):
numcolors = c_numcolors.value
@@ -484,9 +484,9 @@
class AsmGcRootFrameworkGcPolicy(BasicFrameworkGcPolicy):
- def gettransformer(self, translator):
+ def gettransformer(self, translator, gchooks):
from rpython.memory.gctransform import asmgcroot
- return asmgcroot.AsmGcRootFrameworkGCTransformer(translator)
+ return asmgcroot.AsmGcRootFrameworkGCTransformer(translator, gchooks)
def GC_KEEPALIVE(self, funcgen, v):
return 'pypy_asm_keepalive(%s);' % funcgen.expr(v)
diff --git a/rpython/translator/c/genc.py b/rpython/translator/c/genc.py
--- a/rpython/translator/c/genc.py
+++ b/rpython/translator/c/genc.py
@@ -64,13 +64,14 @@
split = False
def __init__(self, translator, entrypoint, config, gcpolicy=None,
- secondary_entrypoints=()):
+ gchooks=None, secondary_entrypoints=()):
self.translator = translator
self.entrypoint = entrypoint
self.entrypoint_name = getattr(self.entrypoint, 'func_name', None)
self.originalentrypoint = entrypoint
self.config = config
self.gcpolicy = gcpolicy # for tests only, e.g. rpython/memory/
+ self.gchooks = gchooks
self.eci = self.get_eci()
self.secondary_entrypoints = secondary_entrypoints
@@ -91,6 +92,7 @@
exctransformer = translator.getexceptiontransformer()
db = LowLevelDatabase(translator, standalone=self.standalone,
gcpolicyclass=gcpolicyclass,
+ gchooks=self.gchooks,
exctransformer=exctransformer,
thread_enabled=self.config.translation.thread,
sandbox=self.config.translation.sandbox)
diff --git a/rpython/translator/c/src/asm_gcc_x86.h b/rpython/translator/c/src/asm_gcc_x86.h
--- a/rpython/translator/c/src/asm_gcc_x86.h
+++ b/rpython/translator/c/src/asm_gcc_x86.h
@@ -70,6 +70,7 @@
// lfence
// I don't know how important it is, comment talks about time warps
+#define READ_TIMESTAMP_UNIT TIMESTAMP_UNIT_TSC
#ifndef PYPY_CPU_HAS_STANDARD_PRECISION
/* On x86-32, we have to use the following hacks to set and restore
diff --git a/rpython/translator/c/src/asm_gcc_x86_64.h b/rpython/translator/c/src/asm_gcc_x86_64.h
--- a/rpython/translator/c/src/asm_gcc_x86_64.h
+++ b/rpython/translator/c/src/asm_gcc_x86_64.h
@@ -7,5 +7,6 @@
val = (_rdx << 32) | _rax; \
} while (0)
+#define READ_TIMESTAMP_UNIT TIMESTAMP_UNIT_TSC
#define RPy_YieldProcessor() asm("pause")
diff --git a/rpython/translator/c/src/asm_msvc.h b/rpython/translator/c/src/asm_msvc.h
--- a/rpython/translator/c/src/asm_msvc.h
+++ b/rpython/translator/c/src/asm_msvc.h
@@ -13,3 +13,4 @@
#include <intrin.h>
#pragma intrinsic(__rdtsc)
#define READ_TIMESTAMP(val) do { val = (long long)__rdtsc(); } while (0)
+#define READ_TIMESTAMP_UNIT TIMESTAMP_UNIT_TSC
diff --git a/rpython/translator/c/src/debug_print.h b/rpython/translator/c/src/debug_print.h
--- a/rpython/translator/c/src/debug_print.h
+++ b/rpython/translator/c/src/debug_print.h
@@ -51,6 +51,11 @@
RPY_EXTERN long pypy_have_debug_prints;
RPY_EXPORTED FILE *pypy_debug_file;
+/* these should be in sync with the values defined in rlib/rtimer.py */
+#define TIMESTAMP_UNIT_TSC 0
+#define TIMESTAMP_UNIT_NS 1
+#define TIMESTAMP_UNIT_QUERY_PERFORMANCE_COUNTER 2
+
#define OP_LL_READ_TIMESTAMP(val) READ_TIMESTAMP(val)
#include "src/asm.h"
@@ -62,11 +67,15 @@
# ifdef _WIN32
# define READ_TIMESTAMP(val) QueryPerformanceCounter((LARGE_INTEGER*)&(val))
+# define READ_TIMESTAMP_UNIT TIMESTAMP_UNIT_QUERY_PERFORMANCE_COUNTER
# else
RPY_EXTERN long long pypy_read_timestamp(void);
# define READ_TIMESTAMP(val) (val) = pypy_read_timestamp()
+# define READ_TIMESTAMP_UNIT TIMESTAMP_UNIT_NS
# endif
#endif
+
+#define OP_LL_GET_TIMESTAMP_UNIT(res) res = READ_TIMESTAMP_UNIT
diff --git a/rpython/translator/driver.py b/rpython/translator/driver.py
--- a/rpython/translator/driver.py
+++ b/rpython/translator/driver.py
@@ -413,11 +413,13 @@
translator.frozen = True
standalone = self.standalone
+ get_gchooks = self.extra.get('get_gchooks', lambda: None)
+ gchooks = get_gchooks()
if standalone:
from rpython.translator.c.genc import CStandaloneBuilder
cbuilder = CStandaloneBuilder(self.translator, self.entry_point,
- config=self.config,
+ config=self.config, gchooks=gchooks,
secondary_entrypoints=
self.secondary_entrypoints + annotated_jit_entrypoints)
else:
@@ -426,7 +428,8 @@
cbuilder = CLibraryBuilder(self.translator, self.entry_point,
functions=functions,
name='libtesting',
- config=self.config)
+ config=self.config,
+ gchooks=gchooks)
if not standalone: # xxx more messy
cbuilder.modulename = self.extmod_name
database = cbuilder.build_database()
diff --git a/rpython/translator/goal/targetgcbench.py b/rpython/translator/goal/targetgcbench.py
--- a/rpython/translator/goal/targetgcbench.py
+++ b/rpython/translator/goal/targetgcbench.py
@@ -1,16 +1,25 @@
from rpython.translator.goal import gcbench
+from rpython.memory.test.test_transformed_gc import MyGcHooks, GcHooksStats
# _____ Define and setup target ___
+GC_HOOKS_STATS = GcHooksStats()
+
+def entry_point(argv):
+ GC_HOOKS_STATS.reset()
+ ret = gcbench.entry_point(argv)
+ minors = GC_HOOKS_STATS.minors
+ steps = GC_HOOKS_STATS.steps
+ collects = GC_HOOKS_STATS.collects
+ print 'GC hooks statistics'
+ print ' gc-minor: ', minors
+ print ' gc-collect-step: ', steps
+ print ' gc-collect: ', collects
+ return ret
+
+def get_gchooks():
+ return MyGcHooks(GC_HOOKS_STATS)
+
def target(*args):
gcbench.ENABLE_THREADS = False # not RPython
- return gcbench.entry_point, None
-
-"""
-Why is this a stand-alone target?
-
-The above target specifies None as the argument types list.
-This is a case treated specially in the driver.py . If the list
-of input types is empty, it is meant to be a list of strings,
-actually implementing argv of the executable.
-"""
+ return entry_point, None
More information about the pypy-commit
mailing list