1
+ """Performance test to demonstrate dependency caching optimization."""
2
+
3
+ import time
4
+ from unittest .mock import Mock , patch
5
+
6
+ import pytest
7
+
8
+ from pip ._internal .metadata import BaseDistribution
9
+ from pip ._internal .models .link import Link
10
+ from pip ._internal .req .req_install import InstallRequirement
11
+ from pip ._internal .resolution .resolvelib .candidates import LinkCandidate
12
+ from pip ._vendor .packaging .requirements import Requirement as PackagingRequirement
13
+ from pip ._vendor .packaging .utils import canonicalize_name
14
+ from pip ._vendor .packaging .version import Version
15
+
16
+
17
+ class MockDistribution (BaseDistribution ):
18
+ """Mock distribution for testing dependency parsing performance."""
19
+
20
+ def __init__ (self , name : str , version : str , dependencies : list [str ]):
21
+ self ._canonical_name = canonicalize_name (name )
22
+ self ._version = Version (version )
23
+ self ._dependencies = [PackagingRequirement (dep ) for dep in dependencies ]
24
+ self ._extras = ["extra1" , "extra2" ]
25
+
26
+ @property
27
+ def canonical_name (self ):
28
+ return self ._canonical_name
29
+
30
+ @property
31
+ def version (self ):
32
+ return self ._version
33
+
34
+ def iter_dependencies (self , extras = None ):
35
+ """Simulate expensive dependency parsing operation."""
36
+ # Simulate some processing time for parsing dependencies
37
+ time .sleep (0.001 ) # 1ms per call to simulate parsing overhead
38
+ return iter (self ._dependencies )
39
+
40
+ def iter_provided_extras (self ):
41
+ """Simulate expensive extras parsing operation."""
42
+ # Simulate some processing time for parsing extras
43
+ time .sleep (0.0005 ) # 0.5ms per call to simulate parsing overhead
44
+ return iter (self ._extras )
45
+
46
+ @property
47
+ def requires_python (self ):
48
+ return None
49
+
50
+
51
+ def create_mock_candidate_old_approach ():
52
+ """Create a candidate that simulates the old approach without caching."""
53
+
54
+ class OldApproachCandidate (LinkCandidate ):
55
+ """Candidate that doesn't cache dependencies (old approach)."""
56
+
57
+ def __init__ (self , * args , ** kwargs ):
58
+ # Skip the parent __init__ to avoid complex setup
59
+ self ._link = Mock ()
60
+ self ._source_link = Mock ()
61
+ self ._factory = Mock ()
62
+ self ._ireq = Mock ()
63
+ self ._name = canonicalize_name ("test-package" )
64
+ self ._version = Version ("1.0.0" )
65
+ self ._hash = None
66
+ # Don't initialize caching attributes
67
+ self .dist = MockDistribution ("test-package" , "1.0.0" , [
68
+ "requests>=2.0.0" ,
69
+ "urllib3>=1.0.0" ,
70
+ "certifi>=2020.1.1" ,
71
+ "charset-normalizer>=2.0.0" ,
72
+ "idna>=2.5"
73
+ ])
74
+
75
+ def _get_cached_dependencies (self ):
76
+ """Old approach: always re-parse dependencies."""
77
+ return list (self .dist .iter_dependencies (list (self .dist .iter_provided_extras ())))
78
+
79
+ def _get_cached_extras (self ):
80
+ """Old approach: always re-parse extras."""
81
+ return list (self .dist .iter_provided_extras ())
82
+
83
+ def iter_dependencies (self , with_requires : bool ):
84
+ """Simulate multiple calls to dependency parsing."""
85
+ if with_requires :
86
+ # Old approach: re-parse dependencies every time
87
+ requires = list (self .dist .iter_dependencies (list (self .dist .iter_provided_extras ())))
88
+ for r in requires :
89
+ yield None # Simplified for testing
90
+
91
+ return OldApproachCandidate ()
92
+
93
+
94
+ def create_mock_candidate_new_approach ():
95
+ """Create a candidate that uses the new caching approach."""
96
+
97
+ class NewApproachCandidate (LinkCandidate ):
98
+ """Candidate that caches dependencies (new approach)."""
99
+
100
+ def __init__ (self , * args , ** kwargs ):
101
+ # Skip the parent __init__ to avoid complex setup
102
+ self ._link = Mock ()
103
+ self ._source_link = Mock ()
104
+ self ._factory = Mock ()
105
+ self ._ireq = Mock ()
106
+ self ._name = canonicalize_name ("test-package" )
107
+ self ._version = Version ("1.0.0" )
108
+ self ._hash = None
109
+ # Initialize caching attributes
110
+ self ._cached_dependencies = None
111
+ self ._cached_extras = None
112
+ self .dist = MockDistribution ("test-package" , "1.0.0" , [
113
+ "requests>=2.0.0" ,
114
+ "urllib3>=1.0.0" ,
115
+ "certifi>=2020.1.1" ,
116
+ "charset-normalizer>=2.0.0" ,
117
+ "idna>=2.5"
118
+ ])
119
+
120
+ def _get_cached_dependencies (self ):
121
+ """New approach: cache parsed dependencies."""
122
+ if self ._cached_dependencies is None :
123
+ if self ._cached_extras is None :
124
+ self ._cached_extras = list (self .dist .iter_provided_extras ())
125
+ self ._cached_dependencies = list (
126
+ self .dist .iter_dependencies (self ._cached_extras )
127
+ )
128
+ return self ._cached_dependencies
129
+
130
+ def _get_cached_extras (self ):
131
+ """New approach: cache parsed extras."""
132
+ if self ._cached_extras is None :
133
+ self ._cached_extras = list (self .dist .iter_provided_extras ())
134
+ return self ._cached_extras
135
+
136
+ def iter_dependencies (self , with_requires : bool ):
137
+ """Use cached dependencies to avoid re-parsing."""
138
+ if with_requires :
139
+ # New approach: use cached dependencies
140
+ requires = self ._get_cached_dependencies ()
141
+ for r in requires :
142
+ yield None # Simplified for testing
143
+
144
+ return NewApproachCandidate ()
145
+
146
+
147
+ def test_dependency_parsing_performance_comparison ():
148
+ """Test that demonstrates the performance improvement from dependency caching."""
149
+
150
+ # Test parameters
151
+ num_iterations = 50 # Number of times to call iter_dependencies
152
+
153
+ # Test old approach (no caching)
154
+ old_candidate = create_mock_candidate_old_approach ()
155
+
156
+ start_time = time .time ()
157
+ for _ in range (num_iterations ):
158
+ list (old_candidate .iter_dependencies (with_requires = True ))
159
+ old_approach_time = time .time () - start_time
160
+
161
+ # Test new approach (with caching)
162
+ new_candidate = create_mock_candidate_new_approach ()
163
+
164
+ start_time = time .time ()
165
+ for _ in range (num_iterations ):
166
+ list (new_candidate .iter_dependencies (with_requires = True ))
167
+ new_approach_time = time .time () - start_time
168
+
169
+ # Calculate performance improvement
170
+ speedup = old_approach_time / new_approach_time if new_approach_time > 0 else float ('inf' )
171
+ time_saved = old_approach_time - new_approach_time
172
+ percentage_improvement = (time_saved / old_approach_time ) * 100 if old_approach_time > 0 else 0
173
+
174
+ print (f"\n === Dependency Caching Performance Test Results ===" )
175
+ print (f"Number of iter_dependencies() calls: { num_iterations } " )
176
+ print (f"Old approach (no caching): { old_approach_time :.4f} seconds" )
177
+ print (f"New approach (with caching): { new_approach_time :.4f} seconds" )
178
+ print (f"Time saved: { time_saved :.4f} seconds" )
179
+ print (f"Speedup: { speedup :.2f} x" )
180
+ print (f"Performance improvement: { percentage_improvement :.1f} %" )
181
+ print ("=" * 55 )
182
+
183
+ # Assert that the new approach is faster
184
+ assert new_approach_time < old_approach_time , (
185
+ f"New approach should be faster. "
186
+ f"Old: { old_approach_time :.4f} s, New: { new_approach_time :.4f} s"
187
+ )
188
+
189
+ # Assert significant performance improvement (at least 2x speedup)
190
+ assert speedup >= 2.0 , (
191
+ f"Expected at least 2x speedup, got { speedup :.2f} x"
192
+ )
193
+
194
+
195
+ def test_dependency_caching_correctness ():
196
+ """Test that caching doesn't change the behavior, only improves performance."""
197
+
198
+ old_candidate = create_mock_candidate_old_approach ()
199
+ new_candidate = create_mock_candidate_new_approach ()
200
+
201
+ # Both approaches should return the same dependencies
202
+ old_deps = list (old_candidate .iter_dependencies (with_requires = True ))
203
+ new_deps = list (new_candidate .iter_dependencies (with_requires = True ))
204
+
205
+ assert len (old_deps ) == len (new_deps ), "Both approaches should return same number of dependencies"
206
+
207
+ # Test multiple calls return consistent results with caching
208
+ new_deps_second_call = list (new_candidate .iter_dependencies (with_requires = True ))
209
+ assert len (new_deps ) == len (new_deps_second_call ), "Cached results should be consistent"
210
+
211
+
212
+ if __name__ == "__main__" :
213
+ # Run the performance test directly
214
+ test_dependency_parsing_performance_comparison ()
215
+ test_dependency_caching_correctness ()
216
+ print ("All tests passed!" )
0 commit comments