ptdu 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ptdu/performance.py ADDED
@@ -0,0 +1,348 @@
1
+ """Performance optimizations for large directory handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Callable, Optional
8
+
9
+ if TYPE_CHECKING:
10
+ from ptdu.models import DirNode
11
+
12
+
13
+ @dataclass
14
+ class VirtualRange:
15
+ """Represents a visible range for virtual scrolling."""
16
+
17
+ start: int
18
+ end: int
19
+ total: int
20
+
21
+
22
+ class VirtualScroller:
23
+ """Manages virtual scrolling for large directories."""
24
+
25
+ # Default number of items to render
26
+ DEFAULT_WINDOW_SIZE: int = 100
27
+ # Buffer size for smooth scrolling
28
+ BUFFER_SIZE: int = 20
29
+
30
+ def __init__(
31
+ self, total_items: int = 0, window_size: int = DEFAULT_WINDOW_SIZE
32
+ ) -> None:
33
+ """Initialize virtual scroller.
34
+
35
+ Args:
36
+ total_items: Total number of items
37
+ window_size: Number of items to render at once
38
+ """
39
+ self._total: int = total_items
40
+ self._window_size: int = window_size
41
+ self._current_start: int = 0
42
+
43
+ def set_total(self, total: int) -> None:
44
+ """Update total item count.
45
+
46
+ Args:
47
+ total: New total number of items
48
+ """
49
+ self._total = total
50
+ self._clamp_start()
51
+
52
+ def get_visible_range(self) -> VirtualRange:
53
+ """Get the currently visible range.
54
+
55
+ Returns:
56
+ VirtualRange with start, end, and total
57
+ """
58
+ end = min(self._current_start + self._window_size, self._total)
59
+ return VirtualRange(
60
+ start=self._current_start,
61
+ end=end,
62
+ total=self._total,
63
+ )
64
+
65
+ def scroll_to(self, position: int) -> VirtualRange:
66
+ """Scroll to a specific position.
67
+
68
+ Args:
69
+ position: Target position (0-based)
70
+
71
+ Returns:
72
+ New visible range
73
+ """
74
+ self._current_start = max(0, min(position, self._total - 1))
75
+ self._clamp_start()
76
+ return self.get_visible_range()
77
+
78
+ def scroll_down(self, amount: int = 1) -> VirtualRange:
79
+ """Scroll down by amount.
80
+
81
+ Args:
82
+ amount: Number of items to scroll
83
+
84
+ Returns:
85
+ New visible range
86
+ """
87
+ self._current_start += amount
88
+ self._clamp_start()
89
+ return self.get_visible_range()
90
+
91
+ def scroll_up(self, amount: int = 1) -> VirtualRange:
92
+ """Scroll up by amount.
93
+
94
+ Args:
95
+ amount: Number of items to scroll
96
+
97
+ Returns:
98
+ New visible range
99
+ """
100
+ self._current_start -= amount
101
+ self._clamp_start()
102
+ return self.get_visible_range()
103
+
104
+ def page_down(self) -> VirtualRange:
105
+ """Scroll down by one page.
106
+
107
+ Returns:
108
+ New visible range
109
+ """
110
+ return self.scroll_down(self._window_size)
111
+
112
+ def page_up(self) -> VirtualRange:
113
+ """Scroll up by one page.
114
+
115
+ Returns:
116
+ New visible range
117
+ """
118
+ return self.scroll_up(self._window_size)
119
+
120
+ def _clamp_start(self) -> None:
121
+ """Clamp start position to valid range."""
122
+ max_start = max(0, self._total - self._window_size)
123
+ self._current_start = max(0, min(self._current_start, max_start))
124
+
125
+ def is_visible(self, index: int) -> bool:
126
+ """Check if an index is currently visible.
127
+
128
+ Args:
129
+ index: Item index to check
130
+
131
+ Returns:
132
+ True if visible
133
+ """
134
+ visible = self.get_visible_range()
135
+ return visible.start <= index < visible.end
136
+
137
+ def get_item_index_at(self, y_position: int, item_height: int) -> int:
138
+ """Get item index at a Y position.
139
+
140
+ Args:
141
+ y_position: Y coordinate
142
+ item_height: Height of each item in pixels
143
+
144
+ Returns:
145
+ Item index
146
+ """
147
+ return self._current_start + (y_position // item_height)
148
+
149
+
150
+ class MemoryOptimizer:
151
+ """Optimizes memory usage for large directory trees."""
152
+
153
+ # Maximum children to keep in memory per directory
154
+ MAX_CACHED_CHILDREN: int = 1000
155
+ # Maximum depth for initial scan
156
+ MAX_INITIAL_DEPTH: int = 3
157
+
158
+ def __init__(self) -> None:
159
+ """Initialize memory optimizer."""
160
+ self._unloaded_nodes: set[str] = set()
161
+ self._max_depth_reached: int = 0
162
+
163
+ def should_unload_children(self, node: DirNode) -> bool:
164
+ """Check if a node's children should be unloaded.
165
+
166
+ Args:
167
+ node: Directory node to check
168
+
169
+ Returns:
170
+ True if children should be unloaded
171
+ """
172
+ if not node.is_dir:
173
+ return False
174
+
175
+ # Unload if too many children
176
+ if len(node.children) > self.MAX_CACHED_CHILDREN:
177
+ return True
178
+
179
+ return False
180
+
181
+ def unload_children(self, node: DirNode) -> list[str]:
182
+ """Unload children from memory, returning names of unloaded children.
183
+
184
+ Args:
185
+ node: Directory node to unload
186
+
187
+ Returns:
188
+ List of unloaded child names
189
+ """
190
+ if not node.is_dir:
191
+ return []
192
+
193
+ unloaded: list[str] = []
194
+
195
+ # Keep track of unloaded nodes
196
+ for name, child in node.children.items():
197
+ self._unloaded_nodes.add(str(child.path))
198
+ unloaded.append(name)
199
+
200
+ # Clear children but keep size info
201
+ node.children.clear()
202
+ node._is_loaded = False
203
+
204
+ return unloaded
205
+
206
+ def reload_children(
207
+ self,
208
+ node: DirNode,
209
+ loader: Callable[[], list[DirNode]],
210
+ ) -> None:
211
+ """Reload children using a loader function.
212
+
213
+ Args:
214
+ node: Directory node to reload
215
+ loader: Function that returns list of child nodes
216
+ """
217
+ if not node.is_dir:
218
+ return
219
+
220
+ children = loader()
221
+ for child in children:
222
+ node.add_child(child)
223
+
224
+ node._is_loaded = True
225
+ self._unloaded_nodes.discard(str(node.path))
226
+
227
+ def get_initial_scan_depth(self, estimated_total_items: int) -> int:
228
+ """Get recommended initial scan depth based on estimated size.
229
+
230
+ Args:
231
+ estimated_total_items: Estimated number of items
232
+
233
+ Returns:
234
+ Recommended scan depth
235
+ """
236
+ if estimated_total_items > 100000:
237
+ return 2
238
+ elif estimated_total_items > 10000:
239
+ return 3
240
+ else:
241
+ return self.MAX_INITIAL_DEPTH
242
+
243
+ def limit_recursion_depth(
244
+ self, current_depth: int, max_depth: Optional[int]
245
+ ) -> int:
246
+ """Calculate effective max depth with limits.
247
+
248
+ Args:
249
+ current_depth: Current recursion depth
250
+ max_depth: Requested max depth (None for unlimited)
251
+
252
+ Returns:
253
+ Effective max depth
254
+ """
255
+ if max_depth is None:
256
+ # Apply safety limit for unlimited scans
257
+ return current_depth + 10
258
+
259
+ return max_depth
260
+
261
+
262
+ class LargeDirectoryHandler:
263
+ """Handles special cases for very large directories."""
264
+
265
+ # Threshold for considering a directory "large"
266
+ LARGE_DIR_THRESHOLD: int = 10000
267
+
268
+ def __init__(self) -> None:
269
+ """Initialize large directory handler."""
270
+ self._large_directories: set[str] = set()
271
+ self._item_counts: dict[str, int] = {}
272
+
273
+ def register_directory(self, path: str, item_count: int) -> None:
274
+ """Register a directory with its item count.
275
+
276
+ Args:
277
+ path: Directory path
278
+ item_count: Number of items
279
+ """
280
+ self._item_counts[path] = item_count
281
+
282
+ if item_count > self.LARGE_DIR_THRESHOLD:
283
+ self._large_directories.add(path)
284
+
285
+ def is_large_directory(self, path: str) -> bool:
286
+ """Check if a directory is considered large.
287
+
288
+ Args:
289
+ path: Directory path
290
+
291
+ Returns:
292
+ True if large
293
+ """
294
+ return path in self._large_directories
295
+
296
+ def get_item_count(self, path: str) -> int:
297
+ """Get cached item count for a directory.
298
+
299
+ Args:
300
+ path: Directory path
301
+
302
+ Returns:
303
+ Item count or 0
304
+ """
305
+ return self._item_counts.get(path, 0)
306
+
307
+ def get_memory_usage_estimate(self) -> dict[str, int]:
308
+ """Estimate current memory usage.
309
+
310
+ Returns:
311
+ Dictionary with memory stats in bytes
312
+ """
313
+ # Rough estimates
314
+ node_overhead = sys.getsizeof(object()) + 200 # Base node size
315
+ total_nodes = sum(self._item_counts.values())
316
+
317
+ return {
318
+ "estimated_bytes": total_nodes * node_overhead,
319
+ "total_nodes": total_nodes,
320
+ "large_directories": len(self._large_directories),
321
+ }
322
+
323
+
324
+ def estimate_directory_size(path: str) -> int:
325
+ """Quickly estimate number of items in a directory.
326
+
327
+ Args:
328
+ path: Directory path
329
+
330
+ Returns:
331
+ Estimated item count
332
+ """
333
+ import os
334
+ from pathlib import Path
335
+
336
+ count = 0
337
+ try:
338
+ # Quick scan - only first level
339
+ with os.scandir(path) as entries:
340
+ for entry in entries:
341
+ count += 1
342
+ # Limit quick scan
343
+ if count >= 1000:
344
+ return 1000
345
+ except (OSError, PermissionError):
346
+ pass
347
+
348
+ return count