diff options
-rw-r--r-- | lib/spack/spack/installer.py | 45 | ||||
-rw-r--r-- | lib/spack/spack/solver/asp.py | 13 | ||||
-rw-r--r-- | lib/spack/spack/test/install.py | 8 | ||||
-rw-r--r-- | lib/spack/spack/test/util/timer.py | 149 | ||||
-rw-r--r-- | lib/spack/spack/util/timer.py | 155 | ||||
-rw-r--r-- | var/spack/repos/builtin.mock/packages/dev-build-test-install-phases/package.py | 5 |
6 files changed, 305 insertions, 70 deletions
diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py index 41860ced19..c348dd58c5 100644 --- a/lib/spack/spack/installer.py +++ b/lib/spack/spack/installer.py @@ -54,9 +54,9 @@ import spack.repo import spack.store import spack.util.executable import spack.util.path +import spack.util.timer as timer from spack.util.environment import EnvironmentModifications, dump_environment from spack.util.executable import which -from spack.util.timer import Timer #: Counter to support unique spec sequencing that is used to ensure packages #: with the same priority are (initially) processed in the order in which they @@ -302,9 +302,9 @@ def _install_from_cache(pkg, cache_only, explicit, unsigned=False): bool: ``True`` if the package was extract from binary cache, ``False`` otherwise """ - timer = Timer() + t = timer.Timer() installed_from_cache = _try_install_from_binary_cache( - pkg, explicit, unsigned=unsigned, timer=timer + pkg, explicit, unsigned=unsigned, timer=t ) pkg_id = package_id(pkg) if not installed_from_cache: @@ -314,14 +314,14 @@ def _install_from_cache(pkg, cache_only, explicit, unsigned=False): tty.msg("{0}: installing from source".format(pre)) return False - timer.stop() + t.stop() tty.debug("Successfully extracted {0} from binary cache".format(pkg_id)) _print_timer( pre=_log_prefix(pkg.name), pkg_id=pkg_id, - fetch=timer.phases.get("search", 0) + timer.phases.get("fetch", 0), - build=timer.phases.get("install", 0), - total=timer.total, + fetch=t.duration("search") + t.duration("fetch"), + build=t.duration("install"), + total=t.duration(), ) _print_installed_pkg(pkg.spec.prefix) spack.hooks.post_install(pkg.spec) @@ -370,7 +370,7 @@ def _process_external_package(pkg, explicit): def _process_binary_cache_tarball( - pkg, binary_spec, explicit, unsigned, mirrors_for_spec=None, timer=None + pkg, binary_spec, explicit, unsigned, mirrors_for_spec=None, timer=timer.NULL_TIMER ): """ Process the binary cache tarball. @@ -389,11 +389,11 @@ def _process_binary_cache_tarball( bool: ``True`` if the package was extracted from binary cache, else ``False`` """ + timer.start("fetch") download_result = binary_distribution.download_tarball( binary_spec, unsigned, mirrors_for_spec=mirrors_for_spec ) - if timer: - timer.phase("fetch") + timer.stop("fetch") # see #10063 : install from source if tarball doesn't exist if download_result is None: tty.msg("{0} exists in binary cache but with different hash".format(pkg.name)) @@ -403,6 +403,7 @@ def _process_binary_cache_tarball( tty.msg("Extracting {0} from binary cache".format(pkg_id)) # don't print long padded paths while extracting/relocating binaries + timer.start("install") with spack.util.path.filter_padding(): binary_distribution.extract_tarball( binary_spec, download_result, allow_root=False, unsigned=unsigned, force=False @@ -410,12 +411,11 @@ def _process_binary_cache_tarball( pkg.installed_from_binary_cache = True spack.store.db.add(pkg.spec, spack.store.layout, explicit=explicit) - if timer: - timer.phase("install") + timer.stop("install") return True -def _try_install_from_binary_cache(pkg, explicit, unsigned=False, timer=None): +def _try_install_from_binary_cache(pkg, explicit, unsigned=False, timer=timer.NULL_TIMER): """ Try to extract the package from binary cache. @@ -428,10 +428,10 @@ def _try_install_from_binary_cache(pkg, explicit, unsigned=False, timer=None): """ pkg_id = package_id(pkg) tty.debug("Searching for binary cache of {0}".format(pkg_id)) - matches = binary_distribution.get_mirrors_for_spec(pkg.spec) - if timer: - timer.phase("search") + timer.start("search") + matches = binary_distribution.get_mirrors_for_spec(pkg.spec) + timer.stop("search") if not matches: return False @@ -1904,7 +1904,7 @@ class BuildProcessInstaller(object): self.env_mods = install_args.get("env_modifications", EnvironmentModifications()) # timer for build phases - self.timer = Timer() + self.timer = timer.Timer() # If we are using a padded path, filter the output to compress padded paths # The real log still has full-length paths. @@ -1959,8 +1959,8 @@ class BuildProcessInstaller(object): pre=self.pre, pkg_id=self.pkg_id, fetch=self.pkg._fetch_time, - build=self.timer.total - self.pkg._fetch_time, - total=self.timer.total, + build=self.timer.duration() - self.pkg._fetch_time, + total=self.timer.duration(), ) _print_installed_pkg(self.pkg.prefix) @@ -2033,6 +2033,7 @@ class BuildProcessInstaller(object): ) with log_contextmanager as logger: + # Redirect stdout and stderr to daemon pipe with logger.force_echo(): inner_debug_level = tty.debug_level() tty.set_debug(debug_level) @@ -2040,12 +2041,12 @@ class BuildProcessInstaller(object): tty.msg(msg.format(self.pre, phase_fn.name)) tty.set_debug(inner_debug_level) - # Redirect stdout and stderr to daemon pipe - self.timer.phase(phase_fn.name) - # Catch any errors to report to logging phase_fn.execute() + + self.timer.start(phase_fn.name) spack.hooks.on_phase_success(pkg, phase_fn.name, log_file) + self.timer.stop(phase_fn.name) except BaseException: combine_phase_logs(pkg.phase_log_files, pkg.log_path) diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index c070741c2c..9ff88af4df 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -619,11 +619,13 @@ class PyclingoDriver(object): self.control = control or default_clingo_control() # set up the problem -- this generates facts and rules self.assumptions = [] + timer.start("setup") with self.control.backend() as backend: self.backend = backend setup.setup(self, specs, reuse=reuse) - timer.phase("setup") + timer.stop("setup") + timer.start("load") # read in the main ASP program and display logic -- these are # handwritten, not generated, so we load them as resources parent_dir = os.path.dirname(__file__) @@ -653,12 +655,13 @@ class PyclingoDriver(object): self.control.load(os.path.join(parent_dir, "concretize.lp")) self.control.load(os.path.join(parent_dir, "os_compatibility.lp")) self.control.load(os.path.join(parent_dir, "display.lp")) - timer.phase("load") + timer.stop("load") # Grounding is the first step in the solve -- it turns our facts # and first-order logic rules into propositional logic. + timer.start("ground") self.control.ground([("base", [])]) - timer.phase("ground") + timer.stop("ground") # With a grounded program, we can run the solve. result = Result(specs) @@ -676,8 +679,10 @@ class PyclingoDriver(object): if clingo_cffi: solve_kwargs["on_unsat"] = cores.append + + timer.start("solve") solve_result = self.control.solve(**solve_kwargs) - timer.phase("solve") + timer.stop("solve") # once done, construct the solve result result.satisfiable = solve_result.satisfiable diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py index da54216402..4f6ef31a41 100644 --- a/lib/spack/spack/test/install.py +++ b/lib/spack/spack/test/install.py @@ -252,12 +252,8 @@ def test_install_times(install_mockery, mock_fetch, mutable_mock_repo): # The order should be maintained phases = [x["name"] for x in times["phases"]] - total = sum([x["seconds"] for x in times["phases"]]) - for name in ["one", "two", "three", "install"]: - assert name in phases - - # Give a generous difference threshold - assert abs(total - times["total"]["seconds"]) < 5 + assert phases == ["one", "two", "three", "install"] + assert all(isinstance(x["seconds"], float) for x in times["phases"]) def test_flatten_deps(install_mockery, mock_fetch, mutable_mock_repo): diff --git a/lib/spack/spack/test/util/timer.py b/lib/spack/spack/test/util/timer.py new file mode 100644 index 0000000000..16c1564663 --- /dev/null +++ b/lib/spack/spack/test/util/timer.py @@ -0,0 +1,149 @@ +# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +import json +from io import StringIO + +import spack.util.timer as timer + + +class Tick(object): + """Timer that increments the seconds passed by 1 + everytime tick is called.""" + + def __init__(self): + self.time = 0.0 + + def tick(self): + self.time += 1 + return self.time + + +def test_timer(): + # 0 + t = timer.Timer(now=Tick().tick) + + # 1 (restart) + t.start() + + # 2 + t.start("wrapped") + + # 3 + t.start("first") + + # 4 + t.stop("first") + assert t.duration("first") == 1.0 + + # 5 + t.start("second") + + # 6 + t.stop("second") + assert t.duration("second") == 1.0 + + # 7-8 + with t.measure("third"): + pass + assert t.duration("third") == 1.0 + + # 9 + t.stop("wrapped") + assert t.duration("wrapped") == 7.0 + + # tick 10-13 + t.start("not-stopped") + assert t.duration("not-stopped") == 1.0 + assert t.duration("not-stopped") == 2.0 + assert t.duration("not-stopped") == 3.0 + + # 14 + assert t.duration() == 13.0 + + # 15 + t.stop() + assert t.duration() == 14.0 + + +def test_timer_stop_stops_all(): + # Ensure that timer.stop() effectively stops all timers. + + # 0 + t = timer.Timer(now=Tick().tick) + + # 1 + t.start("first") + + # 2 + t.start("second") + + # 3 + t.start("third") + + # 4 + t.stop() + + assert t.duration("first") == 3.0 + assert t.duration("second") == 2.0 + assert t.duration("third") == 1.0 + assert t.duration() == 4.0 + + +def test_stopping_unstarted_timer_is_no_error(): + t = timer.Timer(now=Tick().tick) + assert t.duration("hello") == 0.0 + t.stop("hello") + assert t.duration("hello") == 0.0 + + +def test_timer_write(): + text_buffer = StringIO() + json_buffer = StringIO() + + # 0 + t = timer.Timer(now=Tick().tick) + + # 1 + t.start("timer") + + # 2 + t.stop("timer") + + # 3 + t.stop() + + t.write_tty(text_buffer) + t.write_json(json_buffer) + + output = text_buffer.getvalue().splitlines() + assert "timer" in output[0] + assert "1.000s" in output[0] + assert "total" in output[1] + assert "3.000s" in output[1] + + deserialized = json.loads(json_buffer.getvalue()) + assert deserialized == { + "phases": [{"name": "timer", "seconds": 1.0}], + "total": {"seconds": 3.0}, + } + + +def test_null_timer(): + # Just ensure that the interface of the noop-timer doesn't break at some point + buffer = StringIO() + t = timer.NullTimer() + t.start() + t.start("first") + t.stop("first") + with t.measure("second"): + pass + t.stop() + assert t.duration("first") == 0.0 + assert t.duration() == 0.0 + assert not t.phases + t.write_json(buffer) + t.write_tty(buffer) + assert not buffer.getvalue() diff --git a/lib/spack/spack/util/timer.py b/lib/spack/spack/util/timer.py index f7a9bc8e9d..f7f13e6a3e 100644 --- a/lib/spack/spack/util/timer.py +++ b/lib/spack/spack/util/timer.py @@ -11,51 +11,140 @@ a stack trace and drops the user into an interpreter. """ import sys import time +from collections import OrderedDict, namedtuple +from contextlib import contextmanager + +from llnl.util.lang import pretty_seconds import spack.util.spack_json as sjson +Interval = namedtuple("Interval", ("begin", "end")) -class Timer(object): - """ - Simple timer for timing phases of a solve or install - """ - - def __init__(self): - self.start = time.time() - self.last = self.start - self.phases = {} - self.end = None - - def phase(self, name): - last = self.last - now = time.time() - self.phases[name] = now - last - self.last = now +#: name for the global timer (used in start(), stop(), duration() without arguments) +global_timer_name = "_global" + + +class NullTimer(object): + """Timer interface that does nothing, useful in for "tell + don't ask" style code when timers are optional.""" + + def start(self, name=global_timer_name): + pass + + def stop(self, name=global_timer_name): + pass + + def duration(self, name=global_timer_name): + return 0.0 + + @contextmanager + def measure(self, name): + yield @property - def total(self): - """Return the total time""" - if self.end: - return self.end - self.start - return time.time() - self.start + def phases(self): + return [] + + def write_json(self, out=sys.stdout): + pass - def stop(self): + def write_tty(self, out=sys.stdout): + pass + + +#: instance of a do-nothing timer +NULL_TIMER = NullTimer() + + +class Timer(object): + """Simple interval timer""" + + def __init__(self, now=time.time): """ - Stop the timer to record a total time, if desired. + Arguments: + now: function that gives the seconds since e.g. epoch """ - self.end = time.time() + self._now = now + self._timers = OrderedDict() # type: OrderedDict[str,Interval] - def write_json(self, out=sys.stdout): + # _global is the overal timer since the instance was created + self._timers[global_timer_name] = Interval(self._now(), end=None) + + def start(self, name=global_timer_name): """ - Write a json object with times to file + Start or restart a named timer, or the global timer when no name is given. + + Arguments: + name (str): Optional name of the timer. When no name is passed, the + global timer is started. """ - phases = [{"name": p, "seconds": s} for p, s in self.phases.items()] - times = {"phases": phases, "total": {"seconds": self.total}} + self._timers[name] = Interval(self._now(), None) + + def stop(self, name=global_timer_name): + """ + Stop a named timer, or all timers when no name is given. Stopping a + timer that has not started has no effect. + + Arguments: + name (str): Optional name of the timer. When no name is passed, all + timers are stopped. + """ + interval = self._timers.get(name, None) + if not interval: + return + self._timers[name] = Interval(interval.begin, self._now()) + + def duration(self, name=global_timer_name): + """ + Get the time in seconds of a named timer, or the total time if no + name is passed. The duration is always 0 for timers that have not been + started, no error is raised. + + Arguments: + name (str): (Optional) name of the timer + + Returns: + float: duration of timer. + """ + try: + interval = self._timers[name] + except KeyError: + return 0.0 + # Take either the interval end, the global timer, or now. + end = interval.end or self._timers[global_timer_name].end or self._now() + return end - interval.begin + + @contextmanager + def measure(self, name): + """ + Context manager that allows you to time a block of code. + + Arguments: + name (str): Name of the timer + """ + begin = self._now() + yield + self._timers[name] = Interval(begin, self._now()) + + @property + def phases(self): + """Get all named timers (excluding the global/total timer)""" + return [k for k in self._timers.keys() if k != global_timer_name] + + def write_json(self, out=sys.stdout): + """Write a json object with times to file""" + phases = [{"name": p, "seconds": self.duration(p)} for p in self.phases] + times = {"phases": phases, "total": {"seconds": self.duration()}} out.write(sjson.dump(times)) def write_tty(self, out=sys.stdout): - now = time.time() - out.write("Time:\n") - for phase, t in self.phases.items(): - out.write(" %-15s%.4f\n" % (phase + ":", t)) - out.write("Total: %.4f\n" % (now - self.start)) + """Write a human-readable summary of timings""" + # Individual timers ordered by registration + formatted = [(p, pretty_seconds(self.duration(p))) for p in self.phases] + + # Total time + formatted.append(("total", pretty_seconds(self.duration()))) + + # Write to out + for name, duration in formatted: + out.write(" {:10s} {:>10s}\n".format(name, duration)) diff --git a/var/spack/repos/builtin.mock/packages/dev-build-test-install-phases/package.py b/var/spack/repos/builtin.mock/packages/dev-build-test-install-phases/package.py index 916156c1f5..1576e36ad4 100644 --- a/var/spack/repos/builtin.mock/packages/dev-build-test-install-phases/package.py +++ b/var/spack/repos/builtin.mock/packages/dev-build-test-install-phases/package.py @@ -3,8 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -from time import sleep - from spack.package import * @@ -17,15 +15,12 @@ class DevBuildTestInstallPhases(Package): phases = ["one", "two", "three", "install"] def one(self, spec, prefix): - sleep(1) print("One locomoco") def two(self, spec, prefix): - sleep(2) print("Two locomoco") def three(self, spec, prefix): - sleep(3) print("Three locomoco") def install(self, spec, prefix): |