comparison mercurial/smartset.py @ 35501: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
35500:87918218da70 35501: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)