Skip to content

Commit

Permalink
docs: Updated 3 tutorial examples throughout. Included some additiona…
Browse files Browse the repository at this point in the history
…l code examples, fixed typos, and added more commentary.

Signed-off-by: Dee Lucic <[email protected]>
  • Loading branch information
drz416 committed May 20, 2024
1 parent a82e412 commit 628da15
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 99 deletions.
6 changes: 3 additions & 3 deletions docs/flamegraph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ follows:
allocated memory.

- For quickly identifying the functions that allocated more memory
directly, look for large plateaus along the bottom edge, as these show
directly, look for wide ceilings along the bottom edge, as these show
a single stack trace was responsible for a large chunk of the total
memory of the snapshot that the graph represents.

Expand Down Expand Up @@ -106,7 +106,7 @@ follows:
graph by default.

And of course, if you switch from the "icicle" view to the "flame" view,
the root jumps to the bottom of the page, and call stacks grow upwards
the root drops to the bottom of the page, and call stacks grow upwards
from it instead of downwards.

Simple example
Expand Down Expand Up @@ -424,7 +424,7 @@ for understanding its memory usage patterns.
about allocations over time. They also can't be used for finding
:doc:`temporary allocations </temporary_allocations>`.

You can see an example of a temporal flamegraph
You can see an example of a temporal flame graph
`here <_static/flamegraphs/memray-flamegraph-fib.html>`_.

Conclusion
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ You can also invoke Memray without version-qualifying it:
The downside to the unqualified ``memray`` script is that it's not immediately
clear what Python interpreter will be used to execute Memray. If you're using
a virtualenv that's not a problem because you know exactly what interpreter is
a virtual environment that's not a problem because you know exactly what interpreter is
in use, but otherwise you need to be careful to ensure that ``memray`` is
running with the interpreter you meant to use.

Expand Down
85 changes: 55 additions & 30 deletions docs/tutorials/1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ are used for all of the tests covered in this workshop.
Executing the tests
^^^^^^^^^^^^^^^^^^^

Pytest is already installed in your docker image, so you can simply invoke it to execute your tests.
Run the following command in your docker image's shell, it will search all subdirectories for tests.
Pytest is already installed in your virtual environment, so you can simply invoke it to execute your tests.
Run the following command in your terminal, it will search all subdirectories for tests.
This will test the entire workshop.

.. code:: shell
pytest
This can be tedious to test everything when we are working on 1 example at a time. To save time,
let's specify the specific test we want to run.
It can be tedious to test all excercises when we are working on 1 excercise at a time. To save time,
let's run only the tests for excercise 1.

.. code:: shell
Expand All @@ -94,27 +94,32 @@ some additional information. Looks like our test case allocated more memory than
will be taking advantage of this amazing feature included with Memray to help run our workshop. Your
goal for each exercise will be to modify the exercises (NOT the tests), in order to respect these memory limits.

Flamegraphs, what are they?
---------------------------
Flame graphs, what are they?
----------------------------

OK, so we know our test is broken. How can we use Memray to help us dive deeper into the underlying
problem? The answer, is a flamegraph! A flamegraph is an HTML file that can be used to visualize how
your program utilizes memory at the point in time where the memory usage is at its peak.
problem? The answer, is a flame graph! A flame graph is a tool used to visualize memory usage of a program
at points in time where the memory usage is at its peak. Memray can generate an HTML file that renders a
*flamegraph report*.

.. image:: ../_static/images/exercise1_flamegraph.png


On the middle portion of the screen, we can see the memory usage plotted vs time. The vertical (Y)
axis is memory used, and the horizontal (X) axis is time. Down below, a single moment in time (the
point when memory usage reached its peak) is plotted as a "flame graph". Each row in that flame
The *flamegraph report* is made up for three sections. At the top we have some controls to adjust the
appearance of our report. The middle portion of the screen shows a line plot where we can see total memory
usage of our program plotted over time. The vertical (Y) axis is memory used, and the horizontal (X) axis
is time. The lower portion is the flame graph and it displays only a single moment in time from the entire
execution runtime of the program. It shows the point when memory usage reached its peak. Each row in the flame
graph is a frame in your stack trace. The width of each box represents the relative amount of memory
used.
used. In *icicles* mode, the lowest row is the top of the stack, and shows the functions that allocated memory,
while in *flames* mode, the rows are flipped such that the top row shows the top of the stack and is the location
where memory is allocated.

