[crossposted to APR and SVN dev lists.]

Hi, all.  I've been working with Sander to resolve some pool problems.

** Here's the problem:

We have two use-cases in Subversion now that literally run out of
memory; the application footprint grows without bound until the OS
kills things.  Here they are:

  1.  'svn checkout file:///repository -r HEAD'
  2.  'svnadmin dump repository > dumpfile'

These use-cases, it turns out, are nearly identical.  They both simply
walk over a repository tree and push data at a vtable, which in turn
writes the data to disk (either as a working copy, or as a dumpfile).
The pools are controlled by our filesystem-walker, and we're being
*extremely* careful about controlling pool lifetimes and clearing subpools
in loops.  (Gstein has pounded these concepts into our heads.  :-))

  [Note: 'svn checkout http://...' works fine, because svn is doing an
   independent GET request for each file... which means a separate
   pool created and destroyed by httpd for each item.]

It turns out that the real issue here is that our current pool
implementation always grows forever, until the entire pool hierarchy
is destroyed when the main() process exits.  I believe that when Mike
Pilato stat'ed our pools, we were actually only using a few
megs... but we were still creating a memory footprint of 200M+ !


** Here's the solution:

Sander whipped out two of his pool patches that have been previously
ignored up till now.  He combined them into a single patch.  Here's
what they do:

  1.  Keep each pool's list of 'old' active blocks sorted by size.  If
      a request comes in that won't fit in the current active block,
      then try to alloc the request in the largest 'old' active block
      (instead of alloc'ing a completely new active block).
      Effectively, we get some memory re-use now.

  2.  When a pool is destroyed or cleared, normally all of its
      inactive blocks are given back to a 'global free list' in the
      pool hierarchy.  We now have a threshold size that can be set:
      if the global-free-list ever grows larger than the threshold,
      then the 'extra' inactive blocks are actually free()d.  We've
      set the maximum free-list size at 4M for now.

Anyway, I've included the patches below for people to look at.
Apparently Sander posted these before, but nobody had a reason to
care.  Well, now Subversion *really* needs these features.

I've tested this patch on our two use cases: the memory footprint for
'svn checkout' bounces up and down, but eventually plateaus around
25M.  The footprint for 'svnadmin dump' also bounces around, but
plateaus around 17M.  A huge difference! 

Sander is worried about possible pool-performance hits from these
changes, so he's hoping folks might be able to do some timing stats.
(Personally, via my sophisticated use of the unix 'time' command, I
find no noticeable difference.)

The patch affects two APR files (apr_allocator.h, apr_pools.c), and
one svn file (svn_error.c)... assuming svn users want to take
advantage of the new pool features.  I've posted the patch below.

Assuming nobody objects, Sander plans to apply these changes in a day
or two.  We just want to give people time to look them over.


Index: include/apr_allocator.h
===================================================================
RCS file: /home/cvs/apr/include/apr_allocator.h,v
retrieving revision 1.4
diff -u -r1.4 apr_allocator.h
--- include/apr_allocator.h     9 Apr 2002 06:56:55 -0000       1.4
+++ include/apr_allocator.h     22 May 2002 16:11:50 -0000
@@ -83,7 +83,9 @@
 
 struct apr_memnode_t {
     apr_memnode_t *next;
+    apr_memnode_t **ref;
     apr_uint32_t   index;
+    apr_uint32_t   free_index;
     char          *first_avail;
     char          *endp;
 };
@@ -150,6 +152,16 @@
  * @param allocator The allocator to get the owner from
  */
 APR_DECLARE(apr_pool_t *) apr_allocator_get_owner(apr_allocator_t *allocator);
+
+
+/**
+ * Set the current threshold at which the allocator should start
+ * giving blocks back to the system.
+ * @param allocator The allocator the set the threshold on
+ * @param size The threshold.  0 == unlimited.
+ */
+APR_DECLARE(void) apr_allocator_set_max_free(apr_allocator_t *allocator,
+                                             apr_size_t size);
 
 #include "apr_thread_mutex.h"
 
Index: memory/unix/apr_pools.c
===================================================================
RCS file: /home/cvs/apr/memory/unix/apr_pools.c,v
retrieving revision 1.167
diff -u -r1.167 apr_pools.c
--- memory/unix/apr_pools.c     5 May 2002 22:24:41 -0000       1.167
+++ memory/unix/apr_pools.c     22 May 2002 16:11:55 -0000
@@ -93,6 +93,8 @@
 
 struct apr_allocator_t {
     apr_uint32_t        max_index;
+    apr_uint32_t        max_free_index;
+    apr_uint32_t        current_free_index;
 #if APR_HAS_THREADS
     apr_thread_mutex_t *mutex;
 #endif /* APR_HAS_THREADS */
@@ -166,6 +168,32 @@
     return allocator->owner;
 }
 
+APR_DECLARE(void) apr_allocator_set_max_free(apr_allocator_t *allocator,
+                                             apr_size_t size)
+{
+    apr_uint32_t max_free_index;
+
+#if APR_HAS_THREADS
+    apr_thread_mutex_t *mutex;
+
+    mutex = apr_allocator_get_mutex(allocator);
+    if (mutex != NULL)
+        apr_thread_mutex_lock(mutex);
+#endif /* APR_HAS_THREADS */
+
+    max_free_index = APR_ALIGN(size, BOUNDARY_SIZE) >> BOUNDARY_INDEX;
+    allocator->current_free_index += max_free_index;
+    allocator->current_free_index -= allocator->max_free_index;
+    allocator->max_free_index = max_free_index;
+    if (allocator->current_free_index > max_free_index)
+        allocator->current_free_index = max_free_index;
+
+#if APR_HAS_THREADS
+    if (mutex != NULL)
+        apr_thread_mutex_unlock(mutex);
+#endif
+}
+
 APR_INLINE
 APR_DECLARE(apr_memnode_t *) apr_allocator_alloc(apr_allocator_t *allocator,
                                                  apr_size_t size)
@@ -228,6 +256,10 @@
                 allocator->max_index = max_index;
             }
 
