diff --git a/src/_vmprof.c b/src/_vmprof.c index f96feb20..c27e8cbf 100644 --- a/src/_vmprof.c +++ b/src/_vmprof.c @@ -163,7 +163,42 @@ static void cpyprof_code_dealloc(PyObject *co) Original_code_dealloc(co); } -static PyObject *enable_vmprof(PyObject* self, PyObject *args) + +static int pop(PyObject *kwargs, const char *kw, + const char *format, void *addr) +{ + PyObject *item = NULL; + if (kwargs == NULL) + return 1; /* nothing to do */ + + item = PyDict_GetItemString(kwargs, kw); + if (item == NULL || item == Py_None) + /* kw not there, or it's None, nothing to do */ + return 1; + + /* pop the kw and parse it */ + if (PyDict_DelItemString(kwargs, kw) != 0) + return 0; + if (!PyArg_Parse(item, format, addr)) + return 0; + return 1; +} + +/* signature: + enable(fd, interval, **options) + + options can contain arbitrary keys; the return value is a dictionary + containing all options which were not understood/supported by the backend + (or None if you didn't pass any keyword arg) + + Currently, it supports these options: + memory + lines + native + real_time +*/ +static PyObject *enable_vmprof(PyObject *self, PyObject *args, + PyObject *kwargs) { int fd; int memory = 0; @@ -173,10 +208,20 @@ static PyObject *enable_vmprof(PyObject* self, PyObject *args) double interval; char *p_error; - if (!PyArg_ParseTuple(args, "id|iiii", &fd, &interval, &memory, &lines, &native, &real_time)) { + if (!PyArg_ParseTuple(args, "id", &fd, &interval)) + return NULL; + if (!pop(kwargs, "lines", "i", &lines)) return NULL; - } +#ifdef VMPROF_UNIX + if (!pop(kwargs, "memory", "i", &memory)) + return NULL; + if (!pop(kwargs, "native", "i", &native)) + return NULL; + if (!pop(kwargs, "real_time", "i", &real_time)) + return NULL; +#endif + if (write(fd, NULL, 0) != 0) { PyErr_SetString(PyExc_ValueError, "file descriptor must be writeable"); return NULL; @@ -192,13 +237,6 @@ static PyObject *enable_vmprof(PyObject* self, PyObject *args) return NULL; } -#ifndef VMPROF_UNIX - if (real_time) { - PyErr_SetString(PyExc_ValueError, "real time profiling is only supported on Linux and MacOS"); - return NULL; - } -#endif - vmp_profile_lines(lines); if (!Original_code_dealloc) { @@ -219,9 +257,15 @@ static PyObject *enable_vmprof(PyObject* self, PyObject *args) vmprof_set_enabled(1); - Py_RETURN_NONE; + if (kwargs == NULL) + Py_RETURN_NONE; + else { + Py_INCREF(kwargs); + return kwargs; + } } + static PyObject * vmp_is_enabled(PyObject *module, PyObject *noargs) { if (vmprof_is_enabled()) { Py_RETURN_TRUE; @@ -422,7 +466,8 @@ remove_real_time_thread(PyObject *module, PyObject * noargs) { #endif static PyMethodDef VMProfMethods[] = { - {"enable", enable_vmprof, METH_VARARGS, "Enable profiling."}, + {"enable", (PyCFunction)enable_vmprof, METH_VARARGS | METH_KEYWORDS, + "Enable profiling."}, {"disable", disable_vmprof, METH_NOARGS, "Disable profiling."}, {"write_all_code_objects", write_all_code_objects, METH_O, "Write eagerly all the IDs of code objects"}, diff --git a/vmprof/__init__.py b/vmprof/__init__.py index a7fae211..adb4d739 100644 --- a/vmprof/__init__.py +++ b/vmprof/__init__.py @@ -1,5 +1,6 @@ import os import sys +import warnings try: from shutil import which except ImportError: @@ -14,6 +15,8 @@ from vmprof.stats import Stats from vmprof.profiler import Profiler, read_profile +class VMProfWarning(RuntimeWarning): + pass PY3 = sys.version_info[0] >= 3 IS_PYPY = '__pypy__' in sys.builtin_module_names @@ -32,7 +35,8 @@ def disable(): if hasattr(_vmprof, 'stop_sampling'): fileno = _vmprof.stop_sampling() if fileno >= 0: - # TODO does fileobj leak the fd? I dont think so, but need to check + # TODO does fileobj leak the fd? I dont think so, but need to + # check fileobj = FdWrapper(fileno) l = LogReaderDumpNative(fileobj, LogReaderState()) l.read_all() @@ -42,18 +46,10 @@ def disable(): except IOError as e: raise Exception("Error while writing profile: " + str(e)) -def _is_native_enabled(native): - if os.name == "nt": - if native: - raise ValueError("native profiling is only supported on Linux & Mac OS X") - native = False - else: - if native is None: - native = True - return native if IS_PYPY: - def enable(fileno, period=DEFAULT_PERIOD, memory=False, lines=False, native=None, real_time=False, warn=True): + def enable(fileno, period=DEFAULT_PERIOD, memory=False, lines=False, + native=None, real_time=False, warn=True): pypy_version_info = sys.pypy_version_info[:3] MAJOR = pypy_version_info[0] MINOR = pypy_version_info[1] @@ -61,11 +57,14 @@ def enable(fileno, period=DEFAULT_PERIOD, memory=False, lines=False, native=None if not isinstance(period, float): raise ValueError("You need to pass a float as an argument") if warn and pypy_version_info < (4, 1, 0): - raise Exception("PyPy <4.1 have various kinds of bugs, pass warn=False if you know what you're doing") + raise Exception("PyPy <4.1 have various kinds of bugs, " + "pass warn=False if you know what you're doing") if warn and memory: - print("Memory profiling is currently unsupported for PyPy. Running without memory statistics.") + print("Memory profiling is currently unsupported for PyPy. " + "Running without memory statistics.") if warn and lines: - print('Line profiling is currently unsupported for PyPy. Running without lines statistics.\n') + print("Line profiling is currently unsupported for PyPy. " + "Running without lines statistics.") native = _is_native_enabled(native) # if MAJOR >= 5 and MINOR >= 9 and PATCH >= 0: @@ -79,57 +78,82 @@ def enable(fileno, period=DEFAULT_PERIOD, memory=False, lines=False, native=None _vmprof.enable(fileno, period) else: # CPYTHON - def enable(fileno, period=DEFAULT_PERIOD, memory=False, lines=False, native=None, real_time=False): + + # note that default values are None: this way, we can detect whether the + # user passed an explicit value, and emit a warning if the backend does + # not support it + def enable(fileno, period=DEFAULT_PERIOD, memory=None, lines=None, + native=None, real_time=None): if not isinstance(period, float): - raise ValueError("You need to pass a float as an argument") - native = _is_native_enabled(native) - _vmprof.enable(fileno, period, memory, lines, native, real_time) + raise TypeError("'period' must be a float") + + unsupported_opts = _vmprof.enable(fileno, period, + memory=memory, + lines=lines, + native=native, + real_time=real_time) + if unsupported_opts: + optlist = [] + for key, value in unsupported_opts.items(): + if value is not None: + optlist.append(key) + if optlist: + optlist = ", ".join(sorted(optlist)) + msg = ("Unsupported option(s) on this platform/backed: %s" + % optlist) + warnings.warn(msg, VMProfWarning, stacklevel=2) def sample_stack_now(skip=0): - """ Helper utility mostly for tests, this is considered - private API. + """ + Helper utility mostly for tests, this is considered private API. - It will return a list of stack frames the python program currently - walked. + It will return a list of stack frames the python program currently + walked. """ stackframes = _vmprof.sample_stack_now(skip) assert isinstance(stackframes, list) return stackframes def resolve_addr(addr): - """ Private API, returns the symbol name of the given address. - Only considers linking symbols found by dladdr. + """ + Private API, returns the symbol name of the given address. + Only considers linking symbols found by dladdr. """ return _vmprof.resolve_addr(addr) def insert_real_time_thread(): - """ Inserts a thread into the list of threads to be sampled in real time mode. - When enabling real time mode, the caller thread is inserted automatically. - Returns the number of registered threads, or -1 if we can't insert thread. + """ + Inserts a thread into the list of threads to be sampled in real time mode. + When enabling real time mode, the caller thread is inserted automatically. + Returns the number of registered threads, or -1 if we can't insert thread. """ return _vmprof.insert_real_time_thread() def remove_real_time_thread(): - """ Removes a thread from the list of threads to be sampled in real time mode. - When disabling in real time mode, *all* threads are removed automatically. - Returns the number of registered threads, or -1 if we can't remove thread. + """ + Removes a thread from the list of threads to be sampled in real time mode. + When disabling in real time mode, *all* threads are removed automatically. + Returns the number of registered threads, or -1 if we can't remove thread. """ return _vmprof.remove_real_time_thread() def is_enabled(): - """ Indicates if vmprof has already been enabled for this process. - Returns True or False. None is returned if the state is unknown. + """ + Indicates if vmprof has already been enabled for this process. Returns + True or False. None is returned if the state is unknown. """ if hasattr(_vmprof, 'is_enabled'): return _vmprof.is_enabled() raise NotImplementedError("is_enabled is not implemented on this platform") def get_profile_path(): - """ Returns the absolute path for the file that is currently open. - None is returned if the backend implementation does not implement that function, - or profiling is not enabled. + """ + Returns the absolute path for the file that is currently open. None is + returned if the backend implementation does not implement that function, + or profiling is not enabled. """ if hasattr(_vmprof, 'get_profile_path'): return _vmprof.get_profile_path() - raise NotImplementedError("get_profile_path not implemented on this platform") + raise NotImplementedError("get_profile_path not implemented " + "on this platform") diff --git a/vmprof/profiler.py b/vmprof/profiler.py index 403abd5e..e43ed690 100644 --- a/vmprof/profiler.py +++ b/vmprof/profiler.py @@ -56,7 +56,8 @@ class Profiler(object): def __init__(self): self._lib_cache = {} - def measure(self, name=None, period=0.001, memory=False, native=False, real_time=False): + def measure(self, name=None, period=0.001, memory=None, native=None, + real_time=None): self.ctx = ProfilerContext(name, period, memory, native, real_time) return self.ctx diff --git a/vmprof/test/test__vmprof.py b/vmprof/test/test__vmprof.py new file mode 100644 index 00000000..53e0c69b --- /dev/null +++ b/vmprof/test/test__vmprof.py @@ -0,0 +1,43 @@ +import pytest +import sys +import _vmprof + +def enable_and_disable(fname, *args, **kwargs): + with fname.open('w+b') as f: + try: + if not kwargs: + # try hard to pass kwargs == NULL, instead of {} + return _vmprof.enable(f.fileno(), *args) + else: + return _vmprof.enable(f.fileno(), *args, **kwargs) + finally: + _vmprof.stop_sampling() + _vmprof.disable() + +@pytest.mark.skip +def test_enable(tmpdir): + prof = tmpdir.join('a.vmprof') + ret = enable_and_disable(prof, 0.004) + assert ret is None + +@pytest.mark.skip +def test_enable_options(tmpdir): + prof = tmpdir.join('a.vmprof') + ret = enable_and_disable(prof, 0.004, lines=True) + assert ret == {} + +@pytest.mark.skip +def test_enable_options_unix_only(tmpdir): + prof = tmpdir.join('a.vmprof') + ret = enable_and_disable(prof, 0.004, memory=True, native=True, + real_time=True) + if sys.platform == 'win32': + assert ret == {'memory': True, 'native': True, 'real_time': True} + else: + assert ret == {} + +@pytest.mark.skip +def test_enable_options_unknown(tmpdir): + prof = tmpdir.join('a.vmprof') + ret = enable_and_disable(prof, 0.004, foo=1, bar=2) + assert ret == {'foo': 1, 'bar': 2} diff --git a/vmprof/test/test_run.py b/vmprof/test/test_run.py index 29455978..e03090ca 100644 --- a/vmprof/test/test_run.py +++ b/vmprof/test/test_run.py @@ -67,12 +67,6 @@ def function_foo(): l = [a for a in xrange(COUNT)] return l -def function_bar(): - import time - for k in range(1000): - time.sleep(0.001) - return 1+1 - def function_bar(): return function_foo() @@ -132,6 +126,32 @@ def test_enable_disable(): d = dict(stats.top_profile()) assert d[foo_full_name] > 0 +def test_enable_warnings(monkeypatch, recwarn): + import _vmprof + # simulate a _vmprof backend which does NOT native and real_time (like + # e.g. the windows one) + def fake_enable(fileno, internval, **kwargs): + kwargs.pop('memory', None) + kwargs.pop('lines', None) + return kwargs + monkeypatch.setattr(_vmprof, 'enable', fake_enable) + + # check that if we do NOT specify any explit value, we don't get the + # warning + recwarn.clear() + vmprof.enable(0) + assert len(recwarn.list) == 0 + + # check that if we specify native and real_time, we get a warning + recwarn.clear() + vmprof.enable(0, native=True, real_time=True) + assert len(recwarn.list) == 1 + w = recwarn.pop(vmprof.VMProfWarning) + assert str(w.message) == ("Unsupported option(s) on this " + "platform/backed: native, real_time") + + + def test_start_end_time(): prof = vmprof.Profiler() before_profile = datetime.now() @@ -177,6 +197,9 @@ def test_nested_call(): else: assert foo_full_name in names t = stats.get_tree() + # XXX: this test if *VERY* fragile: if we are "too slow" to exit + # vmprof.enable(), vmprof takes a sample there and the loop crashes with a + # KeyError (which I have no clue what it means) while 'function_bar' not in t.name: t = t[''] assert len(t.children) == 1