docs: def memory_usage(proc=-1, interval=.1, timeout=None, timestamps=False,
                   include_children=False, max_usage=False, retval=False,
                   stream=None):
      """
      Return the memory usage of a process or piece of code
  
      Parameters
      ----------
      proc : {int, string, tuple, subprocess.Popen}, optional
          The process to monitor. Can be given by an integer/string
          representing a PID, by a Popen object or by a tuple
          representing a Python function. The tuple contains three
          values (f, args, kw) and specifies to run the function
          f(*args, **kw).
          Set to -1 (default) for current process.
  
      interval : float, optional
          Interval at which measurements are collected.
  
      timeout : float, optional
          Maximum amount of time (in seconds) to wait before returning.
  
      max_usage : bool, optional
          Only return the maximum memory usage (default False)
  
      retval : bool, optional
          For profiling python functions. Save the return value of the profiled
          function. Return value of memory_usage becomes a tuple:
          (mem_usage, retval)
  
      timestamps : bool, optional
          if True, timestamps of memory usage measurement are collected as well.
  
      stream : File
          if stream is a File opened with write access, then results are written
          to this file instead of stored in memory and returned at the end of
          the subprocess. Useful for long-running processes.
          Implies timestamps=True.
  
      Returns
      -------
      mem_usage : list of floating-poing values
          memory usage, in MiB. It's length is always < timeout / interval
          if max_usage is given, returns the two elements maximum memory and
          number of measurements effectuated
      ret : return value of the profiled function
          Only returned if retval is set to True
      """
      if stream is not None:
          timestamps = True
  
      if not max_usage:
          ret = []
      else:
          ret = -1
  
      if timeout is not None:
          max_iter = int(timeout / interval)
      elif isinstance(proc, int):
          # external process and no timeout
          max_iter = 1
      else:
          # for a Python function wait until it finishes
          max_iter = float('inf')
  
      if hasattr(proc, '__call__'):
          proc = (proc, (), {})
      if isinstance(proc, (list, tuple)):
          if len(proc) == 1:
              f, args, kw = (proc[0], (), {})
          elif len(proc) == 2:
              f, args, kw = (proc[0], proc[1], {})
          elif len(proc) == 3:
              f, args, kw = (proc[0], proc[1], proc[2])
          else:
              raise ValueError
  
          while True:
              child_conn, parent_conn = Pipe()  # this will store MemTimer's results
              p = MemTimer(os.getpid(), interval, child_conn, timestamps=timestamps,
                        max_usage=max_usage, include_children=include_children)
              p.start()
              parent_conn.recv()  # wait until we start getting memory
              returned = f(*args, **kw)
              parent_conn.send(0)  # finish timing
              ret = parent_conn.recv()
              n_measurements = parent_conn.recv()
              if retval:
                  ret = ret, returned
              p.join(5 * interval)
              if n_measurements > 4 or interval < 1e-6:
                  break
              interval /= 10.
      elif isinstance(proc, subprocess.Popen):
          # external process, launched from Python
          line_count = 0
          while True:
              if not max_usage:
                  mem_usage = _get_memory(proc.pid, timestamps=timestamps,
                                          include_children=include_children)
                  if stream is not None:
                      stream.write("MEM {0:.6f} {1:.4f}\n".format(*mem_usage))
                  else:
                      ret.append(mem_usage)
              else:
                  ret = max([ret,
                             _get_memory(proc.pid,
                                         include_children=include_children)])
              time.sleep(interval)
              line_count += 1
              # flush every 50 lines. Make 'tail -f' usable on profile file
              if line_count > 50:
                  line_count = 0
                  if stream is not None:
                      stream.flush()
              if timeout is not None:
                  max_iter -= 1
                  if max_iter == 0:
                      break
              if proc.poll() is not None:
                  break
      else:
          # external process
          if max_iter == -1:
              max_iter = 1
          counter = 0
          while counter < max_iter:
              counter += 1
              if not max_usage:
                  mem_usage = _get_memory(proc, timestamps=timestamps,
                                          include_children=include_children)
                  if stream is not None:
                      stream.write("MEM {0:.6f} {1:.4f}\n".format(*mem_usage))
                  else:
                      ret.append(mem_usage)
              else:
                  ret = max([ret,
                             _get_memory(proc, include_children=include_children)
                             ])
  
              time.sleep(interval)
              # Flush every 50 lines.
              if counter % 50 == 0 and stream is not None:
                  stream.flush()
      if stream:
          return None
      return ret