+            allocator->current_free_index += node->index;
+            if (allocator->current_free_index > allocator->max_free_index)
+                allocator->current_free_index = allocator->max_free_index;
+
 #if APR_HAS_THREADS
             if (allocator->mutex)
                 apr_thread_mutex_unlock(allocator->mutex);
@@ -264,6 +296,10 @@
         if (node) {
             *ref = node->next;
 
+            allocator->current_free_index += node->index;
+            if (allocator->current_free_index > allocator->max_free_index)
+                allocator->current_free_index = allocator->max_free_index;
+
 #if APR_HAS_THREADS
             if (allocator->mutex)
                 apr_thread_mutex_unlock(allocator->mutex);
@@ -299,8 +335,9 @@
 APR_DECLARE(void) apr_allocator_free(apr_allocator_t *allocator,
                                      apr_memnode_t *node)
 {
-    apr_memnode_t *next;
+    apr_memnode_t *next, *freelist = NULL;
     apr_uint32_t index, max_index;
+    apr_uint32_t max_free_index, current_free_index;
 
 #if APR_HAS_THREADS
     if (allocator->mutex)
@@ -308,6 +345,8 @@
 #endif /* APR_HAS_THREADS */
 
     max_index = allocator->max_index;
+    max_free_index = allocator->max_free_index;
+    current_free_index = allocator->current_free_index;
 
     /* Walk the list of submitted nodes and free them one by one,
      * shoving them in the right 'size' buckets as we go.
@@ -316,7 +355,11 @@
         next = node->next;
         index = node->index;
 
-        if (index < MAX_INDEX) {
+        if (max_free_index != 0 && index > current_free_index) {
+            node->next = freelist;
+            freelist = node;
+        }
+        else if (index < MAX_INDEX) {
             /* Add the node to the appropiate 'size' bucket.  Adjust
              * the max_index when appropiate.
              */
@@ -325,6 +368,7 @@
                 max_index = index;
             }
             allocator->free[index] = node;
+            current_free_index -= index;
         }
         else {
             /* This node is too large to keep in a specific size bucket,
@@ -332,15 +376,23 @@
              */
             node->next = allocator->free[0];
             allocator->free[0] = node;
+            current_free_index -= index;
         }
     } while ((node = next) != NULL);
 
     allocator->max_index = max_index;
+    allocator->current_free_index = current_free_index;
 
 #if APR_HAS_THREADS
     if (allocator->mutex)
         apr_thread_mutex_unlock(allocator->mutex);
 #endif /* APR_HAS_THREADS */
+
+    while (freelist != NULL) {
+        node = freelist;
+        freelist = node->next;
+        free(node);
+    }
 }
 
 
@@ -544,6 +596,7 @@
     apr_memnode_t *active, *node;
     void *mem;
     char *endp;
+    apr_uint32_t free_index;
 
     size = APR_ALIGN_DEFAULT(size);
     active = pool->active;
@@ -557,17 +610,52 @@
         return mem;
     }
 
