Merge lp:~jameinel/bzr/2.0.4-494406-serve-memory-leak into lp:bzr

Proposed by John A Meinel
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jameinel/bzr/2.0.4-494406-serve-memory-leak
Merge into: lp:bzr
Diff against target: 107 lines (+40/-15)
3 files modified
NEWS (+12/-1)
bzrlib/_bencode_pyx.pyx (+1/-1)
bzrlib/export/dir_exporter.py (+27/-13)
To merge this branch: bzr merge lp:~jameinel/bzr/2.0.4-494406-serve-memory-leak
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+16368@code.launchpad.net
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

I managed to track down the memory leak on server-side operations to bzrlib.bencode.Encoder.

It turns out that Encoder was using '__del__' as its cleanup function, when it should have been '__dealloc__'. I don't know if __del__ ever gets called for Pyrex objects, but I do know that __dealloc__ always gets called. (And doesn't cause problems with GC cycles, as it gets put into the tp_dealloc slot.)

Anyway, this one-line patch gets rid of the memory leak as near as I can tell. (At least cycling on 'bzr commit' no longer leaks memory.)

It could also have a fair impact on codehostings 'lp-serve'. As that would be encoding a *lot* of bytes to send over the wire. This memory was also "dark" memory, in that it was allocated directly using 'malloc()' and thus not seen by Meliae. I'm not positive, but I think for every byte that 'bzr lp-serve' would send over the wire, it would get wrapped by a 'bencode' operation, which means that every byte sent out gets permanently leaked in memory. (Maybe we don't serve bulk data through a bencode wrapper.) At least, all of the response tuples are certainly encoded.

It doesn't affect my 'memory reduction' tests that I used for 2.1.0b3, because those were operating locally.

Revision history for this message
John A Meinel (jameinel) wrote :

This was meant to be proposed for 2.0
https://code.edge.launchpad.net/~jameinel/bzr/2.0.4-494406-serve-memory-leak/+merge/16369

Please review it there.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2009-12-18 09:05:13 +0000
+++ NEWS 2009-12-18 22:32:31 +0000
@@ -50,7 +50,7 @@
50bzr 2.0.4 (not released yet)50bzr 2.0.4 (not released yet)
51############################51############################
5252
53:Codename: template53:Codename:
54:2.0.4: ???54:2.0.4: ???
5555
56Compatibility Breaks56Compatibility Breaks
@@ -62,6 +62,17 @@
62Bug Fixes62Bug Fixes
63*********63*********
6464
65* ``bzr export dir`` now requests all file content as a record stream,
66 rather than requsting the file content one file-at-a-time. This can make
67 exporting over the network significantly faster (54min => 9min in one
68 case). (John Arbash Meinel, #343218)
69
70* ``bzr serve`` no longer slowly leaks memory. The compiled
71 ``bzrlib.bencode.Encoder()`` class was using ``__del__`` to cleanup and
72 free resources, and it should have been using ``__dealloc__``.
73 This will likely have an impact on any other process that is serving for
74 an extended period of time. (John Arbash Meinel, #494406)
75
65Improvements76Improvements
66************77************
6778
6879
=== modified file 'bzrlib/_bencode_pyx.pyx'
--- bzrlib/_bencode_pyx.pyx 2009-10-15 18:28:14 +0000
+++ bzrlib/_bencode_pyx.pyx 2009-12-18 22:32:31 +0000
@@ -268,7 +268,7 @@
268 self.maxsize = maxsize268 self.maxsize = maxsize
269 self.tail = p269 self.tail = p
270270
271 def __del__(self):271 def __dealloc__(self):
272 free(self.buffer)272 free(self.buffer)
273 self.buffer = NULL273 self.buffer = NULL
274 self.maxsize = 0274 self.maxsize = 0
275275
=== modified file 'bzrlib/export/dir_exporter.py'
--- bzrlib/export/dir_exporter.py 2009-07-29 13:46:55 +0000
+++ bzrlib/export/dir_exporter.py 2009-12-18 22:32:31 +0000
@@ -1,4 +1,4 @@
1# Copyright (C) 2005 Canonical Ltd1# Copyright (C) 2005, 2009 Canonical Ltd
2#2#
3# This program is free software; you can redistribute it and/or modify3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by4# it under the terms of the GNU General Public License as published by
@@ -52,21 +52,17 @@
52 raise errors.BzrError("Can't export tree to non-empty directory.")52 raise errors.BzrError("Can't export tree to non-empty directory.")
53 else:53 else:
54 raise54 raise
55 # Iterate everything, building up the files we will want to export, and
56 # creating the directories and symlinks that we need.
57 # This tracks (file_id, (destination_path, executable))
58 # This matches the api that tree.iter_files_bytes() wants
59 # Note in the case of revision trees, this does trigger a double inventory
60 # lookup, hopefully it isn't too expensive.
61 to_fetch = []
55 for dp, ie in _export_iter_entries(tree, subdir):62 for dp, ie in _export_iter_entries(tree, subdir):
56 fullpath = osutils.pathjoin(dest, dp)63 fullpath = osutils.pathjoin(dest, dp)
57 if ie.kind == "file":64 if ie.kind == "file":
58 if filtered:65 to_fetch.append((ie.file_id, (dp, tree.is_executable(ie.file_id))))
59 chunks = tree.get_file_lines(ie.file_id)
60 filters = tree._content_filter_stack(dp)
61 context = ContentFilterContext(dp, tree, ie)
62 contents = filtered_output_bytes(chunks, filters, context)
63 content = ''.join(contents)
64 fileobj = StringIO.StringIO(content)
65 else:
66 fileobj = tree.get_file(ie.file_id)
67 osutils.pumpfile(fileobj, file(fullpath, 'wb'))
68 if tree.is_executable(ie.file_id):
69 os.chmod(fullpath, 0755)
70 elif ie.kind == "directory":66 elif ie.kind == "directory":
71 os.mkdir(fullpath)67 os.mkdir(fullpath)
72 elif ie.kind == "symlink":68 elif ie.kind == "symlink":
@@ -80,3 +76,21 @@
80 else:76 else:
81 raise errors.BzrError("don't know how to export {%s} of kind %r" %77 raise errors.BzrError("don't know how to export {%s} of kind %r" %
82 (ie.file_id, ie.kind))78 (ie.file_id, ie.kind))
79 # The data returned here can be in any order, but we've already created all
80 # the directories
81 flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | getattr(os, 'O_BINARY', 0)
82 for (relpath, executable), chunks in tree.iter_files_bytes(to_fetch):
83 if filtered:
84 filters = tree._content_filter_stack(relpath)
85 context = ContentFilterContext(relpath, tree, ie)
86 chunks = filtered_output_bytes(chunks, filters, context)
87 fullpath = osutils.pathjoin(dest, relpath)
88 # We set the mode and let the umask sort out the file info
89 mode = 0666
90 if executable:
91 mode = 0777
92 out = os.fdopen(os.open(fullpath, flags, mode), 'wb')
93 try:
94 out.writelines(chunks)
95 finally:
96 out.close()
8397
=== modified file 'doc/en/user-guide/introducing_bazaar.txt'