def test_sigusr2(): """ Sending SIGUSR2 to the process does an extra dump. """ script = TEST_SCRIPTS / "sigusr2.py" output_dir = profile(script) # There are two dumps in the output directory, one for SIGUSR2, one for # shutdown. assert len(list(output_dir.iterdir())) == 2 sigusr2, final = sorted(output_dir.glob("*/peak-memory.prof")) # SIGUSR2 dump only has allocations up to that point script = str(script) path1 = ((script, "<module>", 8), (numpy.core.numeric.__file__, "ones", ANY)) path2 = ((script, "<module>", 11), (numpy.core.numeric.__file__, "ones", ANY)) allocations_sigusr2 = get_allocations(sigusr2, direct=True) assert match(allocations_sigusr2, {path1: big}, as_mb) == pytest.approx(20, 0.1) with pytest.raises(MatchError): match(allocations_sigusr2, {path2: big}, as_mb) allocations_final = get_allocations(final, direct=True) assert match(allocations_final, {path1: big}, as_mb) == pytest.approx(20, 0.1) assert match(allocations_final, {path2: big}, as_mb) == pytest.approx(50, 0.1)
def test_out_of_memory(): """ If an allocation is run that runs out of memory, current allocations are written out. """ script = TEST_SCRIPTS / "oom.py" output_dir = profile(script, expect_exit_code=53) time.sleep(10) # wait for child process to finish allocations = get_allocations( output_dir, [ "out-of-memory.svg", "out-of-memory-reversed.svg", "out-of-memory.prof", ], "out-of-memory.prof", ) ones = (numpy.core.numeric.__file__, "ones", ANY) script = str(script) expected_small_alloc = ((script, "<module>", 9), ones) toobig_alloc = ((script, "<module>", 12), ones) assert match(allocations, {expected_small_alloc: big}, as_mb) == pytest.approx(100, 0.1) assert match(allocations, {toobig_alloc: big}, as_mb) == pytest.approx(1024 * 1024 * 1024, 0.1)
def test_malloc_in_c_extension(): """ Various malloc() and friends variants in C extension gets captured. """ script = TEST_SCRIPTS / "malloc.py" output_dir = profile(script, "--size", "70") allocations = get_allocations(output_dir) script = str(script) # The realloc() in the scripts adds 10 to the 70: path = ((script, "<module>", 32), (script, "main", 28)) assert match(allocations, {path: big}, as_mb) == pytest.approx(70 + 10, 0.1) # The C++ new allocation: path = ((script, "<module>", 32), (script, "main", 23)) assert match(allocations, {path: big}, as_mb) == pytest.approx(40, 0.1) # C++ aligned_alloc(); not available on Conda, where it's just a macro # redirecting to posix_memalign. if not os.environ.get("CONDA_PREFIX"): path = ((script, "<module>", 32), (script, "main", 24)) assert match(allocations, {path: big}, as_mb) == pytest.approx(90, 0.1) # Py*_*Malloc APIs: path = ((script, "<module>", 32), (script, "main", 25)) assert match(allocations, {path: big}, as_mb) == pytest.approx(30, 0.1) # posix_memalign(): path = ((script, "<module>", 32), (script, "main", 26)) assert match(allocations, {path: big}, as_mb) == pytest.approx(15, 0.1)
def test_out_of_memory_slow_leak_cgroups(): """ If an allocation is run that runs out of memory slowly, hitting a cgroup limit that's lower than system memory, current allocations are written out. """ available_memory = psutil.virtual_memory().available script = TEST_SCRIPTS / "oom-slow.py" output_dir = profile( script, expect_exit_code=53, argv_prefix=get_systemd_run_args(available_memory), ) time.sleep(10) # wait for child process to finish allocations = get_allocations( output_dir, [ "out-of-memory.svg", "out-of-memory-reversed.svg", "out-of-memory.prof", ], "out-of-memory.prof", ) expected_alloc = ((str(script), "<module>", 3), ) # Should've allocated at least a little before running out, unless testing # environment is _really_ restricted, in which case other tests would've # failed. assert match(allocations, {expected_alloc: big}, as_mb) > 100
def test_jupyter(tmpdir): """Jupyter magic can run Fil.""" shutil.copyfile(TEST_SCRIPTS / "jupyter.ipynb", tmpdir / "jupyter.ipynb") check_call( [ "jupyter", "nbconvert", "--execute", "jupyter.ipynb", "--to", "html", ], cwd=tmpdir, ) output_dir = tmpdir / "fil-result" # IFrame with SVG was included in output: with open(tmpdir / "jupyter.html") as f: html = f.read() assert "<iframe" in html [svg_path] = re.findall(r'src="([^"]*\.svg)"', html) assert svg_path.endswith("peak-memory.svg") assert Path(tmpdir / svg_path).exists() # Allocations were tracked: allocations = get_allocations(output_dir) print(allocations) path = ( (re.compile("<ipython-input-3-.*"), "__magic_run_with_fil", 2), (re.compile("<ipython-input-2-.*"), "alloc", 4), (numpy.core.numeric.__file__, "ones", ANY), ) assert match(allocations, {path: big}, as_mb) == pytest.approx(48, 0.1)
def test_minus_m_minus_m(): """ `python -m filprofiler -m package` runs the package. """ dir = TEST_SCRIPTS script = (dir / "malloc.py").absolute() output_dir = Path(mkdtemp()) check_call( [ sys.executable, "-m", "filprofiler", "-o", str(output_dir), "run", "-m", "malloc", "--size", "50", ], cwd=dir, ) allocations = get_allocations(output_dir) stripped_allocations = {k[3:]: v for (k, v) in allocations.items()} script = str(script) path = ((script, "<module>", 32), (script, "main", 28)) assert match(stripped_allocations, {path: big}, as_mb) == pytest.approx(50 + 10, 0.1)
def test_jupyter(tmpdir): """Jupyter magic can run Fil.""" shutil.copyfile(TEST_SCRIPTS / "jupyter.ipynb", tmpdir / "jupyter.ipynb") check_call( [ "jupyter", "nbconvert", "--execute", "jupyter.ipynb", "--to", "html", ], cwd=tmpdir, ) output_dir = tmpdir / "fil-result" # IFrame with SVG was included in output: with open(tmpdir / "jupyter.html") as f: html = f.read() assert "<iframe" in html [svg_path] = re.findall(r'src="([^"]*\.svg)"', html) assert svg_path.endswith("peak-memory.svg") assert Path(tmpdir / svg_path).exists() # Allocations were tracked: allocations = get_allocations(output_dir) path = ( (re.compile(".*ipy*"), "__magic_run_with_fil", 3), (re.compile(".*ipy.*"), "alloc", 4), (numpy.core.numeric.__file__, "ones", ANY), ) assert match(allocations, {path: big}, as_mb) == pytest.approx(48, 0.1) actual_path = None for key in allocations: try: match(key, path, lambda x: x) except MatchError: continue else: actual_path = key assert actual_path != None assert actual_path[0][0] != actual_path[1][0] # code is in different cells path2 = ( (re.compile(".*ipy.*"), "__magic_run_with_fil", 2), (numpy.core.numeric.__file__, "ones", ANY), ) assert match(allocations, {path2: big}, as_mb) == pytest.approx(20, 0.1) # It's possible to run nbconvert again. check_call( [ "jupyter", "nbconvert", "--execute", "jupyter.ipynb", "--to", "html", ], cwd=tmpdir, )
def test_tabs(): """ Source code with tabs doesn't break SVG generation. """ script = TEST_SCRIPTS / "tabs.py" output_dir = profile(script) get_allocations(output_dir) # <- smoke test for svg in ["peak-memory.svg", "peak-memory-reversed.svg"]: svg_path = glob(str(output_dir / "*" / svg))[0] with open(svg_path) as f: svg = f.read() # Tabs are still there: assert ">\tarr1, arr2 = make_".replace(" ", "\u00a0") in svg # It's valid XML: ElementTree.fromstring(svg)
def test_fortran(): """ Fil can capture Fortran allocations. """ script = TEST_SCRIPTS / "fortranallocate.py" output_dir = profile(script) allocations = get_allocations(output_dir) script = str(script) path = ((script, "<module>", 3), ) assert match(allocations, {path: big}, as_mb) == pytest.approx(40, 0.1)
def test_c_thread(): """ Allocations in C-only threads are considered allocations by the Python code that launched the thread. """ script = TEST_SCRIPTS / "c-thread.py" output_dir = profile(script) allocations = get_allocations(output_dir) script = str(script) alloc = ((script, "<module>", 13), (script, "main", 9)) assert match(allocations, {alloc: big}, as_mb) == pytest.approx(17, 0.1)
def test_minus_m(): """ `fil-profile -m package` runs the package. """ dir = TEST_SCRIPTS script = (dir / "malloc.py").absolute() output_dir = profile("-m", "malloc", "--size", "50", cwd=dir) allocations = get_allocations(output_dir) script = str(script) path = ((script, "<module>", 32), (script, "main", 28)) assert match(allocations, {path: big}, as_mb) == pytest.approx(50 + 10, 0.1)
def test_minus_m(): """ `fil-profile -m package` runs the package. """ dir = Path("python-benchmarks") script = (dir / "malloc.py").absolute() output_dir = profile("-m", "malloc", "--size", "50", cwd=dir) allocations = get_allocations(output_dir) stripped_allocations = {k[3:]: v for (k, v) in allocations.items()} script = str(script) path = ((script, "<module>", 32), (script, "main", 28)) assert match(stripped_allocations, {path: big}, as_mb) == pytest.approx(50 + 10, 0.1)
def test_anonymous_mmap(): """ Non-file-backed mmap() gets detected and tracked. (NumPy uses Python memory APIs, so is not sufficient to test this.) """ script = TEST_SCRIPTS / "mmaper.py" output_dir = profile(script) allocations = get_allocations(output_dir) script = str(script) path = ((script, "<module>", 6), ) assert match(allocations, {path: big}, as_mb) == pytest.approx(60, 0.1)
def test_python_objects(): """ Python objects gets detected and tracked. (NumPy uses Python memory APIs, so is not sufficient to test this.) """ script = TEST_SCRIPTS / "pyobject.py" output_dir = profile(script) allocations = get_allocations(output_dir) script = str(script) path = ((script, "<module>", 1), ) path2 = ((script, "<module>", 8), (script, "<genexpr>", 8)) assert match(allocations, {path: big}, as_mb) == pytest.approx(34, 1) assert match(allocations, {path2: big}, as_mb) == pytest.approx(46, 1)
def test_temporary_profiling(tmpdir): """Profiling can be run temporarily.""" start_tracing(tmpdir) def f(): arr = np.ones((1024, 1024, 4), dtype=np.uint64) # 32MB f() stop_tracing(tmpdir) # Allocations were tracked: path = ((__file__, "f", 46), (numpy.core.numeric.__file__, "ones", ANY)) allocations = get_allocations(tmpdir) assert match(allocations, {path: big}, as_mb) == pytest.approx(32, 0.1) # Profiling stopped: test_no_profiling()
def test_thread_allocates_after_main_thread_is_done(): """ fil-profile tracks thread allocations that happen after the main thread exits. """ script = TEST_SCRIPTS / "threaded_aftermain.py" output_dir = profile(script) allocations = get_allocations(output_dir) import threading threading = (threading.__file__, "run", ANY) ones = (numpy.core.numeric.__file__, "ones", ANY) script = str(script) thread1_path1 = ((script, "thread1", 9), ones) assert match(allocations, {thread1_path1: big}, as_mb) == pytest.approx(70, 0.1)
def test_temporary_profiling(tmpdir): """Profiling can be run temporarily.""" # get_allocations() expects actual output in a subdirectory. def f(): arr = np.ones((1024, 1024, 4), dtype=np.uint64) # 32MB del arr return 1234 result = profile(f, tmpdir / "output") assert result == 1234 # Allocations were tracked: path = ((__file__, "f", 49), (numpy.core.numeric.__file__, "ones", ANY)) allocations = get_allocations(tmpdir) assert match(allocations, {path: big}, as_mb) == pytest.approx(32, 0.1) # Profiling stopped: test_no_profiling()
def run_in_ipython_shell(code_cells): """Run a list of strings in IPython. Returns parsed allocations. """ InteractiveShell.clear_instance() shell = InteractiveShell.instance( display_pub_class=CapturingDisplayPublisher) for code in code_cells: shell.run_cell(code) InteractiveShell.clear_instance() html = shell.display_pub.outputs[-1]["data"]["text/html"] assert "<iframe" in html [svg_path] = re.findall('src="([^"]*)"', html) assert svg_path.endswith("peak-memory.svg") resultdir = Path(svg_path).parent.parent return get_allocations(resultdir)
def test_threaded_allocation_tracking(): """ fil-profile tracks allocations from all threads. 1. The main thread gets profiled. 2. Other threads get profiled. """ script = TEST_SCRIPTS / "threaded.py" output_dir = profile(script) allocations = get_allocations(output_dir) import threading threading = (threading.__file__, "run", ANY) ones = (numpy.core.numeric.__file__, "ones", ANY) script = str(script) h = (script, "h", 7) # The main thread: main_path = ((script, "<module>", 24), (script, "main", 21), h, ones) assert match(allocations, {main_path: big}, as_mb) == pytest.approx(50, 0.1) # Thread that ends before main thread: thread1_path1 = ( (script, "thread1", 15), (script, "child1", 10), h, ones, ) assert match(allocations, {thread1_path1: big}, as_mb) == pytest.approx(30, 0.1) thread1_path2 = ((script, "thread1", 13), h, ones) assert match(allocations, {thread1_path2: big}, as_mb) == pytest.approx(20, 0.1)
def test_out_of_memory_slow_leak(): """ If an allocation is run that runs out of memory slowly, current allocations are written out. """ script = TEST_SCRIPTS / "oom-slow.py" output_dir = profile(script, expect_exit_code=53) time.sleep(10) # wait for child process to finish allocations = get_allocations( output_dir, [ "out-of-memory.svg", "out-of-memory-reversed.svg", "out-of-memory.prof", ], "out-of-memory.prof", ) expected_alloc = ((str(script), "<module>", 3), ) # Should've allocated at least a little before running out, unless testing # environment is _really_ restricted, in which case other tests would've # failed. assert match(allocations, {expected_alloc: big}, as_mb) > 100