Merge lp:~salgado/launchpad/nuke-debug-references into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/nuke-debug-references
Merge into: lp:launchpad
Diff against target: 376 lines (+1/-305)
4 files modified
lib/canonical/config/schema-lazr.conf (+0/-16)
lib/canonical/launchpad/webapp/configure.zcml (+0/-6)
lib/canonical/launchpad/webapp/publication.py (+1/-98)
lib/lp/bugs/doc/memory-debug.txt (+0/-185)
To merge this branch: bzr merge lp:~salgado/launchpad/nuke-debug-references
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Approve
Review via email: mp+21673@code.launchpad.net

Description of the change

Remove debugReferencesLeak() and co. No longer needed as we'll be using meliae to get memory dumps which can later be analyzed: lp:~salgado/launchpad/use-meliae

To post a comment you must log in.
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Nuke, nuke, nuke!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2010-03-10 01:52:56 +0000
+++ lib/canonical/config/schema-lazr.conf 2010-03-18 19:34:18 +0000
@@ -601,22 +601,6 @@
601# datatype: integer601# datatype: integer
602storm_cache_size: 500602storm_cache_size: 500
603603
604
605[debug]
606# Set this to true to compare memory and reference leaking
607# from one request to the next. Turning this to true without
608# setting the number of threads to 1 will give not-so-useful
609# results.
610references: False
611
612# File to keep the reference count into.
613references_scoreboard_file: /tmp/refcounts-scoreboard.txt
614
615# Log file where leaks will be reported. Actually, this log will contain
616# an entry for each request containing the time, the pageid and the increase
617# in refcounts and memory.
618references_leak_log: /tmp/references-leak.log
619
620[diff]604[diff]
621# The maximum size in bytes to read from the librarian to make available in605# The maximum size in bytes to read from the librarian to make available in
622# the web UI. 512k == 524288 bytes.606# the web UI. 512k == 524288 bytes.
623607
=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml 2010-03-16 05:08:47 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml 2010-03-18 19:34:18 +0000
@@ -753,12 +753,6 @@
753 handler="canonical.launchpad.webapp.adapter.break_main_thread_db_access"753 handler="canonical.launchpad.webapp.adapter.break_main_thread_db_access"
754 />754 />
755755
756 <!-- Make sure that the config is sane for references debugging. -->
757 <subscriber
758 for="zope.app.appsetup.IProcessStartingEvent"
759 handler="canonical.launchpad.webapp.publication.debug_references_startup_check"
760 />
761
762 <!-- Set the default timeout function. -->756 <!-- Set the default timeout function. -->
763 <subscriber757 <subscriber
764 for="zope.app.appsetup.IProcessStartingEvent"758 for="zope.app.appsetup.IProcessStartingEvent"
765759
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2010-03-04 22:42:07 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2010-03-18 19:34:18 +0000
@@ -7,7 +7,6 @@
7 'LaunchpadBrowserPublication',7 'LaunchpadBrowserPublication',
8 ]8 ]
99
10import gc
11import os10import os
12import re11import re
13import sys12import sys
@@ -18,7 +17,6 @@
1817
19from cProfile import Profile18from cProfile import Profile
20from datetime import datetime19from datetime import datetime
21from time import strftime
2220
23import tickcount21import tickcount
24import transaction22import transaction
@@ -47,9 +45,7 @@
47import canonical.launchpad.webapp.adapter as da45import canonical.launchpad.webapp.adapter as da
4846
49from canonical.config import config, dbconfig47from canonical.config import config, dbconfig
50from canonical.mem import (48from canonical.mem import memory, resident
51 countsByType, deltaCounts, memory, mostRefs, printCounts, readCounts,
52 resident)
53from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities49from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
54from canonical.launchpad.readonly import is_read_only50from canonical.launchpad.readonly import is_read_only
55from lp.registry.interfaces.person import (51from lp.registry.interfaces.person import (
@@ -608,9 +604,6 @@
608604
609 da.clear_request_started()605 da.clear_request_started()
610606
611 if config.debug.references:
612 self.debugReferencesLeak(request)
613
614 # Maintain operational statistics.607 # Maintain operational statistics.
615 if getattr(request, '_wants_retry', False):608 if getattr(request, '_wants_retry', False):
616 OpStats.stats['retries'] += 1609 OpStats.stats['retries'] += 1
@@ -719,101 +712,11 @@
719 vss_start, rss_start, vss_end, rss_end))712 vss_start, rss_start, vss_end, rss_end))
720 log.close()713 log.close()
721714
722 def debugReferencesLeak(self, request):
723 """See what kind of references are increasing.
724
725 This logs the current RSS and references count by types in a
726 scoreboard file. If that file exists, we compare the current stats
727 with the previous one and logs the increase along the current page id.
728
729 Note that this only provides reliable results when only one thread is
730 processing requests.
731 """
732 gc.collect()
733 current_rss = resident()
734 current_garbage_count = len(gc.garbage)
735 # Convert type to string, because that's what we get when reading
736 # the old scoreboard.
737 current_refs = [
738 (count, str(ref_type)) for count, ref_type in mostRefs(n=0)]
739 # Add G as prefix to types on the garbage list.
740 current_garbage = [
741 (count, 'G%s' % str(ref_type))
742 for count, ref_type in countsByType(gc.garbage, n=0)]
743 scoreboard_path = config.debug.references_scoreboard_file
744
745 # Read in previous scoreboard if it exists.
746 if os.path.exists(scoreboard_path):
747 scoreboard = open(scoreboard_path, 'r')
748 try:
749 stats = scoreboard.readline().split()
750 prev_rss = int(stats[0].strip())
751 prev_garbage_count = int(stats[1].strip())
752 prev_refs = readCounts(scoreboard, '=== GARBAGE ===\n')
753 prev_garbage = readCounts(scoreboard)
754 finally:
755 scoreboard.close()
756 mem_leak = current_rss - prev_rss
757 garbage_leak = current_garbage_count - prev_garbage_count
758 delta_refs = list(deltaCounts(prev_refs, current_refs))
759 delta_refs.extend(deltaCounts(prev_garbage, current_garbage))
760 self.logReferencesLeak(request, mem_leak, delta_refs)
761
762 # Save the current scoreboard.
763 scoreboard = open(scoreboard_path, 'w')
764 try:
765 scoreboard.write("%d %d\n" % (current_rss, current_garbage_count))
766 printCounts(current_refs, scoreboard)
767 scoreboard.write('=== GARBAGE ===\n')
768 printCounts(current_garbage, scoreboard)
769 finally:
770 scoreboard.close()
771
772 def logReferencesLeak(self, request, mem_leak, delta_refs):
773 """Log the time, pageid, increase in RSS and increase in references.
774 """
775 log = open(config.debug.references_leak_log, 'a')
776 try:
777 pageid = request._orig_env.get('launchpad.pageid', 'Unknown')
778 # It can happen that the pageid is ''?!?
779 if pageid == '':
780 pageid = 'Unknown'
781 leak_in_mb = float(mem_leak) / (1024*1024)
782 formatted_delta = "; ".join(
783 "%s=%d" % (ref_type, count)
784 for count, ref_type in delta_refs)
785 log.write('%s %s %.2fMb %s\n' % (
786 strftime('%Y-%m-%d:%H:%M:%S'),
787 pageid,
788 leak_in_mb,
789 formatted_delta))
790 finally:
791 log.close()
792
793715
794class InvalidThreadsConfiguration(Exception):716class InvalidThreadsConfiguration(Exception):
795 """Exception thrown when the number of threads isn't set correctly."""717 """Exception thrown when the number of threads isn't set correctly."""
796718
797719
798def debug_references_startup_check(event):
799 """Event handler for IProcessStartingEvent.
800
801 If debug/references is set to True, we make sure that the number of
802 threads is configured to 1. We also delete any previous scoreboard file.
803 """
804 if not config.debug.references:
805 return
806
807 if config.threads != 1:
808 raise InvalidThreadsConfiguration(
809 "Number of threads should be one when debugging references.")
810
811 # Remove any previous scoreboard, the content is meaningless once
812 # the server is restarted.
813 if os.path.exists(config.debug.references_scoreboard_file):
814 os.remove(config.debug.references_scoreboard_file)
815
816
817class DefaultPrimaryContext:720class DefaultPrimaryContext:
818 """The default primary context is the context."""721 """The default primary context is the context."""
819722
820723
=== removed file 'lib/lp/bugs/doc/memory-debug.txt'
--- lib/lp/bugs/doc/memory-debug.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/doc/memory-debug.txt 1970-01-01 00:00:00 +0000
@@ -1,185 +0,0 @@
1= Memory debugging =
2
3Application server can leak references, and this is hard to track down.
4The LaunchpadBrowserPublication contains special purpose memory
5debugging helper to help diagnose such problems.
6
7The memory leaking diagnosis is invoked at endRequest() only when the
8debug.references variable is true. It is turned off by default.
9
10 >>> from canonical.config import config
11 >>> config.debug.references
12 False
13
14Setting it to true, will save in endRequest() a dump of the current
15memory usage and counts in the file specified in
16config.debug.references_scoreboard_file. (We also set the
17config.debug.references_leak_log file which will contain the
18differential diagnostic.)
19
20 >>> import os
21 >>> import tempfile
22 >>> from textwrap import dedent
23
24 >>> tempdir = tempfile.mkdtemp(prefix='memory-debug')
25 >>> scoreboard_path = os.path.join(tempdir, 'scoreboard.txt')
26 >>> references_log_path = os.path.join(tempdir, 'references-leak.log')
27
28 >>> config.push('debug-references', dedent("""\
29 ... [debug]
30 ... references: True
31 ... references_scoreboard_file: %s
32 ... references_leak_log: %s
33 ... """ % (scoreboard_path, references_log_path)))
34
35There is a debug_references_startup_check() that is called when the
36IProcessStartingEvent is fired. It raises an exception if the setting
37is on, but the number of threads is not 1.
38
39 >>> from canonical.launchpad.webapp.publication import (
40 ... debug_references_startup_check)
41 >>> orig_threads = config.threads
42 >>> orig_threads > 1
43 True
44 >>> from zope.app.appsetup import ProcessStarting
45
46 # We don't use notify here, because we don't want to invoke the
47 # other subscribers.
48 >>> debug_references_startup_check(ProcessStarting())
49 Traceback (most recent call last):
50 ...
51 InvalidThreadsConfiguration: ...
52
53This handler also deletes any previous scoreboard file that might be
54present (left by a previous run.)
55
56 >>> config.threads = 1
57 >>> open(scoreboard_path, 'w').close()
58 >>> debug_references_startup_check(ProcessStarting())
59 >>> os.path.exists(scoreboard_path)
60 False
61
62The memory monitoring code also watches the garbage list. Make sure that
63there is some available:
64
65 >>> class garbage(object):
66 ... """Uncollectable objects involved in a cycle."""
67 ... def __init__(self):
68 ... self.cycle = self
69 ... def __del__(self):
70 ... """The presence of this method makes us garbage."""
71 >>> trash = garbage()
72 >>> del trash
73
74So the first time endRequest() is called, the references count is dumped
75to the scoreboard file.
76
77 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
78 >>> from canonical.launchpad.webapp.adapter import set_request_started
79 >>> def create_request(pageid='View:+template'):
80 ... """Returns a LaunchpadTestRequest with appropriate fixture.
81 ...
82 ... Sets some attributes on the request expected by the
83 ... publication, and that are usually set by previous publication
84 ... steps.
85 ... """
86 ... request = LaunchpadTestRequest()
87 ... # Adds some expected requests variables by endRequest().
88 ... request._orig_env['launchpad.pageid'] = pageid
89 ... request.oopsid = None
90 ... # Prevents warnings when endRequest() is called.
91 ... set_request_started()
92 ... return request
93
94 >>> from canonical.launchpad.webapp.publication import (
95 ... LaunchpadBrowserPublication)
96 >>> publication = LaunchpadBrowserPublication(None)
97 >>> publication.endRequest(create_request(), None)
98
99The first line of the scoreboard is the number of bytes of RSS used and
100the number of items on the garbage list.
101
102 >>> import re
103 >>> scoreboard = open(scoreboard_path, 'r')
104 >>> lines = scoreboard.readlines()
105 >>> re.match('\d+ \d+$', lines[0].strip()) is not None
106 True
107
108Lines up to the '=== GARBAGE ===' marker contains a count and a type
109name. This is the number of references for that type.
110
111 >>> re.match('\d+ <type.*>$', lines[1].strip()) is not None
112 True
113
114After the '=== GARBAGE ===' marker, again we find a count and type name
115on each subsequent lines.
116
117 >>> garbage_marker = lines.index('=== GARBAGE ===\n')
118 >>> garbage_marker > 1
119 True
120 >>> print "\n".join(lines[garbage_marker:])
121 === GARBAGE ===
122 ...1 G<class 'garbage'>...
123
124On the second request, the scoreboard will be compared to the previous
125one and differences will be reported in the references leak log file.
126The format is:
127
128 date pageid increase_in_RSS increase_in_garbage type=increase; \
129 type=increase; G<type>=increase; ...
130
131Type names prefixed by G means that a number of instances of that type
132was added to the garbage list.
133
134Example:
135
136 2008-04-03:18:15:45 Question:+index 0.57Mb <type 'tuple'>=6419;
137 <type 'dict'>=2744; <type 'list'>=1942; <type 'weakref'>=544;
138 G<class 'garbage'>=1
139
140 >>> os.path.exists(references_log_path)
141 False
142
143(Add some garbage)
144
145 >>> trash = garbage()
146 >>> del trash
147
148 >>> publication.endRequest(create_request(), None)
149 >>> references_log = open(references_log_path, 'r')
150 >>> references_log.read()
151 "... View:+template ...Mb... <type 'list'>=...;
152 ...G<class 'garbage'>=1...\n"
153
154Of course when the tool is off, none of the above processing takes
155place:
156
157 >>> config.push('turn-off-references-debug', dedent("""\
158 ... [debug]
159 ... references: False"""))
160 >>> scoreboard.close()
161 >>> references_log.close()
162 >>> os.remove(scoreboard_path)
163 >>> os.remove(references_log_path)
164
165 >>> publication.endRequest(create_request(), None)
166 >>> os.path.exists(config.debug.references_scoreboard_file)
167 False
168 >>> os.path.exists(config.debug.references_leak_log)
169 False
170
171(Clean up)
172
173 >>> ignored = config.pop('debug-references')
174 >>> config.threads = orig_threads
175
176 >>> import gc
177 >>> new_garbage = []
178 >>> for g in gc.garbage:
179 ... # We only know how to clean our own garbage.
180 ... if isinstance(g, garbage):
181 ... del g.cycle
182 ... else:
183 ... new_garbage.append(g)
184 >>> gc.garbage[:] = new_garbage
185