edsl 0.1.53__py3-none-any.whl → 0.1.55__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.
- edsl/__init__.py +8 -1
- edsl/__init__original.py +134 -0
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +29 -0
- edsl/agents/agent_list.py +36 -1
- edsl/base/base_class.py +281 -151
- edsl/buckets/__init__.py +8 -3
- edsl/buckets/bucket_collection.py +9 -3
- edsl/buckets/model_buckets.py +4 -2
- edsl/buckets/token_bucket.py +2 -2
- edsl/buckets/token_bucket_client.py +5 -3
- edsl/caching/cache.py +131 -62
- edsl/caching/cache_entry.py +70 -58
- edsl/caching/sql_dict.py +17 -0
- edsl/cli.py +99 -0
- edsl/config/config_class.py +16 -0
- edsl/conversation/__init__.py +31 -0
- edsl/coop/coop.py +276 -242
- edsl/coop/coop_jobs_objects.py +59 -0
- edsl/coop/coop_objects.py +29 -0
- edsl/coop/coop_regular_objects.py +26 -0
- edsl/coop/utils.py +24 -19
- edsl/dataset/dataset.py +338 -101
- edsl/db_list/sqlite_list.py +349 -0
- edsl/inference_services/__init__.py +40 -5
- edsl/inference_services/exceptions.py +11 -0
- edsl/inference_services/services/anthropic_service.py +5 -2
- edsl/inference_services/services/aws_bedrock.py +6 -2
- edsl/inference_services/services/azure_ai.py +6 -2
- edsl/inference_services/services/google_service.py +3 -2
- edsl/inference_services/services/mistral_ai_service.py +6 -2
- edsl/inference_services/services/open_ai_service.py +6 -2
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +105 -7
- edsl/interviews/answering_function.py +167 -59
- edsl/interviews/interview.py +124 -72
- edsl/interviews/interview_task_manager.py +10 -0
- edsl/invigilators/invigilators.py +10 -1
- edsl/jobs/async_interview_runner.py +146 -104
- edsl/jobs/data_structures.py +6 -4
- edsl/jobs/decorators.py +61 -0
- edsl/jobs/fetch_invigilator.py +61 -18
- edsl/jobs/html_table_job_logger.py +14 -2
- edsl/jobs/jobs.py +180 -104
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_interview_constructor.py +2 -0
- edsl/jobs/jobs_pricing_estimation.py +127 -46
- edsl/jobs/jobs_remote_inference_logger.py +4 -0
- edsl/jobs/jobs_runner_status.py +30 -25
- edsl/jobs/progress_bar_manager.py +79 -0
- edsl/jobs/remote_inference.py +35 -1
- edsl/key_management/key_lookup_builder.py +6 -1
- edsl/language_models/language_model.py +102 -12
- edsl/language_models/model.py +10 -3
- edsl/language_models/price_manager.py +45 -75
- edsl/language_models/registry.py +5 -0
- edsl/language_models/utilities.py +2 -1
- edsl/notebooks/notebook.py +77 -10
- edsl/questions/VALIDATION_README.md +134 -0
- edsl/questions/__init__.py +24 -1
- edsl/questions/exceptions.py +21 -0
- edsl/questions/question_check_box.py +171 -149
- edsl/questions/question_dict.py +243 -51
- edsl/questions/question_multiple_choice_with_other.py +624 -0
- edsl/questions/question_registry.py +2 -1
- edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
- edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
- edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
- edsl/questions/validation_analysis.py +185 -0
- edsl/questions/validation_cli.py +131 -0
- edsl/questions/validation_html_report.py +404 -0
- edsl/questions/validation_logger.py +136 -0
- edsl/results/result.py +63 -16
- edsl/results/results.py +702 -171
- edsl/scenarios/construct_download_link.py +16 -3
- edsl/scenarios/directory_scanner.py +226 -226
- edsl/scenarios/file_methods.py +5 -0
- edsl/scenarios/file_store.py +117 -6
- edsl/scenarios/handlers/__init__.py +5 -1
- edsl/scenarios/handlers/mp4_file_store.py +104 -0
- edsl/scenarios/handlers/webm_file_store.py +104 -0
- edsl/scenarios/scenario.py +120 -101
- edsl/scenarios/scenario_list.py +800 -727
- edsl/scenarios/scenario_list_gc_test.py +146 -0
- edsl/scenarios/scenario_list_memory_test.py +214 -0
- edsl/scenarios/scenario_list_source_refactor.md +35 -0
- edsl/scenarios/scenario_selector.py +5 -4
- edsl/scenarios/scenario_source.py +1990 -0
- edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
- edsl/surveys/survey.py +22 -0
- edsl/tasks/__init__.py +4 -2
- edsl/tasks/task_history.py +198 -36
- edsl/tests/scenarios/test_ScenarioSource.py +51 -0
- edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
- edsl/utilities/__init__.py +2 -1
- edsl/utilities/decorators.py +121 -0
- edsl/utilities/memory_debugger.py +1010 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/METADATA +52 -76
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/RECORD +102 -78
- edsl/jobs/jobs_runner_asyncio.py +0 -281
- edsl/language_models/unused/fake_openai_service.py +0 -60
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/LICENSE +0 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/WHEEL +0 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1010 @@
|
|
1
|
+
"""
|
2
|
+
Memory debugging utilities for analyzing object references and memory leaks.
|
3
|
+
"""
|
4
|
+
import gc
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
import time
|
8
|
+
import types
|
9
|
+
import inspect
|
10
|
+
import webbrowser
|
11
|
+
from pathlib import Path
|
12
|
+
import base64
|
13
|
+
import html
|
14
|
+
from datetime import datetime
|
15
|
+
from typing import Any, Set, List, Dict, Optional, Tuple, Union
|
16
|
+
|
17
|
+
# Try to import objgraph, which is only available as a dev dependency
|
18
|
+
try:
|
19
|
+
import objgraph
|
20
|
+
OBJGRAPH_AVAILABLE = True
|
21
|
+
except ImportError:
|
22
|
+
OBJGRAPH_AVAILABLE = False
|
23
|
+
|
24
|
+
class MemoryDebugger:
|
25
|
+
"""
|
26
|
+
A class for debugging memory issues and analyzing object references.
|
27
|
+
|
28
|
+
This class provides utilities to:
|
29
|
+
- Inspect objects referring to a target object
|
30
|
+
- Detect reference cycles
|
31
|
+
- Visualize object dependencies
|
32
|
+
- Analyze memory usage patterns
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, target_obj: Any):
|
36
|
+
"""
|
37
|
+
Initialize the debugger with a target object to analyze.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
target_obj: The object to inspect for memory issues
|
41
|
+
"""
|
42
|
+
self.target_obj = target_obj
|
43
|
+
|
44
|
+
def _get_source_info(self, obj: Any) -> Dict[str, Any]:
|
45
|
+
"""Get source code information for an object if available."""
|
46
|
+
result = {
|
47
|
+
'module': getattr(obj, '__module__', 'unknown'),
|
48
|
+
'file': 'unknown',
|
49
|
+
'line': 0,
|
50
|
+
'source': None,
|
51
|
+
'has_source': False,
|
52
|
+
'type_str': str(type(obj))
|
53
|
+
}
|
54
|
+
|
55
|
+
try:
|
56
|
+
# Try to get the source file and line number
|
57
|
+
if hasattr(obj, '__class__'):
|
58
|
+
module = inspect.getmodule(obj.__class__)
|
59
|
+
if module:
|
60
|
+
result['module'] = module.__name__
|
61
|
+
try:
|
62
|
+
file = inspect.getsourcefile(obj.__class__)
|
63
|
+
if file:
|
64
|
+
result['file'] = file
|
65
|
+
try:
|
66
|
+
_, line = inspect.getsourcelines(obj.__class__)
|
67
|
+
result['line'] = line
|
68
|
+
result['has_source'] = True
|
69
|
+
except Exception:
|
70
|
+
pass
|
71
|
+
except Exception:
|
72
|
+
pass
|
73
|
+
except Exception:
|
74
|
+
# Silently fail if we can't get source info
|
75
|
+
pass
|
76
|
+
|
77
|
+
return result
|
78
|
+
|
79
|
+
def _generate_html_report(self, prefix: str = "", output_dir: str = None) -> Tuple[str, str, str]:
|
80
|
+
"""
|
81
|
+
Generate a comprehensive HTML memory debugging report.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
prefix: Optional prefix for output files. If empty, uses target object type.
|
85
|
+
output_dir: Optional directory to write files to. If None, uses tempdir or current directory.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
A tuple containing (html_filename, refs_graph_filename, backrefs_graph_filename)
|
89
|
+
"""
|
90
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
91
|
+
if not prefix:
|
92
|
+
prefix = type(self.target_obj).__name__.lower()
|
93
|
+
|
94
|
+
# Determine output directory
|
95
|
+
if output_dir is None:
|
96
|
+
output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
|
97
|
+
|
98
|
+
if output_dir:
|
99
|
+
# Ensure directory exists
|
100
|
+
os.makedirs(output_dir, exist_ok=True)
|
101
|
+
|
102
|
+
# Prepare filenames
|
103
|
+
html_filename = os.path.join(output_dir, f"{prefix}_memory_debug_{timestamp}.html") if output_dir else f"{prefix}_memory_debug_{timestamp}.html"
|
104
|
+
refs_graph_filename = os.path.join(output_dir, f"{prefix}_outgoing_refs_{timestamp}.png") if output_dir else f"{prefix}_outgoing_refs_{timestamp}.png"
|
105
|
+
backrefs_graph_filename = os.path.join(output_dir, f"{prefix}_incoming_refs_{timestamp}.png") if output_dir else f"{prefix}_incoming_refs_{timestamp}.png"
|
106
|
+
|
107
|
+
# Generate object graphs - both directions
|
108
|
+
graph_exists = False
|
109
|
+
backrefs_graph_exists = False
|
110
|
+
|
111
|
+
if OBJGRAPH_AVAILABLE:
|
112
|
+
try:
|
113
|
+
# Use objgraph's backup function to filter frames and functions
|
114
|
+
def skip_frames(obj):
|
115
|
+
return not isinstance(obj, (types.FrameType, types.FunctionType))
|
116
|
+
|
117
|
+
# Outgoing references (what this object references)
|
118
|
+
objgraph.show_refs(
|
119
|
+
self.target_obj,
|
120
|
+
filename=refs_graph_filename,
|
121
|
+
max_depth=3,
|
122
|
+
extra_ignore=[id(obj) for obj in gc.get_objects()
|
123
|
+
if isinstance(obj, (types.FrameType, types.FunctionType))]
|
124
|
+
)
|
125
|
+
graph_exists = True
|
126
|
+
|
127
|
+
# Incoming references (what references this object)
|
128
|
+
objgraph.show_backrefs(
|
129
|
+
self.target_obj,
|
130
|
+
filename=backrefs_graph_filename,
|
131
|
+
max_depth=3,
|
132
|
+
extra_ignore=[id(obj) for obj in gc.get_objects()
|
133
|
+
if isinstance(obj, (types.FrameType, types.FunctionType))]
|
134
|
+
)
|
135
|
+
backrefs_graph_exists = True
|
136
|
+
except Exception as e:
|
137
|
+
print(f"Warning: Could not generate object graph visualization: {e}")
|
138
|
+
graph_exists = False
|
139
|
+
else:
|
140
|
+
print("Warning: objgraph package is not available. Install it with 'pip install objgraph' to enable visualizations.")
|
141
|
+
|
142
|
+
# Get all reference cycle information
|
143
|
+
import io
|
144
|
+
from contextlib import redirect_stdout
|
145
|
+
|
146
|
+
captured_output = io.StringIO()
|
147
|
+
with redirect_stdout(captured_output):
|
148
|
+
cycles = self.detect_reference_cycles()
|
149
|
+
|
150
|
+
cycle_output = captured_output.getvalue()
|
151
|
+
|
152
|
+
# Get referrers and referents with source info
|
153
|
+
referrers = gc.get_referrers(self.target_obj)
|
154
|
+
referents = gc.get_referents(self.target_obj)
|
155
|
+
|
156
|
+
# Skip frames and function objects for referrers
|
157
|
+
filtered_referrers = [ref for ref in referrers if not isinstance(ref, (types.FrameType, types.FunctionType))]
|
158
|
+
|
159
|
+
# Get source info for all objects
|
160
|
+
referrer_info = [self._get_source_info(ref) for ref in filtered_referrers]
|
161
|
+
referent_info = [self._get_source_info(ref) for ref in referents]
|
162
|
+
|
163
|
+
# Get target object info
|
164
|
+
target_info = self._get_source_info(self.target_obj)
|
165
|
+
|
166
|
+
# Generate HTML content
|
167
|
+
html_content = f"""<!DOCTYPE html>
|
168
|
+
<html lang="en">
|
169
|
+
<head>
|
170
|
+
<meta charset="UTF-8">
|
171
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
172
|
+
<title>Memory Debug Report - {type(self.target_obj).__name__}</title>
|
173
|
+
<style>
|
174
|
+
body {{
|
175
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
176
|
+
line-height: 1.6;
|
177
|
+
color: #333;
|
178
|
+
max-width: 1200px;
|
179
|
+
margin: 0 auto;
|
180
|
+
padding: 20px;
|
181
|
+
}}
|
182
|
+
h1, h2, h3 {{
|
183
|
+
color: #2c3e50;
|
184
|
+
}}
|
185
|
+
h1 {{
|
186
|
+
border-bottom: 2px solid #eaecef;
|
187
|
+
padding-bottom: 10px;
|
188
|
+
}}
|
189
|
+
h2 {{
|
190
|
+
margin-top: 30px;
|
191
|
+
border-bottom: 1px solid #eaecef;
|
192
|
+
padding-bottom: 5px;
|
193
|
+
}}
|
194
|
+
.info-box {{
|
195
|
+
background-color: #f8f9fa;
|
196
|
+
border: 1px solid #e9ecef;
|
197
|
+
border-radius: 5px;
|
198
|
+
padding: 15px;
|
199
|
+
margin: 20px 0;
|
200
|
+
}}
|
201
|
+
.warning {{
|
202
|
+
background-color: #fff3cd;
|
203
|
+
border-color: #ffeeba;
|
204
|
+
}}
|
205
|
+
.object {{
|
206
|
+
margin-bottom: 10px;
|
207
|
+
padding: 10px;
|
208
|
+
background-color: #f8f9fa;
|
209
|
+
border-left: 4px solid #007bff;
|
210
|
+
border-radius: 3px;
|
211
|
+
}}
|
212
|
+
.object-header {{
|
213
|
+
display: flex;
|
214
|
+
justify-content: space-between;
|
215
|
+
font-weight: bold;
|
216
|
+
}}
|
217
|
+
.object-details {{
|
218
|
+
margin-top: 5px;
|
219
|
+
font-size: 0.9em;
|
220
|
+
}}
|
221
|
+
.file-link {{
|
222
|
+
color: #007bff;
|
223
|
+
text-decoration: none;
|
224
|
+
}}
|
225
|
+
.file-link:hover {{
|
226
|
+
text-decoration: underline;
|
227
|
+
}}
|
228
|
+
.unhashable {{
|
229
|
+
border-left-color: #dc3545;
|
230
|
+
}}
|
231
|
+
.cycle {{
|
232
|
+
border-left-color: #fd7e14;
|
233
|
+
}}
|
234
|
+
.tab {{
|
235
|
+
overflow: hidden;
|
236
|
+
border: 1px solid #ccc;
|
237
|
+
background-color: #f1f1f1;
|
238
|
+
border-radius: 5px 5px 0 0;
|
239
|
+
}}
|
240
|
+
.tab button {{
|
241
|
+
background-color: inherit;
|
242
|
+
float: left;
|
243
|
+
border: none;
|
244
|
+
outline: none;
|
245
|
+
cursor: pointer;
|
246
|
+
padding: 10px 16px;
|
247
|
+
transition: 0.3s;
|
248
|
+
}}
|
249
|
+
.tab button:hover {{
|
250
|
+
background-color: #ddd;
|
251
|
+
}}
|
252
|
+
.tab button.active {{
|
253
|
+
background-color: #ccc;
|
254
|
+
}}
|
255
|
+
.tabcontent {{
|
256
|
+
display: none;
|
257
|
+
padding: 20px;
|
258
|
+
border: 1px solid #ccc;
|
259
|
+
border-top: none;
|
260
|
+
border-radius: 0 0 5px 5px;
|
261
|
+
}}
|
262
|
+
.code {{
|
263
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
264
|
+
background-color: #f6f8fa;
|
265
|
+
padding: 10px;
|
266
|
+
border-radius: 3px;
|
267
|
+
overflow-x: auto;
|
268
|
+
}}
|
269
|
+
.graph-container {{
|
270
|
+
text-align: center;
|
271
|
+
margin: 20px 0;
|
272
|
+
}}
|
273
|
+
table {{
|
274
|
+
border-collapse: collapse;
|
275
|
+
width: 100%;
|
276
|
+
margin: 20px 0;
|
277
|
+
}}
|
278
|
+
th, td {{
|
279
|
+
border: 1px solid #ddd;
|
280
|
+
padding: 8px;
|
281
|
+
text-align: left;
|
282
|
+
}}
|
283
|
+
th {{
|
284
|
+
background-color: #f2f2f2;
|
285
|
+
}}
|
286
|
+
tr:nth-child(even) {{
|
287
|
+
background-color: #f9f9f9;
|
288
|
+
}}
|
289
|
+
.toggle-button {{
|
290
|
+
background-color: #4CAF50;
|
291
|
+
border: none;
|
292
|
+
color: white;
|
293
|
+
padding: 5px 10px;
|
294
|
+
text-align: center;
|
295
|
+
text-decoration: none;
|
296
|
+
display: inline-block;
|
297
|
+
font-size: 12px;
|
298
|
+
margin: 4px 2px;
|
299
|
+
cursor: pointer;
|
300
|
+
border-radius: 3px;
|
301
|
+
}}
|
302
|
+
</style>
|
303
|
+
</head>
|
304
|
+
<body>
|
305
|
+
<h1>Memory Debug Report - {type(self.target_obj).__name__}</h1>
|
306
|
+
<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
307
|
+
|
308
|
+
<div class="info-box">
|
309
|
+
<h3>Target Object Information</h3>
|
310
|
+
<p><strong>Type:</strong> {type(self.target_obj)}</p>
|
311
|
+
<p><strong>ID:</strong> {id(self.target_obj)}</p>
|
312
|
+
<p><strong>Module:</strong> {target_info['module']}</p>
|
313
|
+
<p><strong>Reference Count:</strong> {sys.getrefcount(self.target_obj)}</p>
|
314
|
+
{f'<p><strong>Source File:</strong> <a href="file://{target_info["file"]}" class="file-link">{os.path.basename(target_info["file"])}</a> (line {target_info["line"]})</p>' if target_info["has_source"] else ''}
|
315
|
+
</div>
|
316
|
+
|
317
|
+
<div class="tab">
|
318
|
+
<button class="tablinks" onclick="openTab(event, 'Overview')" id="defaultOpen">Overview</button>
|
319
|
+
<button class="tablinks" onclick="openTab(event, 'Referrers')">Referrers ({len(filtered_referrers)})</button>
|
320
|
+
<button class="tablinks" onclick="openTab(event, 'Referents')">Referents ({len(referents)})</button>
|
321
|
+
<button class="tablinks" onclick="openTab(event, 'Cycles')" {'style="color: red;"' if cycles else ''}>Cycles {f"({len(cycles)})" if cycles else ""}</button>
|
322
|
+
<button class="tablinks" onclick="openTab(event, 'Graph')">Reference Visualizations</button>
|
323
|
+
</div>
|
324
|
+
|
325
|
+
<div id="Overview" class="tabcontent">
|
326
|
+
<h2>Memory Analysis Overview</h2>
|
327
|
+
<div class="info-box">
|
328
|
+
<p>This report analyzes the memory references to and from the target object. It helps identify potential memory leaks by showing object relationships.</p>
|
329
|
+
<p><strong>Total referrers:</strong> {len(filtered_referrers)}</p>
|
330
|
+
<p><strong>Total referents:</strong> {len(referents)}</p>
|
331
|
+
<p><strong>Potential reference cycles:</strong> {len(cycles) if cycles else "None detected"}</p>
|
332
|
+
</div>
|
333
|
+
|
334
|
+
<h3>Common Types</h3>
|
335
|
+
<table>
|
336
|
+
<tr>
|
337
|
+
<th>Type</th>
|
338
|
+
<th>Count as Referrer</th>
|
339
|
+
<th>Count as Referent</th>
|
340
|
+
</tr>
|
341
|
+
"""
|
342
|
+
|
343
|
+
# Count types
|
344
|
+
referrer_types = {}
|
345
|
+
for ref in filtered_referrers:
|
346
|
+
type_name = type(ref).__name__
|
347
|
+
referrer_types[type_name] = referrer_types.get(type_name, 0) + 1
|
348
|
+
|
349
|
+
referent_types = {}
|
350
|
+
for ref in referents:
|
351
|
+
type_name = type(ref).__name__
|
352
|
+
referent_types[type_name] = referent_types.get(type_name, 0) + 1
|
353
|
+
|
354
|
+
# Get all unique types
|
355
|
+
all_types = set(list(referrer_types.keys()) + list(referent_types.keys()))
|
356
|
+
|
357
|
+
# Add type rows to the table
|
358
|
+
for type_name in sorted(all_types):
|
359
|
+
html_content += f"""
|
360
|
+
<tr>
|
361
|
+
<td>{type_name}</td>
|
362
|
+
<td>{referrer_types.get(type_name, 0)}</td>
|
363
|
+
<td>{referent_types.get(type_name, 0)}</td>
|
364
|
+
</tr>"""
|
365
|
+
|
366
|
+
html_content += """
|
367
|
+
</table>
|
368
|
+
</div>
|
369
|
+
|
370
|
+
<div id="Referrers" class="tabcontent">
|
371
|
+
<h2>Objects Referring to Target</h2>
|
372
|
+
<p>These objects hold references to the target object:</p>
|
373
|
+
"""
|
374
|
+
|
375
|
+
# Add referrers
|
376
|
+
for i, (ref, info) in enumerate(zip(filtered_referrers, referrer_info)):
|
377
|
+
object_id = id(ref)
|
378
|
+
object_type = type(ref).__name__
|
379
|
+
is_unhashable = not self._is_hashable(ref)
|
380
|
+
|
381
|
+
html_content += f"""
|
382
|
+
<div class="object {'unhashable' if is_unhashable else ''}">
|
383
|
+
<div class="object-header">
|
384
|
+
<span>[{i}] {object_type}{' (unhashable)' if is_unhashable else ''}</span>
|
385
|
+
<span>ID: {object_id}</span>
|
386
|
+
</div>
|
387
|
+
<div class="object-details">
|
388
|
+
<p><strong>Module:</strong> {info['module']}</p>
|
389
|
+
{f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}"""
|
390
|
+
|
391
|
+
# Add more specific information based on type
|
392
|
+
if isinstance(ref, dict):
|
393
|
+
html_content += f"""
|
394
|
+
<p><strong>Dict size:</strong> {len(ref)} items</p>
|
395
|
+
<button class="toggle-button" onclick="toggleDetails('referrer-{i}')">Show/Hide Contents</button>
|
396
|
+
<div id="referrer-{i}" style="display: none;">
|
397
|
+
<table>
|
398
|
+
<tr><th>Key</th><th>Value Type</th></tr>"""
|
399
|
+
|
400
|
+
for k, v in list(ref.items())[:20]: # Limit to 20 items
|
401
|
+
# Skip showing the target object itself
|
402
|
+
if v is self.target_obj:
|
403
|
+
key_html = html.escape(str(k))
|
404
|
+
html_content += f"""
|
405
|
+
<tr><td>{key_html}</td><td><strong>TARGET OBJECT</strong></td></tr>"""
|
406
|
+
else:
|
407
|
+
key_html = html.escape(str(k))
|
408
|
+
html_content += f"""
|
409
|
+
<tr><td>{key_html}</td><td>{type(v).__name__}</td></tr>"""
|
410
|
+
|
411
|
+
if len(ref) > 20:
|
412
|
+
html_content += f"""
|
413
|
+
<tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
|
414
|
+
|
415
|
+
html_content += """
|
416
|
+
</table>
|
417
|
+
</div>"""
|
418
|
+
elif isinstance(ref, (list, tuple)):
|
419
|
+
html_content += f"""
|
420
|
+
<p><strong>{type(ref).__name__} size:</strong> {len(ref)} items</p>
|
421
|
+
<button class="toggle-button" onclick="toggleDetails('referrer-{i}')">Show/Hide Contents</button>
|
422
|
+
<div id="referrer-{i}" style="display: none;">
|
423
|
+
<table>
|
424
|
+
<tr><th>Index</th><th>Value Type</th></tr>"""
|
425
|
+
|
426
|
+
for idx, item in enumerate(ref[:20]): # Limit to 20 items
|
427
|
+
if item is self.target_obj:
|
428
|
+
html_content += f"""
|
429
|
+
<tr><td>{idx}</td><td><strong>TARGET OBJECT</strong></td></tr>"""
|
430
|
+
else:
|
431
|
+
html_content += f"""
|
432
|
+
<tr><td>{idx}</td><td>{type(item).__name__}</td></tr>"""
|
433
|
+
|
434
|
+
if len(ref) > 20:
|
435
|
+
html_content += f"""
|
436
|
+
<tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
|
437
|
+
|
438
|
+
html_content += """
|
439
|
+
</table>
|
440
|
+
</div>"""
|
441
|
+
|
442
|
+
html_content += """
|
443
|
+
</div>
|
444
|
+
</div>"""
|
445
|
+
|
446
|
+
html_content += """
|
447
|
+
</div>
|
448
|
+
|
449
|
+
<div id="Referents" class="tabcontent">
|
450
|
+
<h2>Objects Referenced by Target</h2>
|
451
|
+
<p>These objects are referenced by the target object:</p>
|
452
|
+
"""
|
453
|
+
|
454
|
+
# Add referents
|
455
|
+
for i, (ref, info) in enumerate(zip(referents, referent_info)):
|
456
|
+
object_id = id(ref)
|
457
|
+
object_type = type(ref).__name__
|
458
|
+
is_unhashable = not self._is_hashable(ref)
|
459
|
+
|
460
|
+
html_content += f"""
|
461
|
+
<div class="object {'unhashable' if is_unhashable else ''}">
|
462
|
+
<div class="object-header">
|
463
|
+
<span>[{i}] {object_type}{' (unhashable)' if is_unhashable else ''}</span>
|
464
|
+
<span>ID: {object_id}</span>
|
465
|
+
</div>
|
466
|
+
<div class="object-details">
|
467
|
+
<p><strong>Module:</strong> {info['module']}</p>
|
468
|
+
{f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}"""
|
469
|
+
|
470
|
+
# Add specific information for dicts and sequences
|
471
|
+
if isinstance(ref, dict):
|
472
|
+
html_content += f"""
|
473
|
+
<p><strong>Dict size:</strong> {len(ref)} items</p>
|
474
|
+
<button class="toggle-button" onclick="toggleDetails('referent-{i}')">Show/Hide Contents</button>
|
475
|
+
<div id="referent-{i}" style="display: none;">
|
476
|
+
<table>
|
477
|
+
<tr><th>Key</th><th>Value Type</th></tr>"""
|
478
|
+
|
479
|
+
for k, v in list(ref.items())[:20]: # Limit to 20 items
|
480
|
+
key_html = html.escape(str(k))
|
481
|
+
html_content += f"""
|
482
|
+
<tr><td>{key_html}</td><td>{type(v).__name__}</td></tr>"""
|
483
|
+
|
484
|
+
if len(ref) > 20:
|
485
|
+
html_content += f"""
|
486
|
+
<tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
|
487
|
+
|
488
|
+
html_content += """
|
489
|
+
</table>
|
490
|
+
</div>"""
|
491
|
+
elif isinstance(ref, (list, tuple)):
|
492
|
+
html_content += f"""
|
493
|
+
<p><strong>{type(ref).__name__} size:</strong> {len(ref)} items</p>
|
494
|
+
<button class="toggle-button" onclick="toggleDetails('referent-{i}')">Show/Hide Contents</button>
|
495
|
+
<div id="referent-{i}" style="display: none;">
|
496
|
+
<table>
|
497
|
+
<tr><th>Index</th><th>Value Type</th></tr>"""
|
498
|
+
|
499
|
+
for idx, item in enumerate(ref[:20]): # Limit to 20 items
|
500
|
+
html_content += f"""
|
501
|
+
<tr><td>{idx}</td><td>{type(item).__name__}</td></tr>"""
|
502
|
+
|
503
|
+
if len(ref) > 20:
|
504
|
+
html_content += f"""
|
505
|
+
<tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
|
506
|
+
|
507
|
+
html_content += """
|
508
|
+
</table>
|
509
|
+
</div>"""
|
510
|
+
|
511
|
+
html_content += """
|
512
|
+
</div>
|
513
|
+
</div>"""
|
514
|
+
|
515
|
+
html_content += """
|
516
|
+
</div>
|
517
|
+
|
518
|
+
<div id="Cycles" class="tabcontent">
|
519
|
+
<h2>Reference Cycles</h2>
|
520
|
+
"""
|
521
|
+
|
522
|
+
# Add reference cycle output
|
523
|
+
if cycles:
|
524
|
+
html_content += f"""
|
525
|
+
<div class="info-box warning">
|
526
|
+
<p>Found {len(cycles)} potential reference cycles. These objects might be causing memory leaks.</p>
|
527
|
+
</div>
|
528
|
+
"""
|
529
|
+
# Add cycle details
|
530
|
+
for i, obj in enumerate(cycles):
|
531
|
+
object_type = type(obj).__name__
|
532
|
+
object_id = id(obj)
|
533
|
+
info = self._get_source_info(obj)
|
534
|
+
|
535
|
+
html_content += f"""
|
536
|
+
<div class="object cycle">
|
537
|
+
<div class="object-header">
|
538
|
+
<span>[{i}] {object_type}</span>
|
539
|
+
<span>ID: {object_id}</span>
|
540
|
+
</div>
|
541
|
+
<div class="object-details">
|
542
|
+
<p><strong>Module:</strong> {info['module']}</p>
|
543
|
+
{f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}
|
544
|
+
</div>
|
545
|
+
</div>"""
|
546
|
+
else:
|
547
|
+
html_content += """
|
548
|
+
<p>No reference cycles detected among hashable objects.</p>
|
549
|
+
"""
|
550
|
+
|
551
|
+
# Add unhashable object analysis
|
552
|
+
html_content += """
|
553
|
+
<h3>Unhashable Object Analysis</h3>
|
554
|
+
<div class="code">
|
555
|
+
"""
|
556
|
+
for line in cycle_output.splitlines():
|
557
|
+
html_content += f"{html.escape(line)}<br>"
|
558
|
+
|
559
|
+
html_content += """
|
560
|
+
</div>
|
561
|
+
</div>
|
562
|
+
|
563
|
+
<div id="Graph" class="tabcontent">
|
564
|
+
<h2>Object Graph Visualization</h2>
|
565
|
+
|
566
|
+
<h3>Outgoing References (Objects Referenced By Target)</h3>
|
567
|
+
<p>This graph shows what objects are referenced by the target object:</p>
|
568
|
+
"""
|
569
|
+
|
570
|
+
if graph_exists:
|
571
|
+
try:
|
572
|
+
with open(refs_graph_filename, 'rb') as f:
|
573
|
+
img_data = base64.b64encode(f.read()).decode('utf-8')
|
574
|
+
html_content += f"""
|
575
|
+
<div class="graph-container">
|
576
|
+
<img src="data:image/png;base64,{img_data}" alt="Objects referenced by target" style="max-width: 100%;">
|
577
|
+
</div>
|
578
|
+
<p><a href="file://{os.path.abspath(refs_graph_filename)}" target="_blank">Open full-size outgoing references graph</a></p>
|
579
|
+
"""
|
580
|
+
except Exception as e:
|
581
|
+
html_content += f"""
|
582
|
+
<div class="info-box warning">
|
583
|
+
<p>Could not embed outgoing references graph image: {e}</p>
|
584
|
+
<p><a href="file://{os.path.abspath(refs_graph_filename)}" target="_blank">Open outgoing references graph image</a></p>
|
585
|
+
</div>
|
586
|
+
"""
|
587
|
+
else:
|
588
|
+
html_content += """
|
589
|
+
<div class="info-box warning">
|
590
|
+
<p>Could not generate outgoing references graph visualization.</p>
|
591
|
+
</div>
|
592
|
+
"""
|
593
|
+
|
594
|
+
html_content += """
|
595
|
+
<h3>Incoming References (Objects Referencing Target)</h3>
|
596
|
+
<p>This graph shows what objects have a strong reference to the target object:</p>
|
597
|
+
"""
|
598
|
+
|
599
|
+
if backrefs_graph_exists:
|
600
|
+
try:
|
601
|
+
with open(backrefs_graph_filename, 'rb') as f:
|
602
|
+
img_data = base64.b64encode(f.read()).decode('utf-8')
|
603
|
+
html_content += f"""
|
604
|
+
<div class="graph-container">
|
605
|
+
<img src="data:image/png;base64,{img_data}" alt="Objects referencing the target" style="max-width: 100%;">
|
606
|
+
</div>
|
607
|
+
<p><a href="file://{os.path.abspath(backrefs_graph_filename)}" target="_blank">Open full-size incoming references graph</a></p>
|
608
|
+
"""
|
609
|
+
except Exception as e:
|
610
|
+
html_content += f"""
|
611
|
+
<div class="info-box warning">
|
612
|
+
<p>Could not embed incoming references graph image: {e}</p>
|
613
|
+
<p><a href="file://{os.path.abspath(backrefs_graph_filename)}" target="_blank">Open incoming references graph image</a></p>
|
614
|
+
</div>
|
615
|
+
"""
|
616
|
+
else:
|
617
|
+
html_content += """
|
618
|
+
<div class="info-box warning">
|
619
|
+
<p>Could not generate incoming references graph visualization.</p>
|
620
|
+
</div>
|
621
|
+
"""
|
622
|
+
|
623
|
+
html_content += """
|
624
|
+
</div>
|
625
|
+
|
626
|
+
<script>
|
627
|
+
function openTab(evt, tabName) {
|
628
|
+
var i, tabcontent, tablinks;
|
629
|
+
tabcontent = document.getElementsByClassName("tabcontent");
|
630
|
+
for (i = 0; i < tabcontent.length; i++) {
|
631
|
+
tabcontent[i].style.display = "none";
|
632
|
+
}
|
633
|
+
tablinks = document.getElementsByClassName("tablinks");
|
634
|
+
for (i = 0; i < tablinks.length; i++) {
|
635
|
+
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
636
|
+
}
|
637
|
+
document.getElementById(tabName).style.display = "block";
|
638
|
+
evt.currentTarget.className += " active";
|
639
|
+
}
|
640
|
+
|
641
|
+
function toggleDetails(id) {
|
642
|
+
var element = document.getElementById(id);
|
643
|
+
if (element.style.display === "none") {
|
644
|
+
element.style.display = "block";
|
645
|
+
} else {
|
646
|
+
element.style.display = "none";
|
647
|
+
}
|
648
|
+
}
|
649
|
+
|
650
|
+
// Open the default tab
|
651
|
+
document.getElementById("defaultOpen").click();
|
652
|
+
</script>
|
653
|
+
</body>
|
654
|
+
</html>
|
655
|
+
"""
|
656
|
+
|
657
|
+
# Write HTML to file
|
658
|
+
with open(html_filename, 'w') as f:
|
659
|
+
f.write(html_content)
|
660
|
+
|
661
|
+
return html_filename, refs_graph_filename, backrefs_graph_filename
|
662
|
+
|
663
|
+
def debug_memory(self, prefix: str = "", open_browser: bool = True, output_dir: str = None) -> str:
|
664
|
+
"""
|
665
|
+
Comprehensive memory debugging that writes results to files and opens an HTML report.
|
666
|
+
|
667
|
+
Args:
|
668
|
+
prefix: Optional prefix for output files. If empty, uses target object type.
|
669
|
+
open_browser: Whether to automatically open the HTML report in a browser.
|
670
|
+
output_dir: Optional directory to write files to. If None, uses EDSL_MEMORY_DEBUG_DIR
|
671
|
+
environment variable or current directory.
|
672
|
+
|
673
|
+
Returns:
|
674
|
+
The path to the HTML report file.
|
675
|
+
"""
|
676
|
+
# Generate HTML report with both incoming and outgoing reference visualizations
|
677
|
+
html_filename, outgoing_refs_filename, incoming_refs_filename = self._generate_html_report(prefix, output_dir)
|
678
|
+
|
679
|
+
# Also create the legacy markdown report for backward compatibility
|
680
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
681
|
+
if not prefix:
|
682
|
+
prefix = type(self.target_obj).__name__.lower()
|
683
|
+
|
684
|
+
# Determine output directory
|
685
|
+
if output_dir is None:
|
686
|
+
output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
|
687
|
+
|
688
|
+
# Write reference analysis to markdown file
|
689
|
+
md_filename = os.path.join(output_dir, f"{prefix}_memory_debug_{timestamp}.md") if output_dir else f"{prefix}_memory_debug_{timestamp}.md"
|
690
|
+
with open(md_filename, 'w') as f:
|
691
|
+
f.write(f"# Memory Debug Report for {type(self.target_obj)}\n\n")
|
692
|
+
f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
693
|
+
f.write(f"HTML Report: [View HTML Report]({html_filename})\n\n")
|
694
|
+
f.write(f"Visualizations:\n")
|
695
|
+
f.write(f"- [Outgoing References Graph]({outgoing_refs_filename}) - Objects referenced by the target\n")
|
696
|
+
f.write(f"- [Incoming References Graph]({incoming_refs_filename}) - Objects that reference the target\n\n")
|
697
|
+
|
698
|
+
# Capture reference count
|
699
|
+
f.write(f"## Reference Count\n")
|
700
|
+
f.write(f"Current reference count: {sys.getrefcount(self.target_obj)}\n\n")
|
701
|
+
|
702
|
+
# Capture referrers
|
703
|
+
f.write(f"## Objects Referring to Target\n")
|
704
|
+
referrers = gc.get_referrers(self.target_obj)
|
705
|
+
for ref in referrers:
|
706
|
+
if isinstance(ref, (types.FrameType, types.FunctionType)):
|
707
|
+
continue
|
708
|
+
f.write(f"- Type: {type(ref)}\n")
|
709
|
+
|
710
|
+
# Capture reference cycles
|
711
|
+
f.write(f"\n## Reference Cycles\n")
|
712
|
+
|
713
|
+
# Get all reference cycle information (capture stdout for the detailed info)
|
714
|
+
import io
|
715
|
+
from contextlib import redirect_stdout
|
716
|
+
|
717
|
+
captured_output = io.StringIO()
|
718
|
+
with redirect_stdout(captured_output):
|
719
|
+
cycles = self.detect_reference_cycles()
|
720
|
+
|
721
|
+
# Write the cycle information to file
|
722
|
+
cycle_output = captured_output.getvalue()
|
723
|
+
if cycles:
|
724
|
+
f.write(f"Found {len(cycles)} potential reference cycles:\n")
|
725
|
+
for obj in cycles:
|
726
|
+
f.write(f"- Type: {type(obj)}, ID: {id(obj)}\n")
|
727
|
+
else:
|
728
|
+
f.write("No reference cycles detected among hashable objects\n")
|
729
|
+
|
730
|
+
# Add the detailed unhashable information
|
731
|
+
f.write("\n## Unhashable Object Analysis\n")
|
732
|
+
f.write(cycle_output)
|
733
|
+
|
734
|
+
# Open the HTML report in a browser if requested
|
735
|
+
if open_browser:
|
736
|
+
try:
|
737
|
+
webbrowser.open(f"file://{os.path.abspath(html_filename)}")
|
738
|
+
print(f"Opened memory report in browser: {html_filename}")
|
739
|
+
except Exception as e:
|
740
|
+
print(f"Could not open browser: {e}")
|
741
|
+
print(f"Report saved to: {html_filename}")
|
742
|
+
else:
|
743
|
+
print(f"HTML Report saved to: {html_filename}")
|
744
|
+
|
745
|
+
return html_filename
|
746
|
+
|
747
|
+
def inspect_references(self, skip_frames: bool = True) -> None:
|
748
|
+
"""
|
749
|
+
Inspect what objects are referring to the target object.
|
750
|
+
|
751
|
+
Args:
|
752
|
+
skip_frames: If True, skip function frames and local namespaces
|
753
|
+
"""
|
754
|
+
print(f"\nReference count for {type(self.target_obj)}: {sys.getrefcount(self.target_obj)}")
|
755
|
+
print("\nObjects referring to this object:")
|
756
|
+
|
757
|
+
referrers = gc.get_referrers(self.target_obj)
|
758
|
+
for ref in referrers:
|
759
|
+
# Skip frames and function locals if requested
|
760
|
+
if skip_frames and (isinstance(ref, (types.FrameType, types.FunctionType)) or
|
761
|
+
(isinstance(ref, dict) and ref.get('target_obj') is self.target_obj)):
|
762
|
+
continue
|
763
|
+
|
764
|
+
print(f"\nType: {type(ref)}")
|
765
|
+
|
766
|
+
if isinstance(ref, dict):
|
767
|
+
self._inspect_dict_reference(ref)
|
768
|
+
elif isinstance(ref, list):
|
769
|
+
self._inspect_list_reference(ref)
|
770
|
+
elif isinstance(ref, tuple):
|
771
|
+
self._inspect_tuple_reference(ref)
|
772
|
+
else:
|
773
|
+
print(f" - {ref}")
|
774
|
+
|
775
|
+
def detect_reference_cycles(self) -> Set[Any]:
|
776
|
+
"""
|
777
|
+
Detect potential reference cycles involving the target object.
|
778
|
+
|
779
|
+
Returns:
|
780
|
+
Set of objects that are part of potential reference cycles
|
781
|
+
"""
|
782
|
+
referrers = gc.get_referrers(self.target_obj)
|
783
|
+
referents = gc.get_referents(self.target_obj)
|
784
|
+
|
785
|
+
# Separate hashable and unhashable objects
|
786
|
+
hashable_referrers = []
|
787
|
+
unhashable_referrers = []
|
788
|
+
hashable_referents = []
|
789
|
+
unhashable_referents = []
|
790
|
+
|
791
|
+
for obj in referrers:
|
792
|
+
if self._is_hashable(obj):
|
793
|
+
hashable_referrers.append(obj)
|
794
|
+
else:
|
795
|
+
unhashable_referrers.append(obj)
|
796
|
+
|
797
|
+
for obj in referents:
|
798
|
+
if self._is_hashable(obj):
|
799
|
+
hashable_referents.append(obj)
|
800
|
+
else:
|
801
|
+
unhashable_referents.append(obj)
|
802
|
+
|
803
|
+
# Find common objects among hashable ones
|
804
|
+
common_objects = set(hashable_referrers) & set(hashable_referents)
|
805
|
+
|
806
|
+
if common_objects:
|
807
|
+
print(f"Potential reference cycle detected! Found {len(common_objects)} common objects")
|
808
|
+
for shared_obj in common_objects:
|
809
|
+
print(f"Type: {type(shared_obj)}, ID: {id(shared_obj)}")
|
810
|
+
|
811
|
+
# Report on unhashable objects
|
812
|
+
if unhashable_referrers or unhashable_referents:
|
813
|
+
print(f"Note: {len(unhashable_referrers)} unhashable referrers and {len(unhashable_referents)} unhashable referents were excluded from cycle detection")
|
814
|
+
|
815
|
+
# Check for unhashable objects that might be in both lists
|
816
|
+
potential_unhashable_cycles = []
|
817
|
+
for ureferrer in unhashable_referrers:
|
818
|
+
for ureferent in unhashable_referents:
|
819
|
+
if id(ureferrer) == id(ureferent):
|
820
|
+
potential_unhashable_cycles.append(ureferrer)
|
821
|
+
|
822
|
+
if potential_unhashable_cycles:
|
823
|
+
print(f"Warning: Found {len(potential_unhashable_cycles)} unhashable objects that may be part of cycles:")
|
824
|
+
for obj in potential_unhashable_cycles:
|
825
|
+
print(f" - Type: {type(obj)}, ID: {id(obj)}")
|
826
|
+
|
827
|
+
# Report details of unhashable objects for further investigation
|
828
|
+
print("Unhashable referrers:")
|
829
|
+
for i, obj in enumerate(unhashable_referrers[:5]): # Limit to first 5 to avoid flooding output
|
830
|
+
print(f" - [{i}] Type: {type(obj)}, ID: {id(obj)}")
|
831
|
+
if len(unhashable_referrers) > 5:
|
832
|
+
print(f" ... and {len(unhashable_referrers) - 5} more")
|
833
|
+
|
834
|
+
print("Unhashable referents:")
|
835
|
+
for i, obj in enumerate(unhashable_referents[:5]): # Limit to first 5
|
836
|
+
print(f" - [{i}] Type: {type(obj)}, ID: {id(obj)}")
|
837
|
+
if len(unhashable_referents) > 5:
|
838
|
+
print(f" ... and {len(unhashable_referents) - 5} more")
|
839
|
+
|
840
|
+
# Add unhashable objects that might be in cycles to the full result list
|
841
|
+
result_set = common_objects.copy()
|
842
|
+
return result_set
|
843
|
+
|
844
|
+
def _is_hashable(self, obj: Any) -> bool:
|
845
|
+
"""Helper method to check if an object is hashable."""
|
846
|
+
try:
|
847
|
+
hash(obj)
|
848
|
+
return True
|
849
|
+
except TypeError:
|
850
|
+
return False
|
851
|
+
|
852
|
+
def visualize_dependencies(self, max_depth: int = 3, output_dir: str = None, prefix: str = "") -> Tuple[str, str]:
|
853
|
+
"""
|
854
|
+
Visualize object dependencies using objgraph.
|
855
|
+
|
856
|
+
Args:
|
857
|
+
max_depth: Maximum depth of reference tree to display
|
858
|
+
output_dir: Directory to save visualization files
|
859
|
+
prefix: Prefix for output files
|
860
|
+
|
861
|
+
Returns:
|
862
|
+
Tuple of (outgoing_refs_filename, incoming_refs_filename)
|
863
|
+
"""
|
864
|
+
if not OBJGRAPH_AVAILABLE:
|
865
|
+
print("Warning: objgraph package is not available. Install it with 'pip install objgraph' to enable visualizations.")
|
866
|
+
return "", ""
|
867
|
+
|
868
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
869
|
+
if not prefix:
|
870
|
+
prefix = type(self.target_obj).__name__.lower()
|
871
|
+
|
872
|
+
# Determine output directory
|
873
|
+
if output_dir is None:
|
874
|
+
output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
|
875
|
+
|
876
|
+
if output_dir:
|
877
|
+
# Ensure directory exists
|
878
|
+
os.makedirs(output_dir, exist_ok=True)
|
879
|
+
|
880
|
+
# Prepare filenames
|
881
|
+
refs_filename = os.path.join(output_dir, f"{prefix}_outgoing_refs_{timestamp}.png") if output_dir else f"{prefix}_outgoing_refs_{timestamp}.png"
|
882
|
+
backrefs_filename = os.path.join(output_dir, f"{prefix}_incoming_refs_{timestamp}.png") if output_dir else f"{prefix}_incoming_refs_{timestamp}.png"
|
883
|
+
|
884
|
+
try:
|
885
|
+
# Filter out frames and functions
|
886
|
+
ignore_ids = [id(obj) for obj in gc.get_objects()
|
887
|
+
if isinstance(obj, (types.FrameType, types.FunctionType))]
|
888
|
+
|
889
|
+
# For the regular references (what the object references)
|
890
|
+
objgraph.show_refs(
|
891
|
+
self.target_obj,
|
892
|
+
max_depth=max_depth,
|
893
|
+
filename=refs_filename,
|
894
|
+
extra_ignore=ignore_ids
|
895
|
+
)
|
896
|
+
|
897
|
+
# For the referrers (what references the object)
|
898
|
+
objgraph.show_backrefs(
|
899
|
+
self.target_obj,
|
900
|
+
max_depth=max_depth,
|
901
|
+
filename=backrefs_filename,
|
902
|
+
extra_ignore=ignore_ids
|
903
|
+
)
|
904
|
+
|
905
|
+
print(f"Outgoing references saved to: {refs_filename}")
|
906
|
+
print(f"Incoming references saved to: {backrefs_filename}")
|
907
|
+
|
908
|
+
return refs_filename, backrefs_filename
|
909
|
+
except Exception as e:
|
910
|
+
print(f"Error visualizing dependencies: {e}")
|
911
|
+
return "", ""
|
912
|
+
|
913
|
+
def _inspect_dict_reference(self, ref: Dict) -> None:
|
914
|
+
"""Helper method to inspect dictionary references."""
|
915
|
+
for k, v in ref.items():
|
916
|
+
if v is self.target_obj:
|
917
|
+
print(f" - Found in dict with key: {k}")
|
918
|
+
try:
|
919
|
+
owner = [o for o in gc.get_referrers(ref)
|
920
|
+
if hasattr(o, '__dict__') and o.__dict__ is ref]
|
921
|
+
if owner:
|
922
|
+
print(f" (This dict belongs to: {type(owner[0])})")
|
923
|
+
except:
|
924
|
+
pass
|
925
|
+
|
926
|
+
def _inspect_list_reference(self, ref: List) -> None:
|
927
|
+
"""Helper method to inspect list references."""
|
928
|
+
try:
|
929
|
+
idx = ref.index(self.target_obj)
|
930
|
+
print(f" - Found in list at index: {idx}")
|
931
|
+
owners = [o for o in gc.get_referrers(ref) if hasattr(o, '__dict__')]
|
932
|
+
if owners:
|
933
|
+
print(f" (This list belongs to: {type(owners[0])})")
|
934
|
+
except ValueError:
|
935
|
+
print(" - Found in list (as part of a larger structure)")
|
936
|
+
|
937
|
+
def _inspect_tuple_reference(self, ref: tuple) -> None:
|
938
|
+
"""Helper method to inspect tuple references."""
|
939
|
+
try:
|
940
|
+
idx = ref.index(self.target_obj)
|
941
|
+
print(f" - Found in tuple at index: {idx}")
|
942
|
+
except ValueError:
|
943
|
+
print(" - Found in tuple (as part of a larger structure)")
|
944
|
+
|
945
|
+
def find_reference_paths(self, max_depth: int = 5) -> None:
|
946
|
+
"""
|
947
|
+
Find and print paths to objects that reference the target object.
|
948
|
+
This is a simplified version that directly prints the results.
|
949
|
+
|
950
|
+
Args:
|
951
|
+
max_depth: Maximum depth to search for references
|
952
|
+
"""
|
953
|
+
print(f"\nFinding reference paths to {type(self.target_obj)} (id: {id(self.target_obj)}):")
|
954
|
+
|
955
|
+
# Create a simplified reference chain explorer
|
956
|
+
def find_path_to_referrers(obj, path=None, depth=0, visited=None):
|
957
|
+
if path is None:
|
958
|
+
path = []
|
959
|
+
if visited is None:
|
960
|
+
visited = set()
|
961
|
+
|
962
|
+
if depth > max_depth:
|
963
|
+
return
|
964
|
+
|
965
|
+
# Get referrers excluding frames and functions
|
966
|
+
referrers = [ref for ref in gc.get_referrers(obj)
|
967
|
+
if not isinstance(ref, (types.FrameType, types.FunctionType))]
|
968
|
+
|
969
|
+
for ref in referrers:
|
970
|
+
ref_id = id(ref)
|
971
|
+
|
972
|
+
# Skip if we've seen this object already
|
973
|
+
if ref_id in visited:
|
974
|
+
continue
|
975
|
+
|
976
|
+
visited.add(ref_id)
|
977
|
+
|
978
|
+
# Print current path
|
979
|
+
ref_type = type(ref).__name__
|
980
|
+
current_path = path + [(ref_type, ref_id)]
|
981
|
+
|
982
|
+
# Print the path
|
983
|
+
path_str = " -> ".join([f"{t} (id:{i})" for t, i in current_path])
|
984
|
+
print(f"{' ' * depth}• {ref_type} references {type(obj).__name__}")
|
985
|
+
|
986
|
+
# If it's a container, try to find the specific reference
|
987
|
+
if isinstance(ref, dict):
|
988
|
+
for k, v in ref.items():
|
989
|
+
if v is obj:
|
990
|
+
print(f"{' ' * (depth+2)}(via dict key: {k})")
|
991
|
+
elif isinstance(ref, (list, tuple)):
|
992
|
+
try:
|
993
|
+
idx = ref.index(obj)
|
994
|
+
print(f"{' ' * (depth+2)}(via {type(ref).__name__} index: {idx})")
|
995
|
+
except (ValueError, TypeError):
|
996
|
+
print(f"{' ' * (depth+2)}(as part of a larger structure)")
|
997
|
+
|
998
|
+
# Look for owners of this container
|
999
|
+
if isinstance(ref, dict):
|
1000
|
+
owners = [o for o in gc.get_referrers(ref)
|
1001
|
+
if hasattr(o, '__dict__') and o.__dict__ is ref]
|
1002
|
+
if owners:
|
1003
|
+
owner = owners[0]
|
1004
|
+
print(f"{' ' * (depth+2)}(dict belongs to: {type(owner).__name__} id:{id(owner)})")
|
1005
|
+
|
1006
|
+
# Continue recursion with increased depth
|
1007
|
+
find_path_to_referrers(ref, current_path, depth + 1, visited)
|
1008
|
+
|
1009
|
+
# Start the recursive search
|
1010
|
+
find_path_to_referrers(self.target_obj)
|