Repository: numba/numba

Issue:
Make NRT stats counters optional, off by default
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/main/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

As discussed at last week's public Numba meeting, https://github.com/numba/numba/wiki/Minutes_2022_06_07. Code where:
* there are potentially quite a few allocations/deallocations in a function, often caused by array temporaries 

and
* the function is called by many native threads

results in a lot of pressure on the atomic stats counters defined here:
https://github.com/numba/numba/blob/080633aa333e69781888eee6521f5e2fa6751c75/numba/core/runtime/nrt.c#L53
and their use as is commented out here:
```diff
diff --git a/numba/core/runtime/nrt.c b/numba/core/runtime/nrt.c
index 3a65c9b..213251a 100644
--- a/numba/core/runtime/nrt.c
+++ b/numba/core/runtime/nrt.c
@@ -182,7 +182,7 @@ void NRT_MemInfo_init(NRT_MemInfo *mi,void *data, size_t size,
     mi->external_allocator = external_allocator;
     NRT_Debug(nrt_debug_print("NRT_MemInfo_init mi=%p external_allocator=%p\n", mi, external_allocator));
     /* Update stats */
-    TheMSys.atomic_inc(&TheMSys.stats_mi_alloc);
+//     TheMSys.atomic_inc(&TheMSys.stats_mi_alloc);
 }
 
 NRT_MemInfo *NRT_MemInfo_new(void *data, size_t size,
@@ -321,7 +321,7 @@ void NRT_dealloc(NRT_MemInfo *mi) {
     NRT_Debug(nrt_debug_print("NRT_dealloc meminfo: %p external_allocator: %p\n", mi, mi->external_allocator));
     if (mi->external_allocator) {
         mi->external_allocator->free(mi, mi->external_allocator->opaque_data);
-        TheMSys.atomic_inc(&TheMSys.stats_free);
+//         TheMSys.atomic_inc(&TheMSys.stats_free);
     } else {
         NRT_Free(mi);
     }
@@ -329,7 +329,7 @@ void NRT_dealloc(NRT_MemInfo *mi) {
 
 void NRT_MemInfo_destroy(NRT_MemInfo *mi) {
     NRT_dealloc(mi);
-    TheMSys.atomic_inc(&TheMSys.stats_mi_free);
+//     TheMSys.atomic_inc(&TheMSys.stats_mi_free);
 }
 
 void NRT_MemInfo_acquire(NRT_MemInfo *mi) {
@@ -472,7 +472,7 @@ void* NRT_Allocate_External(size_t size, NRT_ExternalAllocator *allocator) {
         ptr = TheMSys.allocator.malloc(size);
         NRT_Debug(nrt_debug_print("NRT_Allocate_External bytes=%zu ptr=%p\n", size, ptr));
     }
-    TheMSys.atomic_inc(&TheMSys.stats_alloc);
+//     TheMSys.atomic_inc(&TheMSys.stats_alloc);
     return ptr;
 }
 
@@ -486,7 +486,7 @@ void *NRT_Reallocate(void *ptr, size_t size) {
 void NRT_Free(void *ptr) {
     NRT_Debug(nrt_debug_print("NRT_Free %p\n", ptr));
     TheMSys.allocator.free(ptr);
-    TheMSys.atomic_inc(&TheMSys.stats_free);
+//     TheMSys.atomic_inc(&TheMSys.stats_free);
 }
```

I think this code that follows reproduces the situation described above (and at the public meeting) that applies pressure on the atomic counters (using the `count_muons` example as given at the public meeting):

