diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 69d621bbc1..781ba2ff23 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -173,7 +173,7 @@ class BaseInterface(Interface): _additional_metadata = [] _redirect_x = False _references = [] - resource_monitor = True # Enabled for this interface IFF enabled in the config + resource_monitor = None # Enabled for this interface IFF enabled in the config _etelemetry_version_data = None def __init__( @@ -202,9 +202,10 @@ def __init__( self.inputs.trait_set(**inputs) self.ignore_exception = ignore_exception - - if resource_monitor is not None: - self.resource_monitor = resource_monitor + self.resource_monitor = ( + resource_monitor if resource_monitor is not None + else config.resource_monitor + ) if from_file is not None: self.load_inputs_from_json(from_file, overwrite=True) @@ -376,18 +377,13 @@ def run(self, cwd=None, ignore_exception=None, **inputs): if successful, results """ - rtc = RuntimeContext( - resource_monitor=config.resource_monitor and self.resource_monitor, - ignore_exception=ignore_exception - if ignore_exception is not None - else self.ignore_exception, - ) with indirectory(cwd or os.getcwd()): self.inputs.trait_set(**inputs) self._check_mandatory_inputs() self._check_version_requirements(self.inputs) + rtc = RuntimeContext() with rtc(self, cwd=cwd, redirect_x=self._redirect_x) as runtime: # Grab inputs now, as they should not change during execution diff --git a/nipype/interfaces/base/support.py b/nipype/interfaces/base/support.py index 14c8a55da1..d2c4ba7cc2 100644 --- a/nipype/interfaces/base/support.py +++ b/nipype/interfaces/base/support.py @@ -19,6 +19,7 @@ from ... import logging, config from ...utils.misc import is_container, rgetcwd from ...utils.filemanip import md5, hash_infile +from ...utils.profiler import ResourceMonitor iflogger = logging.getLogger("nipype.interface") @@ -30,27 +31,28 @@ class RuntimeContext(AbstractContextManager): __slots__ = ("_runtime", "_resmon", "_ignore_exc") - def __init__(self, resource_monitor=False, ignore_exception=False): + def __init__(self): """Initialize the context manager object.""" - self._ignore_exc = ignore_exception - _proc_pid = os.getpid() - if resource_monitor: - from ...utils.profiler import ResourceMonitor - else: - from ...utils.profiler import ResourceMonitorMock as ResourceMonitor + self._ignore_exc = False + self._resmon = None - self._resmon = ResourceMonitor( - _proc_pid, - freq=float(config.get("execution", "resource_monitor_frequency", 1)), - ) - def __call__(self, interface, cwd=None, redirect_x=False): + def __call__(self, interface, cwd=None, redirect_x=False, ignore_exception=False): """Generate a new runtime object.""" # Tear-up: get current and prev directories _syscwd = rgetcwd(error=False) # Recover when wd does not exist if cwd is None: cwd = _syscwd + self._ignore_exc = ignore_exception or interface.ignore_exception + if interface.resource_monitor is True: + _proc_pid = os.getpid() + self._resmon = ResourceMonitor( + _proc_pid, + cwd=cwd, + freq=float(config.get("execution", "resource_monitor_frequency", 1)), + ) + self._runtime = Bunch( cwd=str(cwd), duration=None, @@ -61,7 +63,7 @@ def __call__(self, interface, cwd=None, redirect_x=False): platform=platform.platform(), prevcwd=str(_syscwd), redirect_x=redirect_x, - resmon=self._resmon.fname or "off", + resmon=getattr(self._resmon, "fname", "off"), returncode=None, startTime=None, version=interface.version, @@ -74,7 +76,8 @@ def __enter__(self): self._runtime.environ["DISPLAY"] = config.get_display() self._runtime.startTime = dt.isoformat(dt.utcnow()) - self._resmon.start() + if self._resmon: + self._resmon.start() # TODO: Perhaps clean-up path and ensure it exists? os.chdir(self._runtime.cwd) return self._runtime @@ -87,8 +90,9 @@ def __exit__(self, exc_type, exc_value, exc_tb): timediff.days * 86400 + timediff.seconds + timediff.microseconds / 1e6 ) # Collect monitored data - for k, v in self._resmon.stop().items(): - setattr(self._runtime, k, v) + if self._resmon: + for k, v in self._resmon.stop().items(): + setattr(self._runtime, k, v) os.chdir(self._runtime.prevcwd) diff --git a/nipype/utils/profiler.py b/nipype/utils/profiler.py index 2179b29db6..266affcaec 100644 --- a/nipype/utils/profiler.py +++ b/nipype/utils/profiler.py @@ -24,47 +24,32 @@ _MB = 1024.0**2 -class ResourceMonitorMock: - """A mock class to use when the monitor is disabled.""" - - @property - def fname(self): - """Get/set the internal filename""" - return None - - def __init__(self, pid, freq=5, fname=None, python=True): - pass - - def start(self): - pass - - def stop(self): - return {} - - class ResourceMonitor(threading.Thread): """ A ``Thread`` to monitor a specific PID with a certain frequence to a file """ - def __init__(self, pid, freq=5, fname=None, python=True): + def __init__(self, pid, freq=0.2, fname=None, cwd=None): # Make sure psutil is imported import psutil - if freq < 0.2: - raise RuntimeError("Frequency (%0.2fs) cannot be lower than 0.2s" % freq) + # Leave process initialized and make first sample + self._process = psutil.Process(pid) + _first_sample = self._sample(cpu_interval=0.2) - if fname is None: - fname = ".proc-%d_time-%s_freq-%0.2f" % (pid, time(), freq) - self._fname = os.path.abspath(fname) + # Continue monitor configuration + freq = max(freq, 0.2) + fname = fname or f".proc-{pid}" + self._fname = os.path.abspath( + os.path.join(cwd, fname) if cwd is not None else fname + ) self._logfile = open(self._fname, "w") self._freq = freq - self._python = python - # Leave process initialized and make first sample - self._process = psutil.Process(pid) - self._sample(cpu_interval=0.2) + # Dump first sample to file + print(",".join(f"{v}" for v in _first_sample), file=self._logfile) + self._logfile.flush() # Start thread threading.Thread.__init__(self) @@ -80,7 +65,8 @@ def stop(self): if not self._event.is_set(): self._event.set() self.join() - self._sample() + # Dump last sample to file + print(",".join(f"{v}" for v in self._sample()), file=self._logfile) self._logfile.flush() self._logfile.close() @@ -104,6 +90,7 @@ def stop(self): return retval def _sample(self, cpu_interval=None): + _time = time() cpu = 0.0 rss = 0.0 vms = 0.0 @@ -132,15 +119,16 @@ def _sample(self, cpu_interval=None): except psutil.NoSuchProcess: pass - print("%f,%f,%f,%f" % (time(), cpu, rss / _MB, vms / _MB), file=self._logfile) - self._logfile.flush() + return (_time, cpu, rss / _MB, vms / _MB) def run(self): """Core monitoring function, called by start()""" start_time = time() wait_til = start_time while not self._event.is_set(): - self._sample() + # Dump sample to file + print(",".join(f"{v}" for v in self._sample()), file=self._logfile) + self._logfile.flush() wait_til += self._freq self._event.wait(max(0, wait_til - time()))