Author: Antonio Cuni <anto.c...@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 _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit