Skip to content

Commit f7a6261

Browse files
authored
Merge pull request #743 from simvue-io/dev
v2.0.0a3 release
2 parents 4ef75c0 + ee9773f commit f7a6261

File tree

12 files changed

+238
-66
lines changed

12 files changed

+238
-66
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Change log
22

3+
## [v2.0.0-alpha3](https://github.com/simvue-io/client/releases/tag/v2.0.0a3) - 2025-03-04
4+
* Updated codecarbon to work with new API
5+
* Codecarbon now works with offline mode
6+
* Codecarbon metadata dict is now nested
7+
* Add PID to sender lock file so it can recover from crashes
8+
* Add accept Gzip encoding
9+
* Fixed list of processes to add / remove from existing list of objects
10+
* Add step to resource metrics
11+
* Fix bug where process user alerts should not be overridden if manually set by the user
12+
313
## [v2.0.0-alpha2](https://github.com/simvue-io/client/releases/tag/v2.0.0a2) - 2025-02-27
414
* Removed 'no config file' and 'unstaged changes' warnings from Offline mode as they do not apply
515
* Made `staging_check` not apply in Offline mode

CITATION.cff

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ keywords:
4242
- alerting
4343
- simulation
4444
license: Apache-2.0
45-
commit: 83b9144abd2092d4be304bf742d72a249ad1d8ff
46-
version: 2.0.0a2
47-
date-released: '2025-02-27'
45+
commit: 64ff8a5344232d44fc7da5b6ff601d3023497977
46+
version: 2.0.0a3
47+
date-released: '2025-03-04'
4848
references:
4949
- title: mlco2/codecarbon
5050
version: v2.8.2

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<picture>
77
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simvue-io/.github/refs/heads/main/simvue-white.png" />
88
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/simvue-io/.github/refs/heads/main/simvue-black.png" />
9-
<img alt="Simvue" src="https://github.com/simvue-io/.github/blob/5eb8cfd2edd3269259eccd508029f269d993282f/simvue-black.png" width="500">
9+
<img alt="Simvue" src="https://raw.githubusercontent.com/simvue-io/.github/refs/heads/main/simvue-black.png" width="500">
1010
</picture>
1111
</p>
1212

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "simvue"
3-
version = "2.0.0a2"
3+
version = "2.0.0a3"
44
description = "Simulation tracking and monitoring"
55
authors = [
66
{name = "Simvue Development Team", email = "[email protected]"}
@@ -16,10 +16,10 @@ classifiers = [
1616
"Operating System :: Unix",
1717
"Operating System :: Microsoft :: Windows",
1818
"Programming Language :: Python :: 3",
19-
"Programming Language :: Python :: 3.9",
2019
"Programming Language :: Python :: 3.10",
2120
"Programming Language :: Python :: 3.11",
2221
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
2323
"Topic :: Scientific/Engineering",
2424
"Topic :: System :: Monitoring",
2525
"Topic :: Utilities",

simvue/api/objects/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def __init__(
171171
{
172172
"Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}",
173173
"User-Agent": _user_agent or f"Simvue Python client {__version__}",
174+
"Accept-Encoding": "gzip",
174175
}
175176
if not self._offline
176177
else {}

simvue/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def __init__(
8484
logger.warning(f"No {label} specified")
8585

8686
self._headers: dict[str, str] = {
87-
"Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}"
87+
"Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}",
88+
"Accept-Encoding": "gzip",
8889
}
8990

9091
@prettify_pydantic

simvue/config/parameters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def check_token(cls, v: typing.Any) -> str | None:
4848

4949
class OfflineSpecifications(pydantic.BaseModel):
5050
cache: pathlib.Path | None = None
51+
country_iso_code: str | None = None
5152

5253

5354
class MetricsSpecifications(pydantic.BaseModel):

simvue/eco.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import logging
33
import datetime
44

5-
from codecarbon import EmissionsTracker
6-
from codecarbon.output_methods.base_output import BaseOutput as cc_BaseOutput
5+
from codecarbon import EmissionsTracker, OfflineEmissionsTracker
6+
from codecarbon.output import BaseOutput as cc_BaseOutput
77
from simvue.utilities import simvue_timestamp
88

99
if typing.TYPE_CHECKING:
@@ -32,30 +32,43 @@ def out(
3232

3333
if meta_update:
3434
logger.debug("Logging CodeCarbon metadata")
35-
self._simvue_run.update_metadata(
36-
{
37-
"codecarbon.country": total.country_name,
38-
"codecarbon.country_iso_code": total.country_iso_code,
39-
"codecarbon.region": total.region,
40-
"codecarbon.version": total.codecarbon_version,
41-
}
35+
try:
36+
self._simvue_run.update_metadata(
37+
{
38+
"codecarbon": {
39+
"country": total.country_name,
40+
"country_iso_code": total.country_iso_code,
41+
"region": total.region,
42+
"version": total.codecarbon_version,
43+
}
44+
}
45+
)
46+
except AttributeError as e:
47+
logger.error(f"Failed to update metadata: {e}")
48+
try:
49+
_cc_timestamp = datetime.datetime.strptime(
50+
total.timestamp, "%Y-%m-%dT%H:%M:%S"
4251
)
43-
44-
_cc_timestamp: datetime.datetime = datetime.datetime.strptime(
45-
total.timestamp, "%Y-%m-%dT%H:%M:%S"
46-
)
52+
except ValueError as e:
53+
logger.error(f"Error parsing timestamp: {e}")
54+
return
4755

4856
logger.debug("Logging CodeCarbon metrics")
49-
self._simvue_run.log_metrics(
50-
metrics={
51-
"codecarbon.total.emissions": total.emissions,
52-
"codecarbon.total.energy_consumed": total.energy_consumed,
53-
"codecarbon.delta.emissions": delta.emissions,
54-
"codecarbon.delta.energy_consumed": delta.energy_consumed,
55-
},
56-
step=self._metrics_step,
57-
timestamp=simvue_timestamp(_cc_timestamp),
58-
)
57+
try:
58+
self._simvue_run.log_metrics(
59+
metrics={
60+
"codecarbon.total.emissions": total.emissions,
61+
"codecarbon.total.energy_consumed": total.energy_consumed,
62+
"codecarbon.delta.emissions": delta.emissions,
63+
"codecarbon.delta.energy_consumed": delta.energy_consumed,
64+
},
65+
step=self._metrics_step,
66+
timestamp=simvue_timestamp(_cc_timestamp),
67+
)
68+
except ArithmeticError as e:
69+
logger.error(f"Failed to log metrics: {e}")
70+
return
71+
5972
self._metrics_step += 1
6073

6174
def live_out(self, total: "EmissionsData", delta: "EmissionsData") -> None:
@@ -71,6 +84,36 @@ def __init__(
7184
super().__init__(
7285
project_name=project_name,
7386
measure_power_secs=metrics_interval,
87+
api_call_interval=1,
88+
experiment_id=None,
89+
experiment_name=None,
90+
logging_logger=CodeCarbonOutput(simvue_run),
91+
save_to_logger=True,
92+
allow_multiple_runs=True,
93+
log_level="error",
94+
)
95+
96+
def set_measure_interval(self, interval: int) -> None:
97+
"""Set the measure interval"""
98+
self._set_from_conf(interval, "measure_power_secs")
99+
100+
def post_init(self) -> None:
101+
self._set_from_conf(self._simvue_run._id, "experiment_id")
102+
self._set_from_conf(self._simvue_run._name, "experiment_name")
103+
self.start()
104+
105+
106+
class OfflineSimvueEmissionsTracker(OfflineEmissionsTracker):
107+
def __init__(
108+
self, project_name: str, simvue_run: "Run", metrics_interval: int
109+
) -> None:
110+
self._simvue_run = simvue_run
111+
logger.setLevel(logging.ERROR)
112+
super().__init__(
113+
country_iso_code=simvue_run._user_config.offline.country_iso_code,
114+
project_name=project_name,
115+
measure_power_secs=metrics_interval,
116+
api_call_interval=1,
74117
experiment_id=None,
75118
experiment_name=None,
76119
logging_logger=CodeCarbonOutput(simvue_run),

simvue/executor.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pathlib
2323
import time
2424
import typing
25+
from simvue.api.objects.alert.user import UserAlert
2526

2627
if typing.TYPE_CHECKING:
2728
import simvue
@@ -113,6 +114,7 @@ def __init__(self, simvue_runner: "simvue.Run", keep_logs: bool = True) -> None:
113114
self._alert_ids: dict[str, str] = {}
114115
self.command_str: dict[str, str] = {}
115116
self._processes: dict[str, subprocess.Popen] = {}
117+
self._all_processes: list[psutil.Process] = []
116118

117119
def std_out(self, process_id: str) -> str | None:
118120
if not os.path.exists(out_file := f"{self._runner.name}_{process_id}.out"):
@@ -270,19 +272,48 @@ def processes(self) -> list[psutil.Process]:
270272
if not self._processes:
271273
return []
272274

273-
_all_processes: list[psutil.Process] = []
275+
_current_processes: list[psutil.Process] = []
274276

275277
for process in self._processes.values():
276278
with contextlib.suppress(psutil.NoSuchProcess):
277-
_all_processes.append(psutil.Process(process.pid))
279+
_current_processes.append(psutil.Process(process.pid))
278280

279281
with contextlib.suppress(psutil.NoSuchProcess, psutil.ZombieProcess):
280-
for process in _all_processes:
282+
for process in _current_processes:
281283
for child in process.children(recursive=True):
282-
if child not in _all_processes:
283-
_all_processes.append(child)
284+
if child not in _current_processes:
285+
_current_processes.append(child)
284286

285-
return list(set(_all_processes))
287+
_current_pids = set([_process.pid for _process in _current_processes])
288+
_previous_pids = set([_process.pid for _process in self._all_processes])
289+
290+
# Find processes which used to exist, which are no longer running
291+
_expired_process_pids = _previous_pids - _current_pids
292+
293+
# Remove these processes from list of all processes
294+
self._all_processes = [
295+
_process
296+
for _process in self._all_processes
297+
if _process.pid not in _expired_process_pids
298+
]
299+
300+
# Find new processes
301+
_new_process_pids = _current_pids - _previous_pids
302+
_new_processes = [
303+
_process
304+
for _process in _current_processes
305+
if _process.pid in _new_process_pids
306+
]
307+
308+
# Get CPU usage stats for each of those new processes, so that next time it's measured by the heartbeat the value is accurate
309+
if _new_processes:
310+
[_process.cpu_percent() for _process in _new_processes]
311+
time.sleep(0.1)
312+
313+
# Add these to the list of all processes
314+
self._all_processes += _new_processes
315+
316+
return self._all_processes
286317

287318
@property
288319
def success(self) -> int:
@@ -342,17 +373,26 @@ def _update_alerts(self) -> None:
342373
# allowing the executor to finish (and as such the run instance to exit)
343374
_wait_limit: float = 1
344375
for proc_id, process in self._processes.items():
376+
# We don't want to override the user's setting for the alert status
377+
# This is so that if a process incorrectly reports its return code,
378+
# the user can manually set the correct status depending on logs etc.
379+
_alert = UserAlert(identifier=self._alert_ids[proc_id])
380+
_is_set = _alert.get_status(run_id=self._runner._id)
381+
345382
if process.returncode != 0:
346383
# If the process fails then purge the dispatcher event queue
347384
# and ensure that the stderr event is sent before the run closes
348385
if self._runner._dispatcher:
349386
self._runner._dispatcher.purge()
350-
351-
self._runner.log_alert(
352-
identifier=self._alert_ids[proc_id], state="critical"
353-
)
387+
if not _is_set:
388+
self._runner.log_alert(
389+
identifier=self._alert_ids[proc_id], state="critical"
390+
)
354391
else:
355-
self._runner.log_alert(identifier=self._alert_ids[proc_id], state="ok")
392+
if not _is_set:
393+
self._runner.log_alert(
394+
identifier=self._alert_ids[proc_id], state="ok"
395+
)
356396

357397
_current_time: float = 0
358398
while (

0 commit comments

Comments
 (0)