```python
import ctypes
import numba
from numba import cfunc, carray, types, njit, prange
from numba.extending import intrinsic
import numpy as np
from timeit import default_timer


@cfunc(types.intp(types.CPointer(types.float32)))
def count_muons(ptr):
    arr = carray(ptr, 10)
    return np.count_nonzero((arr > 1.) & (np.abs(arr) < 7.) & (arr > 0.))


x = np.arange(10, dtype=np.float32)

count = count_muons(x.ctypes.data_as(ctypes.POINTER(ctypes.c_float)))
print("cfunc call", count)


@intrinsic
def addr_as_float32ptr(tyctx, addr):
    sig = types.CPointer(types.float32)(types.intp)

    def codegen(cgctx, builder, sig, llargs):
        ret = builder.inttoptr(llargs[0], cgctx.get_value_type(sig.return_type))
        return ret
    return sig, codegen


@njit
def liveness_sink(*args):
    # keeps things that are only referenced by pointers alive
    pass


@njit(parallel=True)
def foo(n):
    acc0 = acc1 = acc2 = acc3 = 0
    for i in prange(n):
        input_arr = np.arange(10).astype(np.float32)
        addr = input_arr.ctypes.data
        ptr = addr_as_float32ptr(addr)
        acc0 += count_muons(ptr)
        acc1 += count_muons(ptr)
        acc2 += count_muons(ptr)
        acc3 += count_muons(ptr)
        liveness_sink(input_arr)
    return acc0 + acc1 + acc2 + acc3


# make this bigger to run more arrays
n = 4000000

tstart = default_timer()
out = foo(n)
tend = default_timer()
print("jit call", out)
assert out == n * 5 * 4 # n loops with 5 things counted in 4 accumulators

print(f"Elapsed time: {tend-tstart}")
```

Without the patch above the result is:
```
Elapsed time: 19.056538556000305
```
but with the patch above (i.e. atomic counters are disabled)
```
Elapsed time: 6.221216844001901
```
It will probably be easier to fix this after https://github.com/numba/numba/pull/8106 is merged.

CC @jpivarski 




Discussion:
CC @eguiraud
Thank you @stuartarchibald and @jpivarski , and sorry for being so slow at opening issues! 
> Thank you @stuartarchibald and @jpivarski , and sorry for being so slow at opening issues!

No problem. I hope the above is a sufficiently good representation of the issue experienced?
Yes I think that's it. I'm available to try out patches in case that helps. 

And if I'm not mistaken @njriasan wanted to be pinged once an issue was open :)


> Yes I think that's it. I'm available to try out patches in case that helps.