You can click on a particular box to filter out less recent frames from the stack, focusing on a
particular frame and the functions it called into.

More details on :ref:`interpreting flame graphs` are available if this quick summary leaves you
confused.
More information on the :doc:`Flame Graph Reporter<../flamegraph>` or how to
:ref:`Interperet Flame Graphs<interpreting flame graphs>` are available in the docs.

Generating a flamegraph
^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -126,23 +131,42 @@ Run the first exercise labeled ``fibonacci.py``, but make sure to have Memray wr
memray run exercise_1/fibonacci.py
After the run is complete, Memray will conveniently print the command to generate a flamegraph from
the Memray output file.
the Memray output file. Run:

.. code:: shell
memray flamegraph exercise_1/memray-fibonacci.py.<run-id>.bin
The run id will change each time you run the command.
Note: the run-id will change each time you run the command.

Now that we have generated our flamegraph, you can launch the HTML output file in your web browser.
Now that we have generated our flame graph, you can launch the HTML output file in your web browser.

Challenge
---------

Take a closer look at the stack on the flamegraph - you will notice that the ``output.append`` line of
Take a closer look at the stack on the flamegraph you will notice that the list append line of
code appears to be the source of almost all of our script's allocations. Maybe that could be used as
a clue as to what in particular we may want to change to pass our test?

.. code-block:: python
:emphasize-lines: 13
def fibonacci(length):
# edge cases
if length < 1:
return []
if length == 1:
return [1]
if length == 2:
return [1, 1]
output = [1, 1]
for i in range(length - 2):
-> output.append(output[i] + output[i + 1])
return output
Try to edit ``fibonacci.py`` to make the program more memory efficient. Test your solution by running
the ``test_exercise_1.py`` unit test, and inspect the effect your changes have on the memory allocation by
generating new flamegraphs. Ensure you don't break any of the correctness tests along the way!
Expand All @@ -152,16 +176,18 @@ generating new flamegraphs. Ensure you don't break any of the correctness tests
<details>
<summary><i>Toggle to see the sample solution</i></summary>

After examining the flamegraph, we can see that the problem is caused by this intermediate array
After examining the flame graph, we can see that the problem is caused by this intermediate array
``output`` that we are using in order to capture and return the results of the calculation.

Python has an amazing construct that works perfectly in this situation called
Python has an amazing construct that works well in this situation called
`generators <https://wiki.python.org/moin/Generators>`_.

To explain it simply, a generator works by pausing execution of your function when you ``yield``,
and saving its state. After each iteration, we can return to that paused function in order to
retrieve the next value that is needed. This is much more memory efficient than processing the
entire loop and saving the results in memory — especially when you have 100,000 iterations! ::
Essentially, a generator works by pausing execution of your function when a ``yield`` statement
is reached, while the state of the function is saved. After each iteration, we can return to that
paused function in order to retrieve the next value that is needed. This is more memory efficient
than processing the entire loop and saving the results in memory — especially when you have 100,000 iterations!

