local-deep-research 0.3.12__py3-none-any.whl → 0.4.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.
- local_deep_research/__version__.py +1 -1
- local_deep_research/advanced_search_system/filters/base_filter.py +2 -3
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +4 -5
- local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +298 -0
- local_deep_research/advanced_search_system/findings/repository.py +0 -3
- local_deep_research/advanced_search_system/strategies/base_strategy.py +1 -2
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +14 -18
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +4 -8
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +5 -6
- local_deep_research/advanced_search_system/strategies/source_based_strategy.py +2 -2
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +9 -7
- local_deep_research/api/benchmark_functions.py +288 -0
- local_deep_research/api/research_functions.py +8 -4
- local_deep_research/benchmarks/README.md +162 -0
- local_deep_research/benchmarks/__init__.py +51 -0
- local_deep_research/benchmarks/benchmark_functions.py +353 -0
- local_deep_research/benchmarks/cli/__init__.py +16 -0
- local_deep_research/benchmarks/cli/benchmark_commands.py +338 -0
- local_deep_research/benchmarks/cli.py +347 -0
- local_deep_research/benchmarks/comparison/__init__.py +12 -0
- local_deep_research/benchmarks/comparison/evaluator.py +768 -0
- local_deep_research/benchmarks/datasets/__init__.py +53 -0
- local_deep_research/benchmarks/datasets/base.py +295 -0
- local_deep_research/benchmarks/datasets/browsecomp.py +116 -0
- local_deep_research/benchmarks/datasets/custom_dataset_template.py +98 -0
- local_deep_research/benchmarks/datasets/simpleqa.py +74 -0
- local_deep_research/benchmarks/datasets/utils.py +116 -0
- local_deep_research/benchmarks/datasets.py +31 -0
- local_deep_research/benchmarks/efficiency/__init__.py +14 -0
- local_deep_research/benchmarks/efficiency/resource_monitor.py +367 -0
- local_deep_research/benchmarks/efficiency/speed_profiler.py +214 -0
- local_deep_research/benchmarks/evaluators/__init__.py +18 -0
- local_deep_research/benchmarks/evaluators/base.py +74 -0
- local_deep_research/benchmarks/evaluators/browsecomp.py +83 -0
- local_deep_research/benchmarks/evaluators/composite.py +121 -0
- local_deep_research/benchmarks/evaluators/simpleqa.py +271 -0
- local_deep_research/benchmarks/graders.py +410 -0
- local_deep_research/benchmarks/metrics/README.md +80 -0
- local_deep_research/benchmarks/metrics/__init__.py +24 -0
- local_deep_research/benchmarks/metrics/calculation.py +385 -0
- local_deep_research/benchmarks/metrics/reporting.py +155 -0
- local_deep_research/benchmarks/metrics/visualization.py +205 -0
- local_deep_research/benchmarks/metrics.py +11 -0
- local_deep_research/benchmarks/optimization/__init__.py +32 -0
- local_deep_research/benchmarks/optimization/api.py +274 -0
- local_deep_research/benchmarks/optimization/metrics.py +20 -0
- local_deep_research/benchmarks/optimization/optuna_optimizer.py +1163 -0
- local_deep_research/benchmarks/runners.py +434 -0
- local_deep_research/benchmarks/templates.py +65 -0
- local_deep_research/config/llm_config.py +26 -23
- local_deep_research/config/search_config.py +1 -5
- local_deep_research/defaults/default_settings.json +108 -7
- local_deep_research/search_system.py +16 -8
- local_deep_research/utilities/db_utils.py +3 -6
- local_deep_research/utilities/es_utils.py +441 -0
- local_deep_research/utilities/log_utils.py +36 -0
- local_deep_research/utilities/search_utilities.py +8 -9
- local_deep_research/web/app.py +7 -9
- local_deep_research/web/app_factory.py +9 -12
- local_deep_research/web/database/migrations.py +8 -5
- local_deep_research/web/database/models.py +20 -0
- local_deep_research/web/database/schema_upgrade.py +5 -8
- local_deep_research/web/models/database.py +15 -18
- local_deep_research/web/routes/benchmark_routes.py +427 -0
- local_deep_research/web/routes/research_routes.py +13 -17
- local_deep_research/web/routes/settings_routes.py +264 -67
- local_deep_research/web/services/research_service.py +47 -57
- local_deep_research/web/services/settings_manager.py +1 -4
- local_deep_research/web/services/settings_service.py +4 -6
- local_deep_research/web/static/css/styles.css +12 -0
- local_deep_research/web/static/js/components/logpanel.js +164 -155
- local_deep_research/web/static/js/components/research.js +44 -3
- local_deep_research/web/static/js/components/settings.js +27 -0
- local_deep_research/web/static/js/services/socket.js +47 -0
- local_deep_research/web_search_engines/default_search_engines.py +38 -0
- local_deep_research/web_search_engines/engines/meta_search_engine.py +100 -33
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +31 -17
- local_deep_research/web_search_engines/engines/search_engine_brave.py +8 -3
- local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +343 -0
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +14 -6
- local_deep_research/web_search_engines/engines/search_engine_local.py +19 -23
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +9 -12
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +12 -17
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +8 -4
- local_deep_research/web_search_engines/search_engine_base.py +22 -5
- local_deep_research/web_search_engines/search_engine_factory.py +32 -11
- local_deep_research/web_search_engines/search_engines_config.py +14 -1
- {local_deep_research-0.3.12.dist-info → local_deep_research-0.4.0.dist-info}/METADATA +10 -2
- {local_deep_research-0.3.12.dist-info → local_deep_research-0.4.0.dist-info}/RECORD +92 -49
- {local_deep_research-0.3.12.dist-info → local_deep_research-0.4.0.dist-info}/WHEEL +0 -0
- {local_deep_research-0.3.12.dist-info → local_deep_research-0.4.0.dist-info}/entry_points.txt +0 -0
- {local_deep_research-0.3.12.dist-info → local_deep_research-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for dataset handling.
|
3
|
+
|
4
|
+
This module provides utility functions for common dataset operations like
|
5
|
+
decryption, encoding detection, etc.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import base64
|
9
|
+
import hashlib
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def derive_key(password: str, length: int) -> bytes:
|
17
|
+
"""Derive a fixed-length key from the password using SHA256."""
|
18
|
+
hasher = hashlib.sha256()
|
19
|
+
hasher.update(password.encode())
|
20
|
+
key = hasher.digest()
|
21
|
+
return key * (length // len(key)) + key[: length % len(key)]
|
22
|
+
|
23
|
+
|
24
|
+
def decrypt(ciphertext_b64: str, password: str) -> str:
|
25
|
+
"""
|
26
|
+
Decrypt base64-encoded ciphertext with XOR.
|
27
|
+
Uses multiple approaches to handle different encoding formats.
|
28
|
+
"""
|
29
|
+
# Skip decryption for non-encoded strings
|
30
|
+
if not isinstance(ciphertext_b64, str) or len(ciphertext_b64) < 8:
|
31
|
+
return ciphertext_b64
|
32
|
+
|
33
|
+
# Skip if the string doesn't look like base64
|
34
|
+
if not all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' for c in ciphertext_b64):
|
35
|
+
return ciphertext_b64
|
36
|
+
|
37
|
+
# Attempt standard decryption
|
38
|
+
try:
|
39
|
+
encrypted = base64.b64decode(ciphertext_b64)
|
40
|
+
key = derive_key(password, len(encrypted))
|
41
|
+
decrypted = bytes(a ^ b for a, b in zip(encrypted, key))
|
42
|
+
|
43
|
+
# Check if the result looks like valid text
|
44
|
+
result = decrypted.decode('utf-8', errors='replace')
|
45
|
+
|
46
|
+
# Heuristic check - if the decrypted text is mostly ASCII and contains spaces
|
47
|
+
if all(32 <= ord(c) < 127 for c in result[:50]) and ' ' in result[:50]:
|
48
|
+
logger.debug(f"Successfully decrypted with standard method: {result[:50]}...")
|
49
|
+
return result
|
50
|
+
except Exception as e:
|
51
|
+
logger.debug(f"Standard decryption failed: {str(e)}")
|
52
|
+
|
53
|
+
# Alternative method - try using just the first part of the password
|
54
|
+
try:
|
55
|
+
if len(password) > 30:
|
56
|
+
alt_password = password.split()[0] # Use first word
|
57
|
+
encrypted = base64.b64decode(ciphertext_b64)
|
58
|
+
key = derive_key(alt_password, len(encrypted))
|
59
|
+
decrypted = bytes(a ^ b for a, b in zip(encrypted, key))
|
60
|
+
|
61
|
+
result = decrypted.decode('utf-8', errors='replace')
|
62
|
+
if all(32 <= ord(c) < 127 for c in result[:50]) and ' ' in result[:50]:
|
63
|
+
logger.debug(f"Successfully decrypted with alternate method 1: {result[:50]}...")
|
64
|
+
return result
|
65
|
+
except Exception:
|
66
|
+
pass
|
67
|
+
|
68
|
+
# Alternative method 2 - try using the GUID part
|
69
|
+
try:
|
70
|
+
if "GUID" in password:
|
71
|
+
guid_part = password.split("GUID")[1].strip()
|
72
|
+
encrypted = base64.b64decode(ciphertext_b64)
|
73
|
+
key = derive_key(guid_part, len(encrypted))
|
74
|
+
decrypted = bytes(a ^ b for a, b in zip(encrypted, key))
|
75
|
+
|
76
|
+
result = decrypted.decode('utf-8', errors='replace')
|
77
|
+
if all(32 <= ord(c) < 127 for c in result[:50]) and ' ' in result[:50]:
|
78
|
+
logger.debug(f"Successfully decrypted with GUID method: {result[:50]}...")
|
79
|
+
return result
|
80
|
+
except Exception:
|
81
|
+
pass
|
82
|
+
|
83
|
+
# Alternative method 3 - hardcoded key for BrowseComp
|
84
|
+
try:
|
85
|
+
hardcoded_key = "MHGGF2022!" # Known key for BrowseComp dataset
|
86
|
+
encrypted = base64.b64decode(ciphertext_b64)
|
87
|
+
key = derive_key(hardcoded_key, len(encrypted))
|
88
|
+
decrypted = bytes(a ^ b for a, b in zip(encrypted, key))
|
89
|
+
|
90
|
+
result = decrypted.decode('utf-8', errors='replace')
|
91
|
+
if all(32 <= ord(c) < 127 for c in result[:50]) and ' ' in result[:50]:
|
92
|
+
logger.debug(f"Successfully decrypted with hardcoded key: {result[:50]}...")
|
93
|
+
return result
|
94
|
+
except Exception:
|
95
|
+
pass
|
96
|
+
|
97
|
+
# If all attempts fail, return the original
|
98
|
+
logger.debug(f"All decryption attempts failed for: {ciphertext_b64[:20]}...")
|
99
|
+
return ciphertext_b64
|
100
|
+
|
101
|
+
|
102
|
+
def get_known_answer_map() -> Dict[str, str]:
|
103
|
+
"""Get a mapping of known encrypted answers to their decrypted values.
|
104
|
+
|
105
|
+
This function maintains a catalog of known encrypted strings that
|
106
|
+
couldn't be automatically decrypted, along with their verified
|
107
|
+
plaintext values.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Dictionary mapping encrypted strings to their plaintext values.
|
111
|
+
"""
|
112
|
+
return {
|
113
|
+
"dFoTn+K+bcdyWg==": "Tooth Rock",
|
114
|
+
"ERFIwA==": "1945",
|
115
|
+
# Add more mappings as they are discovered during benchmark runs
|
116
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""
|
2
|
+
Dataset handling for benchmarks.
|
3
|
+
|
4
|
+
This is a legacy module that provides backwards compatibility with the old
|
5
|
+
dataset handling functions. New code should use the classes in the datasets
|
6
|
+
package directly.
|
7
|
+
|
8
|
+
Notes on BrowseComp dataset:
|
9
|
+
- BrowseComp data is encrypted with the canary string used as the decryption key
|
10
|
+
- The decrypt() function handles decrypting both problems and answers
|
11
|
+
- For some examples where standard decryption doesn't work, we use additional methods:
|
12
|
+
1. Try using various parts of the canary string as the key
|
13
|
+
2. Try using hardcoded keys that are known to work
|
14
|
+
3. Use a manual mapping for specific encrypted strings that have been verified
|
15
|
+
"""
|
16
|
+
|
17
|
+
import logging
|
18
|
+
from typing import Any, Dict, List, Optional
|
19
|
+
|
20
|
+
from .datasets import DatasetRegistry, load_dataset
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
# Re-export the default dataset URLs
|
25
|
+
from .datasets import DEFAULT_DATASET_URLS
|
26
|
+
|
27
|
+
# Re-export the get_available_datasets function
|
28
|
+
from .datasets import get_available_datasets
|
29
|
+
|
30
|
+
# Re-export the load_dataset function
|
31
|
+
__all__ = ['DEFAULT_DATASET_URLS', 'get_available_datasets', 'load_dataset']
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Efficiency submodule for speed and resource monitoring in Local Deep Research.
|
3
|
+
|
4
|
+
This module provides tools for measuring and optimizing execution speed
|
5
|
+
and resource usage of the research system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from local_deep_research.benchmarks.efficiency.speed_profiler import SpeedProfiler
|
9
|
+
from local_deep_research.benchmarks.efficiency.resource_monitor import ResourceMonitor
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
'SpeedProfiler',
|
13
|
+
'ResourceMonitor',
|
14
|
+
]
|
@@ -0,0 +1,367 @@
|
|
1
|
+
"""
|
2
|
+
Resource monitoring tools for Local Deep Research.
|
3
|
+
|
4
|
+
This module provides functionality for tracking CPU, memory and other
|
5
|
+
system resource usage during the research process.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import threading
|
10
|
+
import time
|
11
|
+
from contextlib import contextmanager
|
12
|
+
from typing import Dict, List, Optional, Any, Callable, Tuple
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
# Try to import psutil, but don't fail if not available
|
17
|
+
try:
|
18
|
+
import psutil
|
19
|
+
PSUTIL_AVAILABLE = True
|
20
|
+
except ImportError:
|
21
|
+
PSUTIL_AVAILABLE = False
|
22
|
+
logger.warning("psutil not available, resource monitoring will be limited")
|
23
|
+
|
24
|
+
|
25
|
+
class ResourceMonitor:
|
26
|
+
"""
|
27
|
+
Monitor system resource usage during research.
|
28
|
+
|
29
|
+
This class provides methods for tracking CPU, memory, and disk usage
|
30
|
+
during system execution. It can be used to identify resource bottlenecks
|
31
|
+
and optimize configurations for different hardware environments.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self,
|
35
|
+
sampling_interval: float = 1.0,
|
36
|
+
track_process: bool = True,
|
37
|
+
track_system: bool = True):
|
38
|
+
"""
|
39
|
+
Initialize the resource monitor.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
sampling_interval: Seconds between resource usage measurements
|
43
|
+
track_process: Whether to track this process's resource usage
|
44
|
+
track_system: Whether to track overall system resource usage
|
45
|
+
"""
|
46
|
+
self.sampling_interval = sampling_interval
|
47
|
+
self.track_process = track_process
|
48
|
+
self.track_system = track_system
|
49
|
+
|
50
|
+
self.monitoring = False
|
51
|
+
self.monitor_thread = None
|
52
|
+
|
53
|
+
# Resource usage data
|
54
|
+
self.process_data = []
|
55
|
+
self.system_data = []
|
56
|
+
self.start_time = None
|
57
|
+
self.end_time = None
|
58
|
+
|
59
|
+
# Check if we can monitor resources
|
60
|
+
self.can_monitor = PSUTIL_AVAILABLE
|
61
|
+
if not self.can_monitor:
|
62
|
+
logger.warning("Resource monitoring requires psutil. Install with: pip install psutil")
|
63
|
+
|
64
|
+
def start(self):
|
65
|
+
"""Start monitoring resource usage."""
|
66
|
+
if not self.can_monitor:
|
67
|
+
logger.warning("Resource monitoring not available (psutil not installed)")
|
68
|
+
return
|
69
|
+
|
70
|
+
if self.monitoring:
|
71
|
+
logger.warning("Resource monitoring already started")
|
72
|
+
return
|
73
|
+
|
74
|
+
self.process_data = []
|
75
|
+
self.system_data = []
|
76
|
+
self.start_time = time.time()
|
77
|
+
self.monitoring = True
|
78
|
+
|
79
|
+
# Start monitoring in a background thread
|
80
|
+
self.monitor_thread = threading.Thread(
|
81
|
+
target=self._monitor_resources,
|
82
|
+
daemon=True
|
83
|
+
)
|
84
|
+
self.monitor_thread.start()
|
85
|
+
|
86
|
+
logger.info(f"Resource monitoring started with {self.sampling_interval}s interval")
|
87
|
+
|
88
|
+
def stop(self):
|
89
|
+
"""Stop monitoring resource usage."""
|
90
|
+
if not self.monitoring:
|
91
|
+
return
|
92
|
+
|
93
|
+
self.monitoring = False
|
94
|
+
self.end_time = time.time()
|
95
|
+
|
96
|
+
# Wait for the monitoring thread to finish
|
97
|
+
if self.monitor_thread:
|
98
|
+
self.monitor_thread.join(timeout=2.0)
|
99
|
+
self.monitor_thread = None
|
100
|
+
|
101
|
+
logger.info("Resource monitoring stopped")
|
102
|
+
|
103
|
+
def _monitor_resources(self):
|
104
|
+
"""Background thread that collects resource usage data."""
|
105
|
+
if not PSUTIL_AVAILABLE:
|
106
|
+
return
|
107
|
+
|
108
|
+
# Get this process
|
109
|
+
current_process = psutil.Process()
|
110
|
+
|
111
|
+
while self.monitoring:
|
112
|
+
timestamp = time.time()
|
113
|
+
|
114
|
+
try:
|
115
|
+
# Monitor this process
|
116
|
+
if self.track_process:
|
117
|
+
process_cpu = current_process.cpu_percent(interval=None)
|
118
|
+
process_memory = current_process.memory_info()
|
119
|
+
|
120
|
+
self.process_data.append({
|
121
|
+
'timestamp': timestamp,
|
122
|
+
'cpu_percent': process_cpu,
|
123
|
+
'memory_rss': process_memory.rss, # Resident Set Size in bytes
|
124
|
+
'memory_vms': process_memory.vms, # Virtual Memory Size in bytes
|
125
|
+
'memory_shared': getattr(process_memory, 'shared', 0),
|
126
|
+
'num_threads': current_process.num_threads(),
|
127
|
+
'open_files': len(current_process.open_files()),
|
128
|
+
'status': current_process.status()
|
129
|
+
})
|
130
|
+
|
131
|
+
# Monitor overall system
|
132
|
+
if self.track_system:
|
133
|
+
system_cpu = psutil.cpu_percent(interval=None)
|
134
|
+
system_memory = psutil.virtual_memory()
|
135
|
+
system_disk = psutil.disk_usage('/')
|
136
|
+
|
137
|
+
self.system_data.append({
|
138
|
+
'timestamp': timestamp,
|
139
|
+
'cpu_percent': system_cpu,
|
140
|
+
'memory_total': system_memory.total,
|
141
|
+
'memory_available': system_memory.available,
|
142
|
+
'memory_used': system_memory.used,
|
143
|
+
'memory_percent': system_memory.percent,
|
144
|
+
'disk_total': system_disk.total,
|
145
|
+
'disk_used': system_disk.used,
|
146
|
+
'disk_percent': system_disk.percent
|
147
|
+
})
|
148
|
+
|
149
|
+
except Exception as e:
|
150
|
+
logger.error(f"Error monitoring resources: {str(e)}")
|
151
|
+
|
152
|
+
# Sleep until next sampling interval
|
153
|
+
time.sleep(self.sampling_interval)
|
154
|
+
|
155
|
+
@contextmanager
|
156
|
+
def monitor(self):
|
157
|
+
"""
|
158
|
+
Context manager for monitoring resources during a block of code.
|
159
|
+
|
160
|
+
Example:
|
161
|
+
with resource_monitor.monitor():
|
162
|
+
# Code to monitor
|
163
|
+
do_something_resource_intensive()
|
164
|
+
"""
|
165
|
+
self.start()
|
166
|
+
try:
|
167
|
+
yield
|
168
|
+
finally:
|
169
|
+
self.stop()
|
170
|
+
|
171
|
+
def get_process_stats(self) -> Dict[str, Any]:
|
172
|
+
"""
|
173
|
+
Get statistics about this process's resource usage.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Dictionary with process resource usage statistics
|
177
|
+
"""
|
178
|
+
if not self.process_data:
|
179
|
+
return {}
|
180
|
+
|
181
|
+
# Extract data series
|
182
|
+
timestamps = [d['timestamp'] for d in self.process_data]
|
183
|
+
cpu_values = [d['cpu_percent'] for d in self.process_data]
|
184
|
+
memory_values = [d['memory_rss'] / (1024 * 1024) for d in self.process_data] # Convert to MB
|
185
|
+
|
186
|
+
# Calculate statistics
|
187
|
+
stats = {
|
188
|
+
'start_time': self.start_time,
|
189
|
+
'end_time': self.end_time,
|
190
|
+
'duration': self.end_time - self.start_time if self.end_time else None,
|
191
|
+
'sample_count': len(self.process_data),
|
192
|
+
'cpu_min': min(cpu_values) if cpu_values else None,
|
193
|
+
'cpu_max': max(cpu_values) if cpu_values else None,
|
194
|
+
'cpu_avg': sum(cpu_values) / len(cpu_values) if cpu_values else None,
|
195
|
+
'memory_min_mb': min(memory_values) if memory_values else None,
|
196
|
+
'memory_max_mb': max(memory_values) if memory_values else None,
|
197
|
+
'memory_avg_mb': sum(memory_values) / len(memory_values) if memory_values else None,
|
198
|
+
'thread_max': max(d['num_threads'] for d in self.process_data) if self.process_data else None,
|
199
|
+
}
|
200
|
+
|
201
|
+
return stats
|
202
|
+
|
203
|
+
def get_system_stats(self) -> Dict[str, Any]:
|
204
|
+
"""
|
205
|
+
Get statistics about overall system resource usage.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Dictionary with system resource usage statistics
|
209
|
+
"""
|
210
|
+
if not self.system_data:
|
211
|
+
return {}
|
212
|
+
|
213
|
+
# Extract data series
|
214
|
+
timestamps = [d['timestamp'] for d in self.system_data]
|
215
|
+
cpu_values = [d['cpu_percent'] for d in self.system_data]
|
216
|
+
memory_values = [d['memory_percent'] for d in self.system_data]
|
217
|
+
disk_values = [d['disk_percent'] for d in self.system_data]
|
218
|
+
|
219
|
+
# Calculate statistics
|
220
|
+
stats = {
|
221
|
+
'start_time': self.start_time,
|
222
|
+
'end_time': self.end_time,
|
223
|
+
'duration': self.end_time - self.start_time if self.end_time else None,
|
224
|
+
'sample_count': len(self.system_data),
|
225
|
+
'cpu_min': min(cpu_values) if cpu_values else None,
|
226
|
+
'cpu_max': max(cpu_values) if cpu_values else None,
|
227
|
+
'cpu_avg': sum(cpu_values) / len(cpu_values) if cpu_values else None,
|
228
|
+
'memory_min_percent': min(memory_values) if memory_values else None,
|
229
|
+
'memory_max_percent': max(memory_values) if memory_values else None,
|
230
|
+
'memory_avg_percent': sum(memory_values) / len(memory_values) if memory_values else None,
|
231
|
+
'disk_min_percent': min(disk_values) if disk_values else None,
|
232
|
+
'disk_max_percent': max(disk_values) if disk_values else None,
|
233
|
+
'disk_avg_percent': sum(disk_values) / len(disk_values) if disk_values else None,
|
234
|
+
'memory_total_gb': self.system_data[0]['memory_total'] / (1024**3) if self.system_data else None,
|
235
|
+
'disk_total_gb': self.system_data[0]['disk_total'] / (1024**3) if self.system_data else None,
|
236
|
+
}
|
237
|
+
|
238
|
+
return stats
|
239
|
+
|
240
|
+
def get_combined_stats(self) -> Dict[str, Any]:
|
241
|
+
"""
|
242
|
+
Get combined resource usage statistics.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Dictionary with both process and system statistics
|
246
|
+
"""
|
247
|
+
process_stats = self.get_process_stats()
|
248
|
+
system_stats = self.get_system_stats()
|
249
|
+
|
250
|
+
# Combine stats
|
251
|
+
stats = {
|
252
|
+
'start_time': self.start_time,
|
253
|
+
'end_time': self.end_time,
|
254
|
+
'duration': self.end_time - self.start_time if self.end_time else None,
|
255
|
+
}
|
256
|
+
|
257
|
+
# Add process stats with 'process_' prefix
|
258
|
+
for key, value in process_stats.items():
|
259
|
+
if key not in ['start_time', 'end_time', 'duration']:
|
260
|
+
stats[f'process_{key}'] = value
|
261
|
+
|
262
|
+
# Add system stats with 'system_' prefix
|
263
|
+
for key, value in system_stats.items():
|
264
|
+
if key not in ['start_time', 'end_time', 'duration']:
|
265
|
+
stats[f'system_{key}'] = value
|
266
|
+
|
267
|
+
# Calculate derived metrics
|
268
|
+
if (process_stats.get('memory_max_mb') is not None and
|
269
|
+
system_stats.get('memory_total_gb') is not None):
|
270
|
+
|
271
|
+
# Process memory as percentage of total system memory
|
272
|
+
system_memory_mb = system_stats['memory_total_gb'] * 1024
|
273
|
+
stats['process_memory_percent'] = (
|
274
|
+
(process_stats['memory_max_mb'] / system_memory_mb) * 100
|
275
|
+
if system_memory_mb > 0 else 0
|
276
|
+
)
|
277
|
+
|
278
|
+
return stats
|
279
|
+
|
280
|
+
def print_summary(self):
|
281
|
+
"""Print a formatted summary of resource usage."""
|
282
|
+
process_stats = self.get_process_stats()
|
283
|
+
system_stats = self.get_system_stats()
|
284
|
+
|
285
|
+
print("\n===== RESOURCE USAGE SUMMARY =====")
|
286
|
+
|
287
|
+
if process_stats:
|
288
|
+
print("\n--- Process Resources ---")
|
289
|
+
print(f"CPU usage: {process_stats.get('cpu_avg', 0):.1f}% avg, "
|
290
|
+
f"{process_stats.get('cpu_max', 0):.1f}% peak")
|
291
|
+
print(f"Memory usage: {process_stats.get('memory_avg_mb', 0):.1f} MB avg, "
|
292
|
+
f"{process_stats.get('memory_max_mb', 0):.1f} MB peak")
|
293
|
+
print(f"Threads: {process_stats.get('thread_max', 0)} max")
|
294
|
+
|
295
|
+
if system_stats:
|
296
|
+
print("\n--- System Resources ---")
|
297
|
+
print(f"CPU usage: {system_stats.get('cpu_avg', 0):.1f}% avg, "
|
298
|
+
f"{system_stats.get('cpu_max', 0):.1f}% peak")
|
299
|
+
print(f"Memory usage: {system_stats.get('memory_avg_percent', 0):.1f}% avg, "
|
300
|
+
f"{system_stats.get('memory_max_percent', 0):.1f}% peak "
|
301
|
+
f"(Total: {system_stats.get('memory_total_gb', 0):.1f} GB)")
|
302
|
+
print(f"Disk usage: {system_stats.get('disk_avg_percent', 0):.1f}% avg "
|
303
|
+
f"(Total: {system_stats.get('disk_total_gb', 0):.1f} GB)")
|
304
|
+
|
305
|
+
print("\n===================================")
|
306
|
+
|
307
|
+
def export_data(self) -> Dict[str, Any]:
|
308
|
+
"""
|
309
|
+
Export all collected data.
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
Dictionary with all collected resource usage data
|
313
|
+
"""
|
314
|
+
return {
|
315
|
+
'start_time': self.start_time,
|
316
|
+
'end_time': self.end_time,
|
317
|
+
'sampling_interval': self.sampling_interval,
|
318
|
+
'process_data': self.process_data,
|
319
|
+
'system_data': self.system_data
|
320
|
+
}
|
321
|
+
|
322
|
+
|
323
|
+
def check_system_resources() -> Dict[str, Any]:
|
324
|
+
"""
|
325
|
+
Check current system resources.
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
Dictionary with current resource usage information
|
329
|
+
"""
|
330
|
+
if not PSUTIL_AVAILABLE:
|
331
|
+
return {
|
332
|
+
'error': 'psutil not available',
|
333
|
+
'available': False
|
334
|
+
}
|
335
|
+
|
336
|
+
try:
|
337
|
+
# Get basic system information
|
338
|
+
cpu_count = psutil.cpu_count(logical=True)
|
339
|
+
cpu_physical = psutil.cpu_count(logical=False)
|
340
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
341
|
+
|
342
|
+
memory = psutil.virtual_memory()
|
343
|
+
disk = psutil.disk_usage('/')
|
344
|
+
|
345
|
+
# Format results
|
346
|
+
result = {
|
347
|
+
'available': True,
|
348
|
+
'cpu_count': cpu_count,
|
349
|
+
'cpu_physical': cpu_physical,
|
350
|
+
'cpu_percent': cpu_percent,
|
351
|
+
'memory_total_gb': memory.total / (1024**3),
|
352
|
+
'memory_available_gb': memory.available / (1024**3),
|
353
|
+
'memory_used_gb': memory.used / (1024**3),
|
354
|
+
'memory_percent': memory.percent,
|
355
|
+
'disk_total_gb': disk.total / (1024**3),
|
356
|
+
'disk_free_gb': disk.free / (1024**3),
|
357
|
+
'disk_percent': disk.percent
|
358
|
+
}
|
359
|
+
|
360
|
+
return result
|
361
|
+
|
362
|
+
except Exception as e:
|
363
|
+
logger.error(f"Error checking system resources: {str(e)}")
|
364
|
+
return {
|
365
|
+
'error': str(e),
|
366
|
+
'available': False
|
367
|
+
}
|