def _from_target(cls, target: Target, *, res_dir: ArtifactPath, cpu, freq, switch_governor=True) -> 'UserspaceSanityItem': """ Create a :class:`UserspaceSanityItem` from a live :class:`lisa.target.Target`. :param cpu: CPU to run on. :type cpu: int :param freq: Frequency to run at. :type freq: int :param switch_governor: Switch the governor to userspace, and undo it at the end. If that has been done in advance, not doing it for every item saves substantial time. :type switch_governor: bool """ sysbench = Sysbench(target, res_dir=res_dir) cm = target.cpufreq.use_governor( 'userspace') if switch_governor else nullcontext() with cm: target.cpufreq.set_frequency(cpu, freq) sysbench.run(cpus=[cpu], max_duration_s=1) work = sysbench.output.nr_events return cls(res_dir, target.plat_info, cpu, freq, work)
def run(self, cpus=None, cgroup=None, as_root=False, update_cpu_capacities=None): logger = self.get_logger() plat_info = self.target.plat_info writeable_capacities = plat_info['cpu-capacities']['writeable'] if update_cpu_capacities: if not writeable_capacities: raise ValueError( 'CPU capacities are not writeable on this target, please use update_cpu_capacities=False or None' ) # If left to None, we update if possible elif update_cpu_capacities is None: update_cpu_capacities = writeable_capacities if not writeable_capacities: logger.warning( 'CPU capacities will not be updated on this platform') if update_cpu_capacities: rtapp_capacities = plat_info['cpu-capacities']['rtapp'] logger.info( f'Will update CPU capacities in sysfs: {rtapp_capacities}') write_kwargs = [ dict( path=f'/sys/devices/system/cpu/cpu{cpu}/cpu_capacity', value=capa, verify=True, ) for cpu, capa in sorted(rtapp_capacities.items()) ] capa_cm = self.target.batch_revertable_write_value(write_kwargs) else: # There might not be any rtapp calibration available, specifically # when we are being called to run the calibration workload. try: rtapp_capacities = plat_info['cpu-capacities']['rtapp'] orig_capacities = plat_info['cpu-capacities']['orig'] except KeyError: pass else: # Spit out some warning in case we are not going to update the # capacities, so we know what to expect RTA.warn_capacities_mismatch(orig_capacities, rtapp_capacities) capa_cm = nullcontext() with capa_cm: super().run(cpus, cgroup, as_root) if self.log_stats: logger.debug(f'Pulling logfiles to: {self.res_dir}') for task in self.tasks: # RT-app appends some number to the logs, so we can't predict the # exact filename logfile = self.target.path.join(self.run_dir, f'*{task}*.log') self.target.pull(logfile, self.res_dir, globbing=True)
def test_check_events(self): if self.expected_success: cm = nullcontext() else: cm = self.assertRaises(MissingTraceEventError) with cm: print('Checking: {}'.format(self.checker)) self.checker.check_events(self.EVENTS_SET)
def get_cpu_calibrations(cls, target, res_dir=None): """ Get the rt-ap calibration value for all CPUs. :param target: Devlib target to run calibration on. :returns: Dict mapping CPU numbers to rt-app calibration values. """ if not target.is_module_available('cpufreq'): cls.get_logger().warning( 'cpufreq module not loaded, skipping setting frequency to max') cm = nullcontext() else: cm = target.cpufreq.use_governor('performance') with cm, target.disable_idle_states(): return cls._calibrate(target, res_dir)
def wrapper(self, *args, filepath=None, axis=None, output=None, img_format=None, always_save=False, colors: TypedList[str] = None, linestyles: TypedList[str] = None, markers: TypedList[str] = None, rc_params=None, **kwargs): def is_f_param(param): """ Return True if the parameter is for `f`, False if it is for setup_plot() """ try: desc = inspect.signature(f).parameters[param] except KeyError: return False else: # Passing kwargs=42 to a function taking **kwargs # should not return True here, as we only consider # explicitly listed arguments return desc.kind not in ( inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL, ) # Factor the *args inside the **kwargs by binding them to the # user-facing signature, which is the one of the wrapper. kwargs.update( inspect.signature(wrapper).bind_partial(self, *args).arguments) f_kwargs = { param: val for param, val in kwargs.items() if is_f_param(param) } img_format = img_format or guess_format(filepath) or 'png' local_fig = axis is None # When we create the figure ourselves, always save the plot to # the default location if local_fig and filepath is None and always_save: filepath = self.get_default_plot_path( img_format=img_format, plot_name=f.__name__, ) cyclers = dict( color=colors, linestyle=linestyles, marker=markers, ) cyclers = { name: value for name, value in cyclers.items() if value } if cyclers: cyclers = [ make_cycler(**{name: value}) for name, value in cyclers.items() ] set_cycler = lambda axis: cls.set_axis_cycler( axis, *cyclers) else: set_cycler = lambda axis: nullcontext() if rc_params: set_rc_params = lambda axis: cls.set_axis_rc_params( axis, rc_params) else: set_rc_params = lambda axis: nullcontext() # Allow returning an axis directly, or just update a given axis if return_axis: # In that case, the function takes all the kwargs with set_cycler(axis), set_rc_params(axis): axis = f(**kwargs, axis=axis) else: if local_fig: setup_plot_kwargs = { param: val for param, val in kwargs.items() if param not in f_kwargs } fig, axis = self.setup_plot(**setup_plot_kwargs) f_kwargs.update( axis=axis, local_fig=f_kwargs.get('local_fig', local_fig), ) with set_cycler(axis), set_rc_params(axis): f(**f_kwargs) if isinstance(axis, numpy.ndarray): fig = axis[0].get_figure() else: fig = axis.get_figure() def resolve_formatter(fmt): format_map = { 'rst': cls._get_rst_content, 'html': cls._get_html, } try: return format_map[fmt] except KeyError: raise ValueError(f'Unsupported format: {fmt}') if output is None: out = axis # Show the LISA figure toolbar if is_running_ipython(): # Make sure we only add one button per figure try: toolbar = self._get_fig_data(fig, 'toolbar') except KeyError: toolbar = self._make_fig_toolbar(fig) self._set_fig_data(fig, 'toolbar', toolbar) display(toolbar) mplcursors.cursor(fig) else: out = resolve_formatter(output)(f, [], f_kwargs, axis) if filepath: if img_format in ('html', 'rst'): content = resolve_formatter(img_format)(f, [], f_kwargs, axis) with open(filepath, 'wt', encoding='utf-8') as fd: fd.write(content) else: fig.savefig(filepath, format=img_format, bbox_inches='tight') return out
def run_rtapp(cls, target, res_dir, profile=None, ftrace_coll=None, cg_cfg=None, wipe_run_dir=True): """ Run the given RTA profile on the target, and collect an ftrace trace. :param target: target to execute the workload on. :type target: lisa.target.Target :param res_dir: Artifact folder where the artifacts will be stored. :type res_dir: str or lisa.utils.ArtifactPath :param profile: ``rt-app`` profile, as a dictionary of ``dict(task_name, RTATask)``. If ``None``, :meth:`~lisa.tests.base.RTATestBundle.get_rtapp_profile` is called with ``target.plat_info``. :type profile: dict(str, lisa.wlgen.rta.RTATask) :param ftrace_coll: Ftrace collector to use to record the trace. This allows recording extra events compared to the default one, which is based on the ``ftrace_conf`` class attribute. :type ftrace_coll: lisa.trace.FtraceCollector :param cg_cfg: CGroup configuration dictionary. If ``None``, :meth:`lisa.tests.base.RTATestBundle.get_cgroup_configuration` is called with ``target.plat_info``. :type cg_cfg: dict :param wipe_run_dir: Remove the run directory on the target after execution of the workload. :type wipe_run_dir: bool """ trace_path = ArtifactPath.join(res_dir, cls.TRACE_PATH) dmesg_path = ArtifactPath.join(res_dir, cls.DMESG_PATH) ftrace_coll = ftrace_coll or FtraceCollector.from_conf( target, cls.ftrace_conf) dmesg_coll = DmesgCollector(target) profile = profile or cls.get_rtapp_profile(target.plat_info) cg_cfg = cg_cfg or cls.get_cgroup_configuration(target.plat_info) trace_events = [ event.replace('rtapp_', '') for event in ftrace_coll.events if event.startswith("rtapp_") ] wload = RTA.by_profile(target, "rta_{}".format(cls.__name__.lower()), profile, res_dir=res_dir, trace_events=trace_events) cgroup = cls._target_configure_cgroup(target, cg_cfg) as_root = cgroup is not None wload_cm = wload if wipe_run_dir else nullcontext(wload) # Pre-hit the calibration information, in case this is a lazy value. # This avoids polluting the trace and the dmesg output with the # calibration tasks. Since we know that rt-app will always need it for # anything useful, it's reasonable to do it here. target.plat_info['rtapp']['calib'] with wload_cm, dmesg_coll, ftrace_coll, target.freeze_userspace(): wload.run(cgroup=cgroup, as_root=as_root) ftrace_coll.get_trace(trace_path) dmesg_coll.get_trace(dmesg_path) return trace_path
def run(self, cpus=None, cgroup=None, background=False, as_root=False, update_cpu_capacities=None): logger = self.get_logger() if update_cpu_capacities is None: update_cpu_capacities = True best_effort = True else: best_effort = False if update_cpu_capacities: plat_info = self.target.plat_info calib_map = plat_info['rtapp']['calib'] true_capacities = self.get_cpu_capacities_from_calibrations(calib_map) # Average in a capacity class, since the kernel will only use one # value for the whole class anyway new_capacities = {} for capa_class in plat_info['capacity-classes']: avg_capa = mean( capa for cpu, capa in true_capacities.items() if cpu in capa_class ) new_capacities.update({cpu: avg_capa for cpu in capa_class}) # Make sure that the max cap is 1024 and that we use integer values new_max_cap = max(new_capacities.values()) new_capacities = { cpu: int(capa * (1024 / new_max_cap)) for cpu, capa in new_capacities.items() } write_kwargs = [ dict( path='/sys/devices/system/cpu/cpu{}/cpu_capacity'.format(cpu), value=capa, verify=True, ) for cpu, capa in sorted(new_capacities.items()) ] cm = self.target.batch_revertable_write_value(write_kwargs) class _CM(): def __enter__(self): logger.info('Updating CPU capacities in sysfs: {}'.format(new_capacities)) try: cm.__enter__() except TargetStableError as e: if best_effort: logger.warning('Could not update the CPU capacities: {}'.format(e)) else: raise def __exit__(self, *args, **kwargs): return cm.__exit__(*args, **kwargs) capa_cm = _CM() else: capa_cm = nullcontext() with capa_cm: super().run(cpus, cgroup, background, as_root) if background: # TODO: handle background case return if not self.log_stats: return logger.debug('Pulling logfiles to: {}'.format(self.res_dir)) for task in self.tasks: # RT-app appends some number to the logs, so we can't predict the # exact filename logfile = self.target.path.join(self.run_dir, '*{}*.log'.format(task)) self.target.pull(logfile, self.res_dir)
def main(argv=None): if argv is None: argv = sys.argv[1:] plots_map = get_plots_map() analysis_nice_name_map = { get_analysis_nice_name(name): name for name in plots_map.keys() } parser = argparse.ArgumentParser( description=""" CLI for LISA analysis plots and reports from traces. Available plots: {} """.format(get_analysis_listing(plots_map)), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( 'trace', help='trace-cmd trace.dat, or systrace file', ) parser.add_argument( '--normalize-time', action='store_true', help= 'Normalize the time in the plot, i.e. start at 0 instead of uptime timestamp', ) parser.add_argument( '--plot', nargs=2, action='append', default=[], metavar=('PLOT', 'OUTPUT_PATH'), help= 'Create the given plot. If OUTPUT_PATH is "interactive", an interactive window will be used', ) parser.add_argument( '--plot-analysis', nargs=3, action='append', default=[], metavar=('ANALYSIS', 'OUTPUT_FOLDER_PATH', 'FORMAT'), help='Create all the plots of the given analysis', ) parser.add_argument( '--plot-all', nargs=2, metavar=('OUTPUT_FOLDER_PATH', 'FORMAT'), help='Create all the plots in the given folder', ) parser.add_argument( '--best-effort', action='store_true', help= 'Try to generate as many of the requested plots as possible without early termination.', ) parser.add_argument( '--window', nargs=2, type=float, metavar=('BEGIN', 'END'), help='Only plot data between BEGIN and END times', ) parser.add_argument( '-X', '--option', nargs=2, action='append', default=[], metavar=('OPTION_NAME', 'VALUE'), help= 'Pass extra parameters to plot methods, e.g. "-X cpu 1". Mismatching names are ignored.', ) parser.add_argument( '--matplotlib-backend', default='GTK3Agg', help='matplotlib backend to use for interactive window', ) parser.add_argument( '--plat-info', help='Platform information, necessary for some plots', ) parser.add_argument( '--xkcd', action='store_true', help='Graphs will look like XKCD plots', ) args = parser.parse_args(argv) flat_plot_map = { plot_name: meth for analysis_name, plot_list in plots_map.items() for plot_name, meth in plot_list.items() } if args.plat_info: plat_info = PlatformInfo.from_yaml_map(args.plat_info) else: plat_info = None if args.plot_all: folder, fmt = args.plot_all plot_analysis_spec_list = [(get_analysis_nice_name(analysis_name), folder, fmt) for analysis_name in plots_map.keys()] else: plot_analysis_spec_list = [] plot_analysis_spec_list.extend(args.plot_analysis) plot_spec_list = [(plot_name, os.path.join(folder, '{}.{}'.format(plot_name, fmt))) for analysis_name, folder, fmt in plot_analysis_spec_list for plot_name, meth in plots_map[ analysis_nice_name_map[analysis_name]].items()] plot_spec_list.extend(args.plot) # Build minimal event list to speed up trace loading time plot_methods = set() for plot_name, file_path in plot_spec_list: try: f = flat_plot_map[plot_name] except KeyError: error('Unknown plot "{}", see --help'.format(plot_name)) plot_methods.add(f) # If best effort is used, we don't want to trigger exceptions ahead of # time. Let it fail for individual plot methods instead, so the trace can # be used for the other events if args.best_effort: events = None else: events = set() for f in plot_methods: with contextlib.suppress(AttributeError): events.update(f.used_events.get_all_events()) 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) if args.window: window = args.window def clip(l, x, r): if x < l: return l elif x > r: return r else: return x window = ( clip(trace.window[0], window[0], trace.window[1]), clip(trace.window[0], window[1], trace.window[1]), ) # There is no overlap between trace and user window, reset to trace # window if window[0] == window[1]: print( 'Window {} does not overlap with trace time range, maybe you forgot --normalize-time ?' .format(tuple(args.window))) window = trace.window trace = trace.get_view(window) for plot_name, file_path in sorted(plot_spec_list): interactive = file_path == 'interactive' f = flat_plot_map[plot_name] if interactive: matplotlib.use(args.matplotlib_backend) file_path = None else: dirname = os.path.dirname(file_path) if dirname: os.makedirs(dirname, exist_ok=True) kwargs = make_plot_kwargs(f, file_path, interactive=interactive, extra_options=args.option) xkcd_cm = plt.xkcd() if args.xkcd else nullcontext() with handle_plot_excep(exit_on_error=not args.best_effort): with xkcd_cm: TraceAnalysisBase.call_on_trace(f, trace, kwargs) if interactive: plt.show()
def wrapper(self, *args, filepath=None, axis=None, output=None, img_format=None, always_save=False, colors=None, **kwargs): # Bind the function to the instance, so we avoid having "self" # showing up in the signature, which breaks parameter # formatting code. f = func.__get__(self, type(self)) def is_f_param(param): """ Return True if the parameter is for `f`, False if it is for setup_plot() """ try: desc = inspect.signature(f).parameters[param] except KeyError: return False else: # Passing kwargs=42 to a function taking **kwargs # should not return True here, as we only consider # explicitely listed arguments return desc.kind not in ( inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL, ) f_kwargs = { param: val for param, val in kwargs.items() if is_f_param(param) } img_format = img_format or guess_format(filepath) or 'png' local_fig = axis is None # When we create the figure ourselves, always save the plot to # the default location if local_fig and filepath is None and always_save: filepath = self.get_default_plot_path( img_format=img_format, plot_name=f.__name__, ) if colors: cycler = make_cycler(color=colors) set_cycler = lambda axis: cls.set_axis_cycler(axis, cycler) else: set_cycler = lambda axis: nullcontext() # Allow returning an axis directly, or just update a given axis if return_axis: # In that case, the function takes all the kwargs with set_cycler(axis): axis = f(*args, **kwargs, axis=axis) else: if local_fig: setup_plot_kwargs = { param: val for param, val in kwargs.items() if param not in f_kwargs } fig, axis = self.setup_plot(**setup_plot_kwargs) with set_cycler(axis): f(*args, axis=axis, local_fig=local_fig, **f_kwargs) if isinstance(axis, numpy.ndarray): fig = axis[0].get_figure() else: fig = axis.get_figure() def resolve_formatter(fmt): format_map = { 'rst': cls._get_rst_content, 'html': cls._get_html, } try: return format_map[fmt] except KeyError: raise ValueError('Unsupported format: {}'.format(fmt)) if output is None: out = axis # Show the LISA figure toolbar if is_running_ipython(): # Make sure we only add one button per figure try: toolbar = self._get_fig_data(fig, 'toolbar') except KeyError: toolbar = self._make_fig_toolbar(fig) self._set_fig_data(fig, 'toolbar', toolbar) display(toolbar) mplcursors.cursor(fig) else: out = resolve_formatter(output)(f, args, f_kwargs, axis) if filepath: if img_format in ('html', 'rst'): content = resolve_formatter(img_format)(f, args, f_kwargs, axis) with open(filepath, 'wt', encoding='utf-8') as fd: fd.write(content) else: fig.savefig(filepath, format=img_format, bbox_inches='tight') return out