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