Thanks for confirming and for volunteering to test, I'll ping you on the patch once it's written (it depends on https://github.com/numba/numba/pull/8124 and https://github.com/numba/numba/pull/8106).

> And if I'm not mistaken @njriasan wanted to be pinged once an issue was open :)

Ah yes, thanks for doing that!
Thanks for tagging me @eguiraud. As a followup on the discussion last one, one option considered was setting `numba.core.runtime.nrtdynmod._disable_atomicity = 1` so I decided to test that setting without the patch. On my machine (Mac M1) these are the timings I got

On main:
```python3
Elapsed time: 6.397735
```

With `numba.core.runtime.nrtdynmod._disable_atomicity = 1`
```python3
Elapsed time: 2.701489
```
Since this has parallel code I'm not sure if `_disable_atomicity` is safe for this example so maybe that test isn't helpful.
> Since this has parallel code I'm not sure if `_disable_atomicity` is safe for this example so maybe that test isn't helpful.

I think it's probably ok, the accumulators will be in their own separate arrays and the rest of the stuff will be in a gufunc and all local. IIRC doing `numba.core.runtime.nrtdynmod._disable_atomicity = 1` will essentially define the `nrt` atomic decrement and increment to be non-atomic, which impacts both the NRT reference counts and the statistics counters. They will still operate, but will just be non-atomic, which probably means the stats will be incorrect.
Does `_disable_atomicity` need to be set at jit time, at execution time or both? E.g. can I set `_disable_atomicity` before jitting code with `numba.cfunc` that I expect to not need the atomic counters and then reset it to it former value afterwards in order to leave user programs unaffected?
> Does `_disable_atomicity` need to be set at jit time, at execution time or both? E.g. can I set `_disable_atomicity` before jitting code with `numba.cfunc` that I expect to not need the atomic counters and then reset it to it former value afterwards in order to leave user programs unaffected?

That module global impacts the construction of the Numba runtime, which happens at the time the target context is intialised (it'll be the CPU compilation context in this case). This is typically when the first `@jit/@cfunc` decorated function is evaluated by the interpreter as those decorators instantiate a `numba.core.Dispatcher` object which sets up the target context as it initialises. This means that `_disable_atomicity` would need to be set logically ahead of the interpreter encountering any `@jit/@cfunc` decorated functions and would impact all future compilations.

I attempted to work around this by creating a new compilation target that inherits from the CPU but switches the atomicity off locally for that specific target, unfortunately it won't work because the Numba runtime is essentially a singleton.
I'm not sure the just disabling the atomic part of the counters is the right thing here. If they are not atomic, I think the values could easily be incorrect (one thread reads; the other increments; the first thread writes back its original value + 1) and if the values don't need to be correct, why have them at all?
And even if the counters are non-atomic, they should (as far as I understand the hardware issues here...) still lead to quite some performance loss, because the cache still has to be invalidated when multiple threads write to the same location (ARM is a bit different in how it does that I think, so the impact on M1 is maybe not as pronounced?)
If the counters should stay there in release mode, then maybe thread local counters would be better? But just disabling them when not in debug mode sounds like the right thing to me.
> I'm not sure the just disabling the atomic part of the counters is the right thing here. If they are not atomic, I think the values could easily be incorrect (one thread reads; the other increments; the first thread writes back its original value + 1) and if the values don't need to be correct, why have them at all? And even if the counters are non-atomic, they should (as far as I understand the hardware issues here...) still lead to quite some performance loss, because the cache still has to be invalidated when multiple threads write to the same location (ARM is a bit different in how it does that I think, so the impact on M1 is maybe not as pronounced?) If the counters should stay there in release mode, then maybe thread local counters would be better? But just disabling them when not in debug mode sounds like the right thing to me.

Thanks for your thoughts on this @aseyboldt. Some additional context from the public Numba meeting... the suggestion above is about seeing what sort of workarounds exist _right now_ in Numba 0.55.x and their relative impact. As noted the counters are entirely redundant unless you need to look at them, so it's fine for them to have a race condition, but they will still have a performance cost (cache invalidation etc). Therefore this suggestion might let some code run more quickly and will hopefully to some degree confirm that the issue is related to pressure on these atomics.

The longer term (probably 0.57 release) suggestion is to make the statistics counters off by default. I've only occasionally seen users looking at them and that's almost always in advanced cases where someone is debugging a reference leak, the largest user is probably Numba's own test suite! As a result, they simply do not need to be present in the standard runtime use case and I think there's general agreement that they should just be turned off unless needed e.g. debug/testing.

Resolving patch:
diff --git a/numba/core/config.py b/numba/core/config.py
--- a/numba/core/config.py
+++ b/numba/core/config.py
@@ -223,6 +223,9 @@ def optional_str(x):
         # Enable debug prints in nrtdynmod and use of "safe" API functions
         DEBUG_NRT = _readenv("NUMBA_DEBUG_NRT", int, 0)
 
+        # Enable NRT statistics counters
+        NRT_STATS = _readenv("NUMBA_NRT_STATS", int, 0)
+
         # How many recently deserialized functions to retain regardless
         # of external references
         FUNCTION_CACHE_SIZE = _readenv("NUMBA_FUNCTION_CACHE_SIZE", int, 128)
diff --git a/numba/core/runtime/nrt.py b/numba/core/runtime/nrt.py
--- a/numba/core/runtime/nrt.py
+++ b/numba/core/runtime/nrt.py
@@ -6,7 +6,7 @@
 
 from numba.core.compiler_lock import global_compiler_lock
 from numba.core.typing.typeof import typeof_impl
-from numba.core import types
+from numba.core import types, config
 from numba.core.runtime import _nrt_python as _nrt
 
 _nrt_mstats = namedtuple("nrt_mstats", ["alloc", "free", "mi_alloc", "mi_free"])
@@ -27,6 +27,10 @@ def initialize(self, ctx):
             # Already initialized
             return
 
+        # Switch stats on if the config requests them.
+        if config.NRT_STATS:
+            _nrt.memsys_enable_stats()
+
         # Register globals into the system
         for py_name in _nrt.c_helpers:
             if py_name.startswith("_"):


Test patch:
diff --git a/numba/tests/support.py b/numba/tests/support.py
--- a/numba/tests/support.py
+++ b/numba/tests/support.py
@@ -40,6 +40,7 @@
 from numba.core.runtime import rtsys
 from numba.np import numpy_support
 from numba.pycc.platform import _external_compiler_ok
+from numba.core.runtime import _nrt_python as _nrt
 
 
 try:
@@ -775,6 +776,16 @@ def capture_cache_log():
             yield out
 
 
+class EnableNRTStatsMixin(object):
+    """Mixin to enable the NRT statistics counters."""
+
+    def setUp(self):
+        _nrt.memsys_enable_stats()
+
+    def tearDown(self):
+        _nrt.memsys_disable_stats()
+
+
 class MemoryLeak(object):
 
     __enable_leak_check = True
@@ -803,16 +814,16 @@ def disable_leak_check(self):
         self.__enable_leak_check = False
 
 
-class MemoryLeakMixin(MemoryLeak):
+class MemoryLeakMixin(EnableNRTStatsMixin, MemoryLeak):
 
     def setUp(self):
         super(MemoryLeakMixin, self).setUp()
         self.memory_leak_setup()
 
     def tearDown(self):
-        super(MemoryLeakMixin, self).tearDown()
         gc.collect()
         self.memory_leak_teardown()
+        super(MemoryLeakMixin, self).tearDown()
 
 
 @contextlib.contextmanager
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -11,7 +11,7 @@
 from numba import jit, njit, typeof
 from numba.core import errors
 from numba.tests.support import (TestCase, tag, needs_lapack, needs_blas,
-                                 _is_armv7l)
+                                 _is_armv7l, EnableNRTStatsMixin)
 from .matmul_usecase import matmul_usecase
 import unittest
 
@@ -28,7 +28,7 @@ def vdot(a, b):
     return np.vdot(a, b)
 
 
-class TestProduct(TestCase):
+class TestProduct(EnableNRTStatsMixin, TestCase):
     """
     Tests for dot products.
     """
@@ -38,6 +38,7 @@ class TestProduct(TestCase):
     def setUp(self):
         # Collect leftovers from previous test cases before checking for leaks
         gc.collect()
+        super(TestProduct, self).setUp()
 
     def sample_vector(self, n, dtype):
         # Be careful to generate only exactly representable float values,
@@ -414,7 +415,7 @@ def kron_matrix(a, b):
     return np.kron(a, b)
 
 
-class TestLinalgBase(TestCase):
+class TestLinalgBase(EnableNRTStatsMixin, TestCase):
     """
     Provides setUp and common data/error modes for testing np.linalg functions.
     """
@@ -425,6 +426,7 @@ class TestLinalgBase(TestCase):
     def setUp(self):
         # Collect leftovers from previous test cases before checking for leaks
         gc.collect()
+        super(TestLinalgBase, self).setUp()
 
     def sample_vector(self, n, dtype):
         # Be careful to generate only exactly representable float values,
diff --git a/numba/tests/test_nrt.py b/numba/tests/test_nrt.py
--- a/numba/tests/test_nrt.py
+++ b/numba/tests/test_nrt.py
@@ -23,8 +23,9 @@
 import numba.core.typing.cffi_utils as cffi_support
 from numba.core.unsafe.nrt import NRT_get_api
 
-from numba.tests.support import (MemoryLeakMixin, TestCase, temp_directory,
-                                 import_dynamic, skip_if_32bit)
+from numba.tests.support import (EnableNRTStatsMixin, TestCase, temp_directory,
+                                 import_dynamic, skip_if_32bit,
+                                 run_in_subprocess)
 from numba.core.registry import cpu_target
 import unittest
 
@@ -304,7 +305,7 @@ def release_memory():
         self.assertLess(stat.size, N * 0.01)
 
 
-class TestNRTIssue(MemoryLeakMixin, TestCase):
+class TestNRTIssue(TestCase):
     def test_issue_with_refct_op_pruning(self):
         """
         GitHub Issue #1244 https://github.com/numba/numba/issues/1244
@@ -574,13 +575,13 @@ def foo(x):
 
 
 @unittest.skipUnless(cffi_support.SUPPORTED, "cffi required")
-class TestNrtExternalCFFI(MemoryLeakMixin, TestCase):
+class TestNrtExternalCFFI(EnableNRTStatsMixin, TestCase):
     """Testing the use of externally compiled C code that use NRT
     """
     def setUp(self):
         # initialize the NRT (in case the tests are run in isolation)
-        super(TestNrtExternalCFFI, self).setUp()
         cpu_target.target_context
+        super(TestNrtExternalCFFI, self).setUp()
 
     def compile_cffi_module(self, name, source, cdef):
         from cffi import FFI
@@ -692,5 +693,144 @@ def test_nrt_api():
         self.assertEqual(expect, got)
 
 
+class TestNrtStatistics(TestCase):
+
+    def setUp(self):
+        # Store the current stats state
+        self.__stats_state = _nrt_python.memsys_stats_enabled()
+
+    def tearDown(self):
+        # Set stats state back to whatever it was before the test ran
+        if self.__stats_state:
+            _nrt_python.memsys_enable_stats()
+        else:
+            _nrt_python.memsys_disable_stats()
+
+    def test_stats_env_var_explicit_on(self):
+        # Checks that explicitly turning the stats on via the env var works.
+        src = """if 1:
+        from numba import njit
+        import numpy as np
+        from numba.core.runtime import rtsys, _nrt_python
+
+        @njit
+        def foo():
+            return np.arange(10)[0]
+
+        assert _nrt_python.memsys_stats_enabled()
+        orig_stats = rtsys.get_allocation_stats()
+        foo()
+        new_stats = rtsys.get_allocation_stats()
+        total_alloc = new_stats.alloc - orig_stats.alloc
+        total_free = new_stats.free - orig_stats.free
+        total_mi_alloc = new_stats.mi_alloc - orig_stats.mi_alloc
+        total_mi_free = new_stats.mi_free - orig_stats.mi_free
+
+        expected = 1
+        assert total_alloc == expected
+        assert total_free == expected
+        assert total_mi_alloc == expected
+        assert total_mi_free == expected
+        """
+        # Check env var explicitly being set works
+        env = os.environ.copy()
+        env['NUMBA_NRT_STATS'] = "1"
+        run_in_subprocess(src, env=env)
+
+    def check_env_var_off(self, env):
+
+        src = """if 1:
+        from numba import njit
+        import numpy as np
+        from numba.core.runtime import rtsys, _nrt_python
+
+        @njit
+        def foo():
+            return np.arange(10)[0]
+
+        assert _nrt_python.memsys_stats_enabled() == False
+        try:
+            rtsys.get_allocation_stats()
+        except RuntimeError as e:
+            assert "NRT stats are disabled." in str(e)
+        """
+        run_in_subprocess(src, env=env)
+
+    def test_stats_env_var_explicit_off(self):
+        # Checks that explicitly turning the stats off via the env var works.
+        env = os.environ.copy()
+        env['NUMBA_NRT_STATS'] = "0"
+        self.check_env_var_off(env)
+
+    def test_stats_env_var_default_off(self):
+        # Checks that the env var not being set is the same as "off", i.e.
+        # default for Numba is off.
+        env = os.environ.copy()
+        env.pop('NUMBA_NRT_STATS', None)
+        self.check_env_var_off(env)
+
+    def test_stats_status_toggle(self):
+
+        @njit
+        def foo():
+            tmp = np.ones(3)
+            return np.arange(5 * tmp[0])
+
+        # Switch on stats
+        _nrt_python.memsys_enable_stats()
+        # check the stats are on
+        self.assertTrue(_nrt_python.memsys_stats_enabled())
+
+        for i in range(2):
+            # capture the stats state
+            stats_1 = rtsys.get_allocation_stats()
+            # Switch off stats
+            _nrt_python.memsys_disable_stats()
+            # check the stats are off
+            self.assertFalse(_nrt_python.memsys_stats_enabled())
+            # run something that would move the counters were they enabled
+            foo()
+            # Switch on stats
+            _nrt_python.memsys_enable_stats()
+            # check the stats are on
+            self.assertTrue(_nrt_python.memsys_stats_enabled())
+            # capture the stats state (should not have changed)
+            stats_2 = rtsys.get_allocation_stats()
+            # run something that will move the counters
+            foo()
+            # capture the stats state (should have changed)
+            stats_3 = rtsys.get_allocation_stats()
+            # check stats_1 == stats_2
+            self.assertEqual(stats_1, stats_2)
+            # check stats_2 < stats_3
+            self.assertLess(stats_2, stats_3)
+
+    def test_rtsys_stats_query_raises_exception_when_disabled(self):
+        # Checks that the standard rtsys.get_allocation_stats() query raises
+        # when stats counters are turned off.
+
+        _nrt_python.memsys_disable_stats()
+        self.assertFalse(_nrt_python.memsys_stats_enabled())
+
+        with self.assertRaises(RuntimeError) as raises:
+            rtsys.get_allocation_stats()
+
+        self.assertIn("NRT stats are disabled.", str(raises.exception))
+
+    def test_nrt_explicit_stats_query_raises_exception_when_disabled(self):
+        # Checks the various memsys_get_stats functions raise if queried when
+        # the stats counters are disabled.
+        method_variations = ('alloc', 'free', 'mi_alloc', 'mi_free')
+        for meth in method_variations:
+            stats_func = getattr(_nrt_python, f'memsys_get_stats_{meth}')
+            with self.subTest(stats_func=stats_func):
+                # Turn stats off
+                _nrt_python.memsys_disable_stats()
+                self.assertFalse(_nrt_python.memsys_stats_enabled())
+                with self.assertRaises(RuntimeError) as raises:
+                    stats_func()
+                self.assertIn("NRT stats are disabled.", str(raises.exception))
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/numba/tests/test_nrt_refct.py b/numba/tests/test_nrt_refct.py
--- a/numba/tests/test_nrt_refct.py
+++ b/numba/tests/test_nrt_refct.py
@@ -10,14 +10,15 @@
 import unittest
 from numba import njit
 from numba.core.runtime import rtsys
-from numba.tests.support import TestCase
+from numba.tests.support import TestCase, EnableNRTStatsMixin
 
 
-class TestNrtRefCt(TestCase):
+class TestNrtRefCt(EnableNRTStatsMixin, TestCase):
 
     def setUp(self):
         # Clean up any NRT-backed objects hanging in a dead reference cycle
         gc.collect()
+        super(TestNrtRefCt, self).setUp()
 
     def test_no_return(self):
         """
diff --git a/numba/tests/test_polynomial.py b/numba/tests/test_polynomial.py
--- a/numba/tests/test_polynomial.py
+++ b/numba/tests/test_polynomial.py
@@ -4,7 +4,7 @@
 import numpy as np
 
 from numba import jit
-from numba.tests.support import TestCase, tag, needs_lapack
+from numba.tests.support import TestCase, tag, needs_lapack, EnableNRTStatsMixin
 import unittest
 
 
@@ -12,7 +12,7 @@ def roots_fn(p):
     return np.roots(p)
 
 
-class TestPolynomialBase(TestCase):
+class TestPolynomialBase(EnableNRTStatsMixin, TestCase):
     """
     Provides setUp and common data/error modes for testing polynomial functions.
     """
@@ -23,6 +23,7 @@ class TestPolynomialBase(TestCase):
     def setUp(self):
         # Collect leftovers from previous test cases before checking for leaks
         gc.collect()
+        super(TestPolynomialBase, self).setUp()
 
     def assert_error(self, cfunc, args, msg, err=ValueError):
         with self.assertRaises(err) as raises:
diff --git a/numba/tests/test_print.py b/numba/tests/test_print.py
--- a/numba/tests/test_print.py
+++ b/numba/tests/test_print.py
@@ -6,7 +6,8 @@
 from numba.core.compiler import compile_isolated, Flags
 from numba import jit
 from numba.core import types, errors, utils
-from numba.tests.support import captured_stdout, tag, TestCase
+from numba.tests.support import (captured_stdout, tag, TestCase,
+                                 EnableNRTStatsMixin)
 
 
 enable_pyobj_flags = Flags()
@@ -43,7 +44,7 @@ def print_closure():
     return jit(nopython=True)(x)
 
 
-class TestPrint(TestCase):
+class TestPrint(EnableNRTStatsMixin, TestCase):
 
     def test_print_values(self):
         """