From a5f56b2a869ede0de0501ce2ac915b82e8cc695a Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Fri, 13 Dec 2024 10:04:22 +0000 Subject: [PATCH 1/5] doc: Fix plot caching FIX Add the plot configuration to the plot cache key. --- doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 1a9f1278a..f0b7b4368 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -222,6 +222,8 @@ def prepare(home, enable_plots, outdir): notebooks = [ 'examples/analysis_plots.ipynb', ] + + plot_conf_path = Path(home, 'doc', 'plot_conf.yml') if enable_plots: def populate(key, temp_path): # We pre-generate all the plots, otherwise we would end up running @@ -230,7 +232,6 @@ def prepare(home, enable_plots, outdir): # https://github.com/sphinx-doc/sphinx/issues/12201 hv.extension('bokeh') - plot_conf_path = Path(home, 'doc', 'plot_conf.yml') plot_conf = DocPlotConf.from_yaml_map(plot_conf_path) plots = autodoc_pre_make_plots(plot_conf) with open(temp_path / 'plots.pickle', 'wb') as f: @@ -265,6 +266,7 @@ def prepare(home, enable_plots, outdir): bokeh.__version__, pn.__version__, jupyterlab.__version__, + plot_conf_path.read_text(), ) cache_path = dir_cache.get_entry(key) with open(cache_path / 'plots.pickle', 'rb') as f: -- GitLab From d001c945f6d4739e19be035807814d3574643e20 Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Fri, 13 Dec 2024 13:09:58 +0000 Subject: [PATCH 2/5] doc: Support inheritance for plot methods FIX Ensure we can deal with inherited plot methods and accurately represent the derived class the method is looked up on. --- doc/conf.py | 48 +++++++++++++++++++++++++++++++++--- lisa/_cli_tools/lisa_plot.py | 2 +- lisa/_doc/helpers.py | 37 +++++++++++++-------------- lisa/analysis/base.py | 45 ++++++++++++++++++++------------- 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f0b7b4368..7ec0577dd 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -31,6 +31,7 @@ import pickle import shutil import shlex from urllib.parse import urlparse +import itertools from sphinx.domains.python import PythonDomain @@ -45,7 +46,7 @@ sys.path.insert(0, os.path.abspath('../')) # Import our packages after modifying sys.path import lisa -from lisa.utils import sphinx_nitpick_ignore, setup_logging, get_obj_name, DirCache +from lisa.utils import sphinx_nitpick_ignore, setup_logging, get_obj_name, DirCache, resolve_dotted_name from lisa.version import VERSION_TOKEN from lisa._doc.helpers import ( autodoc_process_test_method, autodoc_process_analysis_events, @@ -55,6 +56,7 @@ from lisa._doc.helpers import ( DocPlotConf, autodoc_pre_make_plots, intersphinx_warn_missing_reference_handler, ) +from lisa.analysis.base import TraceAnalysisBase import devlib @@ -225,7 +227,30 @@ def prepare(home, enable_plots, outdir): plot_conf_path = Path(home, 'doc', 'plot_conf.yml') if enable_plots: + + def get_plot_methods(names=None): + meths = set(itertools.chain.from_iterable( + subclass.get_plot_methods() + for subclass in TraceAnalysisBase.get_analysis_classes().values() + )) + + if names is None: + return meths + else: + meths = { + get_obj_name(f): f + for f in meths + } + return { + f + for name in names + if (f := meths.get(name)) + } + def populate(key, temp_path): + (names, *_) = key + plot_methods = get_plot_methods(names) + # We pre-generate all the plots, otherwise we would end up running # polars code in a multiprocessing subprocess created by forking # CPython, leading to deadlocks: @@ -233,7 +258,15 @@ def prepare(home, enable_plots, outdir): hv.extension('bokeh') plot_conf = DocPlotConf.from_yaml_map(plot_conf_path) - plots = autodoc_pre_make_plots(plot_conf) + plots = autodoc_pre_make_plots(plot_conf, plot_methods) + plots = { + # Serialize by name so pickle does not raise an exception + # because of the wrappers with the updated __qualname__ and + # __module__. Otherwise, their name resolves to something else + # and it pickle does not allow that. + get_obj_name(k): v + for k, v in plots.items() + } with open(temp_path / 'plots.pickle', 'wb') as f: pickle.dump(plots, f) @@ -260,8 +293,13 @@ def prepare(home, enable_plots, outdir): import panel as pn import jupyterlab + plot_methods = { + get_obj_name(f): f + for f in get_plot_methods() + } dir_cache = DirCache('doc_plots', populate=populate) key = ( + sorted(plot_methods.keys()), hv.__version__, bokeh.__version__, pn.__version__, @@ -269,8 +307,12 @@ def prepare(home, enable_plots, outdir): plot_conf_path.read_text(), ) cache_path = dir_cache.get_entry(key) + with open(cache_path / 'plots.pickle', 'rb') as f: - plots = pickle.load(f) + plots = { + plot_methods[name]: v + for name, v in pickle.load(f).items() + } for _path in notebooks: shutil.copy2( diff --git a/lisa/_cli_tools/lisa_plot.py b/lisa/_cli_tools/lisa_plot.py index d0716f154..90ad3b8b9 100755 --- a/lisa/_cli_tools/lisa_plot.py +++ b/lisa/_cli_tools/lisa_plot.py @@ -308,7 +308,7 @@ Available plots: events = sorted(events) print('Parsing trace events: {}'.format(', '.join(events))) - trace = Trace(args.trace, plat_info=plat_info, events=events, normalize_time=args.normalize_time, write_swap=True) + trace = Trace(args.trace, plat_info=plat_info, events=events, normalize_time=args.normalize_time) if args.window: window = args.window def clip(l, x, r): diff --git a/lisa/_doc/helpers.py b/lisa/_doc/helpers.py index 88f393ea5..3656ef8d0 100644 --- a/lisa/_doc/helpers.py +++ b/lisa/_doc/helpers.py @@ -1970,7 +1970,7 @@ class DocPlotConf(SimpleMultiSrcConf): KeyDesc('plots', 'Mapping of function qualnames to their settings', [Mapping], deepcopy_val=False), )) -def autodoc_pre_make_plots(conf): +def autodoc_pre_make_plots(conf, plot_methods): def spec_of_meth(conf, meth_name): plot_conf = conf['plots'] default_spec = plot_conf.get('default', {}) @@ -2037,11 +2037,6 @@ def autodoc_pre_make_plots(conf): print(f'Plot for {meth.__qualname__} generated in {m.delta}s') return rst_figure - plot_methods = set(itertools.chain.from_iterable( - subclass.get_plot_methods() - for subclass in TraceAnalysisBase.get_analysis_classes().values() - )) - preload_events(conf, plot_methods) plots = { meth: _make_plot(meth) @@ -2055,7 +2050,6 @@ def autodoc_process_analysis_plots(app, what, name, obj, options, lines, plots): if what != 'method': return - name = get_obj_name(obj) try: rst_figure = plots[name] except KeyError: @@ -2066,20 +2060,23 @@ def autodoc_process_analysis_plots(app, what, name, obj, options, lines, plots): lines[:0] = rst_figure.splitlines() -def ana_invocation(obj): - methods = { - func: subclass - for subclass in TraceAnalysisBase.get_analysis_classes().values() - for name, func in inspect.getmembers(subclass, callable) - } +def ana_invocation(obj, name=None): + if callable(obj): + if name: + try: + cls = _get_parent_namespace(name) + except ModuleNotFoundError: + raise ValueError(f'Cannot compute the parent namespace of: {obj}') + else: + cls = get_parent_namespace(obj) - try: - cls = methods[obj] - except (KeyError, TypeError): - raise ValueError(f'Could not find method {obj}') + if cls and (not inspect.ismodule(cls)) and issubclass(cls, AnalysisHelpers): + on_trace_name = f'trace.ana.{cls.name}.{obj.__name__}' + return f"*Called on* :class:`~lisa.trace.Trace` *instances as* ``{on_trace_name}()``" + else: + raise ValueError(f'{obj} is not a method of an analysis class') else: - on_trace_name = f'trace.ana.{cls.name}.{obj.__name__}' - return f"*Called on* :class:`~lisa.trace.Trace` *instances as* ``{on_trace_name}()``" + raise ValueError(f'{obj} is not a method') def autodoc_process_analysis_methods(app, what, name, obj, options, lines): @@ -2087,7 +2084,7 @@ def autodoc_process_analysis_methods(app, what, name, obj, options, lines): Append the list of required trace events """ try: - extra_doc = ana_invocation(obj) + extra_doc = ana_invocation(obj, name) except ValueError: pass else: diff --git a/lisa/analysis/base.py b/lisa/analysis/base.py index fe40cedb3..b8b340dd9 100644 --- a/lisa/analysis/base.py +++ b/lisa/analysis/base.py @@ -46,7 +46,7 @@ import polars as pl import pandas as pd -from lisa.utils import Loggable, deprecate, get_doc_url, get_short_doc, get_subclasses, guess_format, is_running_ipython, measure_time, memoized, update_wrapper_doc, _import_all_submodules, optional_kwargs +from lisa.utils import Loggable, deprecate, get_doc_url, get_short_doc, get_subclasses, guess_format, is_running_ipython, measure_time, memoized, update_wrapper_doc, _import_all_submodules, optional_kwargs, get_parent_namespace from lisa.trace import _CacheDataDesc from lisa.notebook import _hv_fig_to_pane, _hv_link_dataframes, _hv_has_options, axis_cursor_delta, axis_link_dataframes, make_figure from lisa.datautils import _df_to, _pandas_cleanup_df @@ -54,6 +54,22 @@ from lisa.datautils import _df_to, _pandas_cleanup_df # Ensure hv.extension() is called import lisa.notebook +# Make sure we associate each plot method with a single wrapped object, so that +# the resulting wrapper can be used as a key in dictionaries. +@functools.lru_cache(maxsize=None, typed=True) +def _wrap_plot_method(cls, f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + + # Wrap the method so that we record the actual class they were + # looked up on, rather than the base class they happen to be + # defined in. + wrapper.__qualname__ = f'{cls.__qualname__}.{f.__name__}' + wrapper.__module__ = cls.__module__ + return wrapper + + class AnalysisHelpers(Loggable, abc.ABC): """ @@ -393,7 +409,7 @@ class AnalysisHelpers(Loggable, abc.ABC): @classmethod def _get_doc_methods(cls, prefix, instance=None, ignored=None): - ignored = set(ignored) or set() + ignored = set(ignored or []) obj = instance if instance is not None else cls def predicate(f): @@ -410,7 +426,7 @@ class AnalysisHelpers(Loggable, abc.ABC): ) return [ - f + _wrap_plot_method(cls, f) for name, f in inspect.getmembers(obj, predicate=predicate) if f not in ignored ] @@ -1368,20 +1384,15 @@ class TraceAnalysisBase(AnalysisHelpers): it and call the resulting bound method with ``meth_kwargs`` extra keyword arguments. """ - for subcls in cls.get_analysis_classes().values(): - for name, f in inspect.getmembers(subcls): - if f is meth: - break - else: - continue - break + classes = cls.get_analysis_classes().values() + subcls = get_parent_namespace(meth) + if subcls in classes: + # Create an analysis instance and bind the method to it + analysis = subcls(trace=trace) + meth = meth.__get__(analysis, type(analysis)) + + return meth(**meth_kwargs) else: - raise ValueError(f'{meth.__qualname__} is not a method of any subclasses of {cls.__qualname__}') - - # Create an analysis instance and bind the method to it - analysis = subcls(trace=trace) - meth = meth.__get__(analysis, type(analysis)) - - return meth(**meth_kwargs) + raise ValueError(f'Parent class of {meth} is not a registered analysis') # vim :set tabstop=4 shiftwidth=4 expandtab textwidth=80 -- GitLab From dd91f144eefaff3c1922815d9208757a10a472bc Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Thu, 19 Dec 2024 14:28:44 +0000 Subject: [PATCH 3/5] doc: Fix class name typo FIX --- doc/energy_analysis.rst | 2 +- doc/workflows/automated_testing.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/energy_analysis.rst b/doc/energy_analysis.rst index a65772d3f..fc8864a1c 100644 --- a/doc/energy_analysis.rst +++ b/doc/energy_analysis.rst @@ -23,7 +23,7 @@ by EAS, and lets us do some energy analysis. providing this target does have an energy model. Its most noteworthy use is in our :meth:`EAS behavioural tests -`, as it lets us +`, as it lets us estimate the amount of energy consumed in an execution trace and compare this to an estimated energy-optimal placement. diff --git a/doc/workflows/automated_testing.rst b/doc/workflows/automated_testing.rst index 4bbd275c7..1e68a00f4 100644 --- a/doc/workflows/automated_testing.rst +++ b/doc/workflows/automated_testing.rst @@ -225,7 +225,7 @@ Later on, the processing methods can be run from the data collected: processing code over the set of data acquired during an earlier session. A typical use case would be to look at the impact of changing a margin of a test like the ``energy_est_threshold_pct`` parameter of - :meth:`~lisa_tests.kernel..scheduler.eas_behaviour.EASBehaviour.test_task_placement` + :meth:`~lisa_tests.arm.kernel.scheduler.eas_behaviour.EASBehaviour.test_task_placement` Aggregating results ------------------- -- GitLab From 812089cac8407d6a0fe966358b93dbf2ac309335 Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Mon, 23 Dec 2024 16:31:29 +0000 Subject: [PATCH 4/5] doc: Fix warning when copying notebook from cache FIX Fix warning when copying rendered notebook from cache into the doc build output folder if the destination already exists. --- doc/conf.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 7ec0577dd..8ae736ac7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -95,6 +95,14 @@ def getvar(name, default=_NO_DEFAULT): return v +def copy_file(src, dst): + src = Path(src) + dst = Path(dst) + + dst.unlink(missing_ok=True) + shutil.copy2(src, dst) + + def prepare(home, enable_plots, outdir): configs = {} outdir = Path(outdir).resolve() @@ -315,7 +323,7 @@ def prepare(home, enable_plots, outdir): } for _path in notebooks: - shutil.copy2( + copy_file( cache_path / 'ipynb' / _path, Path(home, 'doc', 'workflows', 'ipynb') / _path, ) @@ -323,7 +331,7 @@ def prepare(home, enable_plots, outdir): else: plots = {} for _path in notebooks: - shutil.copy2( + copy_file( notebooks_in_base / _path, Path(home, 'doc', 'workflows', 'ipynb') / _path, ) -- GitLab From 64b4506a0bd18226f8cc76c4cd5595c94a3b3f37 Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Mon, 23 Dec 2024 16:43:05 +0000 Subject: [PATCH 5/5] doc: Fix caching of rendered notebooks FIX Ensure the list of notebook to render is part of the cache key. --- doc/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 8ae736ac7..c6b0006f2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -256,7 +256,7 @@ def prepare(home, enable_plots, outdir): } def populate(key, temp_path): - (names, *_) = key + (names, notebooks, *_) = key plot_methods = get_plot_methods(names) # We pre-generate all the plots, otherwise we would end up running @@ -279,6 +279,8 @@ def prepare(home, enable_plots, outdir): pickle.dump(plots, f) for _path in notebooks: + _path = Path(_path) + in_path = notebooks_in_base / _path out_path = temp_path / 'ipynb' / _path out_path.parent.mkdir(parents=True, exist_ok=True) @@ -286,6 +288,7 @@ def prepare(home, enable_plots, outdir): out_path.unlink() except FileNotFoundError: pass + logging.info(f'Refreshing notebook: {in_path}') subprocess.check_call([ 'jupyter', @@ -308,6 +311,7 @@ def prepare(home, enable_plots, outdir): dir_cache = DirCache('doc_plots', populate=populate) key = ( sorted(plot_methods.keys()), + notebooks, hv.__version__, bokeh.__version__, pn.__version__, -- GitLab