.. code-block:: python
def fibonacci(length):
# edge cases
Expand All @@ -188,16 +214,15 @@ Full code solution `here <https://github.com/bloomberg/memray/blob/main/docs/tut
Conclusion
----------

We should try to avoid loading the entire result set into memory (like into a list) when we plan to
We should try to avoid loading the entire fibonacci sequence into memory (like into a list) when we plan to
iterate on that result set anyways. This is especially true when your result set is very large. It is
typically best to work with generators in these types of cases.
typically best to work with generators in these types of situations.

.. note::

Sometimes it is better to do all the calculations up front. Generators are far more memory
However, sometimes it is better to do all the calculations up front. Generators are far more memory
efficient than lists, but iterating over generators is slightly slower than iterating over
lists, and generators can only be iterated over once. The best trade-off may vary from case to
lists, and generators can only be iterated over once. The best solution will vary from case to
case.

Using Memray's flamegraph can be a quick and easy way to identify where your applications memory usage
bottle neck is.
Using Memray's flame graph can be a quick and easy way to identify where your application has a memory bottleneck.
35 changes: 20 additions & 15 deletions docs/tutorials/2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Exercise 2 - Clinging Onto Memory
Intro
-----

Unlike some low level languages like C, Python will manage our memory for us, and free up memory
Unlike some low level languages like C, Python will manage our memory for us and will free up memory
that's no longer needed. Python's automatic memory management makes our lives easier, but sometimes,
it may not work the way you would expect it to...

Expand All @@ -23,21 +23,21 @@ Take a guess, and then confirm by running ``memray`` and generating a ``flamegra
Expectations vs Reality
"""""""""""""""""""""""

Let's presume that we cant mutate the original data, the best we can do is peak memory of 200MB:
Let's presume that we can't mutate the original data, the best we can do is peak memory of 200MB:
for a brief moment in time both the original 100MB of data and the modified copy of the data will
need to be present. In practice, however, the actual peak usage will be 400MB as demonstrated by the
``flamegraph``:

.. image:: ../_static/images/exercise2_flamegraph.png

Examining our flamegraph further, we can see that we peak at 400MB of allocated memory due to three
Examining our flame graph further, we can see that we peak at 400MB of allocated memory due to three
allocations:

1. Original 100MB array created by ``load_xMb_of_data(100)``
2. The second modified array, created by ``raise_to_power()``
3. Inside ``add_scalar()``, the first modified array, created by ``data_pow()``, is held in memory
(100MB) until a scalar is added to it (another 100MB) and returned. Only once ``add_scalar()``
has returned, the 200MB used get deallocated together.
1. Original 100MB array created by ``load_xMb_of_data(100)`` (held by the ``subtact_scalar()``
function at peak memory usage)
2. The second modified array ``data_pow``, created by ``raise_to_power()``
3. Inside ``add_scalar()``, the first modified array, created by ``duplicate_data()``, is held in memory
(100MB) until a scalar is added to it (another 100MB) and returned.


Challenge
Expand All @@ -56,14 +56,16 @@ Solutions
<details>
<summary><i>Toggle to see the sample solutions</i></summary>

After examining the flamegraph, we can see that the problem is caused by local variables which are
After examining the flame graph, we can see that the problem is caused by local variables which are
no longer needed, but continue to use memory until ``process_data()`` has finished running.
Therefore, we need to refactor the method in a way that does not use unnecessary variables to store
data that will not be read afterwards. There are two main approaches we can use to solve our issue
here:

- Avoiding local variables in ``process_data()`` all together and instead returning the result of
nested function calls::
1. Avoiding local variables in ``process_data()`` all together and instead returning the result
of nested function calls

.. code-block:: python
def process_data():
# no extra reference to the original array
Expand All @@ -80,9 +82,11 @@ here:
ADD_AMOUNT
)
- Reassigning one variable: we can create a single variable, and re-use it multiple times to store
the new value of the manipulated array. This way, we will only hold one array in memory at a time,
instead of holding on to older versions of the mutated array unnecessarily::
2. Reassigning one variable: we can create a single variable, and re-use it multiple times to store
the new value of the manipulated array. This way, we will only hold one array in memory at a time,
instead of holding on to older versions of the mutated array unnecessarily

.. code-block:: python
def process_data():
# reusing the local variable instead of allocating more space
Expand All @@ -94,6 +98,7 @@ here:
data = add_scalar(data, ADD_AMOUNT)
return data
Full code solution `here
<https://github.com/bloomberg/memray/blob/main/docs/tutorials/solutions/exercise_2/holding_onto_memory.py>`_.

Expand All @@ -107,7 +112,7 @@ Conclusion
Typically, holding onto data in memory a little longer than needed is not a big issue. However, when
we are working with large objects, we should be particularly careful. Over-allocating unnecessary
memory can lead to running out of memory on the machine (especially for Linux VMs which are
typically smaller than the older physical machines).
typically smaller than physical machines).

Memray can be a helpful tool when trying to debug where we are over-allocating memory unnecessarily.

Expand Down
Loading

0 comments on commit 628da15

Please sign in to comment.