Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/smartset.py @ 35505:12a46ad67a3c
smartset: split generatorset classes to avoid cycle
I uncovered a cycle manifesting in a memory leak by running
`hgperfrevset '::tip'`. The cycle was due to generatorset.__init__
assigning a bound method to self.__contains__. Internet sleuthing
revealed that assigning a bound method to an instance attribute
always creates a cycle.
This commit creates two new variants of generatorset for the special
cases of ascending and descending generators. The special
implementations of __contains__ have been extracted to these classes
where they are defined as __contains__.
generatorset now implements __new__ and changes the spawned type to
one of the new classes if needed.
Differential Revision: https://phab.mercurial-scm.org/D1780
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Wed, 27 Dec 2017 11:08:32 -0700 |
parents | 01c9700fbf9f |
children | f484b9d95c23 |
comparison
equal
deleted
inserted
replaced
35504:87918218da70 | 35505:12a46ad67a3c |
---|---|
770 >>> xs = generatorset([0, 1, 4], iterasc=True) | 770 >>> xs = generatorset([0, 1, 4], iterasc=True) |
771 >>> assert xs.last() == xs.last() | 771 >>> assert xs.last() == xs.last() |
772 >>> xs.last() # cached | 772 >>> xs.last() # cached |
773 4 | 773 4 |
774 """ | 774 """ |
775 def __new__(cls, gen, iterasc=None): | |
776 if iterasc is None: | |
777 typ = cls | |
778 elif iterasc: | |
779 typ = _generatorsetasc | |
780 else: | |
781 typ = _generatorsetdesc | |
782 | |
783 return super(generatorset, cls).__new__(typ) | |
784 | |
775 def __init__(self, gen, iterasc=None): | 785 def __init__(self, gen, iterasc=None): |
776 """ | 786 """ |
777 gen: a generator producing the values for the generatorset. | 787 gen: a generator producing the values for the generatorset. |
778 """ | 788 """ |
779 self._gen = gen | 789 self._gen = gen |
780 self._asclist = None | 790 self._asclist = None |
781 self._cache = {} | 791 self._cache = {} |
782 self._genlist = [] | 792 self._genlist = [] |
783 self._finished = False | 793 self._finished = False |
784 self._ascending = True | 794 self._ascending = True |
785 if iterasc is not None: | |
786 if iterasc: | |
787 self.fastasc = self._iterator | |
788 self.__contains__ = self._asccontains | |
789 else: | |
790 self.fastdesc = self._iterator | |
791 self.__contains__ = self._desccontains | |
792 | 795 |
793 def __nonzero__(self): | 796 def __nonzero__(self): |
794 # Do not use 'for r in self' because it will enforce the iteration | 797 # Do not use 'for r in self' because it will enforce the iteration |
795 # order (default ascending), possibly unrolling a whole descending | 798 # order (default ascending), possibly unrolling a whole descending |
796 # iterator. | 799 # iterator. |
808 | 811 |
809 # Use new values only, as existing values would be cached. | 812 # Use new values only, as existing values would be cached. |
810 for l in self._consumegen(): | 813 for l in self._consumegen(): |
811 if l == x: | 814 if l == x: |
812 return True | 815 return True |
813 | |
814 self._cache[x] = False | |
815 return False | |
816 | |
817 def _asccontains(self, x): | |
818 """version of contains optimised for ascending generator""" | |
819 if x in self._cache: | |
820 return self._cache[x] | |
821 | |
822 # Use new values only, as existing values would be cached. | |
823 for l in self._consumegen(): | |
824 if l == x: | |
825 return True | |
826 if l > x: | |
827 break | |
828 | |
829 self._cache[x] = False | |
830 return False | |
831 | |
832 def _desccontains(self, x): | |
833 """version of contains optimised for descending generator""" | |
834 if x in self._cache: | |
835 return self._cache[x] | |
836 | |
837 # Use new values only, as existing values would be cached. | |
838 for l in self._consumegen(): | |
839 if l == x: | |
840 return True | |
841 if l < x: | |
842 break | |
843 | 816 |
844 self._cache[x] = False | 817 self._cache[x] = False |
845 return False | 818 return False |
846 | 819 |
847 def __iter__(self): | 820 def __iter__(self): |
945 return self.last() | 918 return self.last() |
946 return next(it(), None) | 919 return next(it(), None) |
947 | 920 |
948 def __repr__(self): | 921 def __repr__(self): |
949 d = {False: '-', True: '+'}[self._ascending] | 922 d = {False: '-', True: '+'}[self._ascending] |
950 return '<%s%s>' % (type(self).__name__, d) | 923 return '<%s%s>' % (type(self).__name__.lstrip('_'), d) |
924 | |
925 class _generatorsetasc(generatorset): | |
926 """Special case of generatorset optimized for ascending generators.""" | |
927 | |
928 fastasc = generatorset._iterator | |
929 | |
930 def __contains__(self, x): | |
931 if x in self._cache: | |
932 return self._cache[x] | |
933 | |
934 # Use new values only, as existing values would be cached. | |
935 for l in self._consumegen(): | |
936 if l == x: | |
937 return True | |
938 if l > x: | |
939 break | |
940 | |
941 self._cache[x] = False | |
942 return False | |
943 | |
944 class _generatorsetdesc(generatorset): | |
945 """Special case of generatorset optimized for descending generators.""" | |
946 | |
947 fastdesc = generatorset._iterator | |
948 | |
949 def __contains__(self, x): | |
950 if x in self._cache: | |
951 return self._cache[x] | |
952 | |
953 # Use new values only, as existing values would be cached. | |
954 for l in self._consumegen(): | |
955 if l == x: | |
956 return True | |
957 if l < x: | |
958 break | |
959 | |
960 self._cache[x] = False | |
961 return False | |
951 | 962 |
952 def spanset(repo, start=0, end=None): | 963 def spanset(repo, start=0, end=None): |
953 """Create a spanset that represents a range of repository revisions | 964 """Create a spanset that represents a range of repository revisions |
954 | 965 |
955 start: first revision included the set (default to 0) | 966 start: first revision included the set (default to 0) |