-    if ((node = apr_allocator_alloc(pool->allocator, size)) == NULL) {
-        if (pool->abort_fn)
-            pool->abort_fn(APR_ENOMEM);
-
-        return NULL;
+    node = active->next;
+    endp = node->first_avail + size;
+    if (endp < node->endp) {
+        *node->ref = node->next;
+        node->next->ref = node->ref;
+    }
+    else {
+        if ((node = apr_allocator_alloc(pool->allocator, size)) == NULL) {
+            if (pool->abort_fn)
+                pool->abort_fn(APR_ENOMEM);
+        }
+        endp = node->first_avail + size;
     }
 
-    active->next = pool->active = node;
+    node->free_index = 0;
 
     mem = node->first_avail;
-    node->first_avail += size;
+    node->first_avail = endp;
+
+    node->ref = active->ref;
+    *node->ref = node;
+    node->next = active;
+    active->ref = &node->next;
+
+    pool->active = node;
+
+    free_index = (APR_ALIGN(active->endp - active->first_avail + 1,
+                            BOUNDARY_SIZE) - BOUNDARY_SIZE) >> BOUNDARY_INDEX;
+
+    active->free_index = free_index;
+    node = active->next;
+    if (free_index >= node->free_index)
+        return mem;
+
+    do {
+        node = node->next;
+    }
+    while (free_index < node->free_index);
+
+    *active->ref = active->next;
+    active->next->ref = active->ref;
+
+    active->ref = node->ref;
+    *active->ref = active;
+    active->next = node;
+    node->ref = &active->next;
 
     return mem;
 }
@@ -625,11 +713,13 @@
     active = pool->active = pool->self;
     active->first_avail = pool->self_first_avail;
 
-    if (active->next == NULL)
+    if (active->next == active)
         return;
 
+    *active->ref = NULL;
     apr_allocator_free(pool->allocator, active->next);
-    active->next = NULL;
+    active->next = active;
+    active->ref = &active->next;
 }
 
 APR_DECLARE(void) apr_pool_destroy(apr_pool_t *pool)
@@ -672,6 +762,7 @@
      */
     allocator = pool->allocator;
     active = pool->self;
+    *active->ref = NULL;
 
 #if APR_HAS_THREADS
     if (apr_allocator_get_owner(allocator) == pool) {
@@ -724,6 +815,9 @@
         return APR_ENOMEM;
     }
 
+    node->next = node;
+    node->ref = &node->next;
+
     pool = (apr_pool_t *)node->first_avail;
     node->first_avail = pool->self_first_avail = (char *)pool + SIZEOF_POOL_T;
 
@@ -791,7 +885,7 @@
 struct psprintf_data {
     apr_vformatter_buff_t vbuff;
     apr_memnode_t   *node;
-    apr_allocator_t *allocator;
+    apr_pool_t      *pool;
     apr_byte_t       got_a_new_node;
     apr_memnode_t   *free;
 };
@@ -800,29 +894,70 @@
 {
     struct psprintf_data *ps = (struct psprintf_data *)vbuff;
     apr_memnode_t *node, *active;
-    apr_size_t cur_len;
+    apr_size_t cur_len, size;
     char *strp;
-    apr_allocator_t *allocator;
+    apr_pool_t *pool;
+    apr_uint32_t free_index;
 
-    allocator = ps->allocator;
-    node = ps->node;
+    pool = ps->pool;
+    active = ps->node;
     strp = ps->vbuff.curpos;
-    cur_len = strp - node->first_avail;
+    cur_len = strp - active->first_avail;
+    size = cur_len << 1;
 
-    if ((active = apr_allocator_alloc(allocator, cur_len << 1)) == NULL)
-        return -1;
+    node = active->next;
+    if (!ps->got_a_new_node && node->first_avail + size < node->endp) {
+        *node->ref = node->next;
+        node->next->ref = node->ref;
+
+        node->ref = active->ref;
+        *node->ref = node;
+        node->next = active;
+        active->ref = &node->next;
+
+        node->free_index = 0;
+
+        pool->active = node;
+
+        free_index = (APR_ALIGN(active->endp - active->first_avail + 1,
+                                BOUNDARY_SIZE) - BOUNDARY_SIZE) >> 
BOUNDARY_INDEX;
+
+        active->free_index = free_index;
+        node = active->next;
+        if (free_index < node->free_index) {
+            do {
+                node = node->next;
+            }
+            while (free_index < node->free_index);
+
+            *active->ref = active->next;
+            active->next->ref = active->ref;
 
-    memcpy(active->first_avail, node->first_avail, cur_len);
+            active->ref = node->ref;
+            *active->ref = active;
+            active->next = node;
+            node->ref = &active->next;
+        }
+ 
+        node = pool->active;
+    }
+    else {
+        if ((node = apr_allocator_alloc (pool->allocator, size)) == NULL)
+            return -1;
+
+        if (ps->got_a_new_node) {
+            active->next = ps->free;
+            ps->free = node; 
+        }
 
-    if (ps->got_a_new_node) {
-        node->next = ps->free;
-        ps->free = node;
+        ps->got_a_new_node = 1;
     }
 
-    ps->node = active;
-    ps->vbuff.curpos = active->first_avail + cur_len;
-    ps->vbuff.endpos = active->endp - 1; /* Save a byte for NUL terminator */
-    ps->got_a_new_node = 1;
+    memcpy(node->first_avail, active->first_avail, cur_len);
+
+    ps->node = node;
+    ps->vbuff.curpos = node->first_avail + cur_len;
+    ps->vbuff.endpos = node->endp - 1; /* Save a byte for NUL terminator */
 
     return 0;
 }
@@ -832,10 +967,11 @@
     struct psprintf_data ps;
     char *strp;
     apr_size_t size;
-    apr_memnode_t *active;
+    apr_memnode_t *active, *node;
+    apr_uint32_t free_index;
 
     ps.node = active = pool->active;
-    ps.allocator = pool->allocator;
+    ps.pool = pool;
     ps.vbuff.curpos  = ps.node->first_avail;
 
     /* Save a byte for the NUL terminator */
@@ -858,15 +994,48 @@
     strp = ps.node->first_avail;
     ps.node->first_avail += size;
 
+   if (ps.free)
+        apr_allocator_free(pool->allocator, ps.free);
+
     /*
      * Link the node in if it's a new one
      */
-    if (ps.got_a_new_node) {
-        active->next = pool->active = ps.node;
+    if (!ps.got_a_new_node)
+        return strp;
+
+    active = pool->active;
+    node = ps.node;
+
+    node->free_index = 0;
+
+    node->ref = active->ref;
+    *node->ref = node;
+    node->next = active;
+    active->ref = &node->next;
+
+    pool->active = node;
+
+    free_index = (APR_ALIGN(active->endp - active->first_avail + 1,
+                            BOUNDARY_SIZE) - BOUNDARY_SIZE) >> BOUNDARY_INDEX;
+
+    active->free_index = free_index;
+    node = active->next;
+
+    if (free_index >= node->free_index)
+        return strp;
+
+    do {
+        node = node->next;
     }
+    while (free_index < node->free_index);
+
+    *active->ref = active->next;
+    active->next->ref = active->ref;
 
-    if (ps.free)
-        apr_allocator_free(ps.allocator, ps.free);
+    active->ref = node->ref;
+    *active->ref = active;
+    active->next = node;
+    node->ref = &active->next;
 
     return strp;
 }


Index: ./subversion/libsvn_subr/svn_error.c
===================================================================
--- ./subversion/libsvn_subr/svn_error.c
+++ ./subversion/libsvn_subr/svn_error.c        Wed May 22 10:39:46 2002
@@ -25,6 +25,7 @@
 #include "apr_pools.h"
 #include "apr_strings.h"
 #include "apr_hash.h"
+#include "apr_allocator.h"
 
 #include "svn_pools.h"
 #include "svn_error.h"
@@ -423,18 +424,46 @@
 SVN_POOL_FUNC_DEFINE(apr_pool_t *, svn_pool_create)
 {
   apr_pool_t *ret_pool;
+  apr_allocator_t *allocator = NULL;
+  apr_status_t apr_err;
+
+  /* For the top level pool we want a seperate allocator */
+  if (pool == NULL)
+    {
+      apr_err = apr_allocator_create (&allocator);
+      if (apr_err)
+        abort_on_pool_failure (apr_err);
+
+      /* Use magic number for testing, 2MB */
+      apr_allocator_set_max_free (allocator, 4096 * 1024);
+    }
 
 #if !APR_POOL_DEBUG
-  apr_pool_create_ex (&ret_pool, pool, abort_on_pool_failure, NULL);
+  apr_pool_create_ex (&ret_pool, pool, abort_on_pool_failure, allocator);
 #else /* APR_POOL_DEBUG */
   apr_pool_create_ex_debug (&ret_pool, pool, abort_on_pool_failure,
-                            NULL, file_line);
+                            allocator, file_line);
 #endif /* APR_POOL_DEBUG */
 
   /* If there is no parent, then initialize ret_pool as the "top". */
   if (pool == NULL)
     {
-      apr_status_t apr_err = svn_error_init_pool (ret_pool);
+#if APR_HAS_THREADS
+      {
+        apr_thread_mutex_t *mutex;
+
+        apr_err = apr_thread_mutex_create (&mutex, APR_THREAD_MUTEX_DEFAULT,
+                                           ret_pool);
+        if (apr_err)
+          abort_on_pool_failure (apr_err);
+
+        apr_allocator_set_mutex (allocator, mutex);
+      }
+#endif /* APR_HAS_THREADS */
+
+      apr_allocator_set_owner (allocator, ret_pool);
+     
+      apr_err = svn_error_init_pool (ret_pool);
       if (apr_err)
         abort_on_pool_failure (apr_err);
     }

Reply via email to