Source code for lisa.analysis.idle

# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2015, ARM Limited and contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from functools import reduce
import operator
import warnings
import typing

import pandas as pd
import holoviews as hv

from lisa.datautils import df_add_delta, df_refit_index, df_split_signals
from lisa.analysis.base import TraceAnalysisBase
from lisa.trace import requires_events, CPU
from lisa.analysis.base import TraceAnalysisBase


[docs] class IdleAnalysis(TraceAnalysisBase): """ Support for plotting Idle Analysis data :param trace: input Trace object :type trace: lisa.trace.Trace """ name = 'idle' ############################################################################### # DataFrame Getter Methods ###############################################################################
[docs] @TraceAnalysisBase.df_method @requires_events('cpu_idle') def df_cpus_idle(self, cpus=None): """ Dataframe of the ``cpu_idle`` event, with the following columns: * ``cpu`` * ``state``: Instead of 4294967295, the -1 type independent value is used. :param cpus: Optionally, filter on that list of CPUs :type cpus: list(int) or None """ df = self.trace.df_event('cpu_idle') # Filter before rename to avoid copying data we will ignore if cpus is not None: df = df[df['cpu_id'].isin(cpus)] df = df.rename({'cpu_id': 'cpu'}, axis=1) # The event uses an unsigned int even though the kernel uses -1, so use # -1 to avoid being tied to the event field type size non_idle = (2 ** 32) -1 df['state'] = df['state'].replace(non_idle, -1) return df
[docs] @TraceAnalysisBase.df_method @df_cpus_idle.used_events def df_cpu_idle(self, cpu=None): """ Same as :meth:`df_cpus_idle` but for one CPU. """ if cpu is None: warnings.warn('cpu=None is deprecated, use df_cpus_idle() to get a dataframe for all CPUs', DeprecationWarning) cpus = None else: cpus = [cpu] return self.df_cpus_idle(cpus=cpus)
[docs] @df_cpu_idle.used_events def signal_cpu_active(self, cpu): """ Build a square wave representing the active (i.e. non-idle) CPU time :param cpu: CPU ID :type cpu: int :returns: A :class:`pandas.Series` that equals 1 at timestamps where the CPU is reported to be non-idle, 0 otherwise """ cpu_df = self.df_cpu_idle(cpu) # Turn -1 into 1 and everything else into 0 cpu_active = cpu_df.state.map({-1: 1}) cpu_active.fillna(value=0, inplace=True) return cpu_active
[docs] @signal_cpu_active.used_events def signal_cluster_active(self, cluster): """ Build a square wave representing the active (i.e. non-idle) cluster time :param cluster: list of CPU IDs belonging to a cluster :type cluster: list(int) :returns: A :class:`pandas.Series` that equals 1 at timestamps where at least one CPU is reported to be non-idle, 0 otherwise """ active = self.signal_cpu_active(cluster[0]).to_frame(name=cluster[0]) for cpu in cluster[1:]: active = active.join( self.signal_cpu_active(cpu).to_frame(name=cpu), how='outer' ) active.ffill(inplace=True) # There might be NaNs in the signal where we got data from some CPUs # before others. That will break the .astype(int) below, so drop rows # with NaN in them. active.dropna(inplace=True) # Cluster active is the OR between the actives on each CPU # belonging to that specific cluster cluster_active = reduce( operator.or_, [cpu_active.astype(int) for _, cpu_active in active.items()] ) return cluster_active
[docs] @TraceAnalysisBase.df_method @signal_cpu_active.used_events def df_cpus_wakeups(self): """ Get a DataFrame showing when CPUs have woken from idle :param cpus: List of CPUs to find wakeups for. If None, all CPUs. :type cpus: list(int) or None :returns: A :class:`pandas.DataFrame` with * A ``cpu`` column (the CPU that woke up at the row index) """ cpus = list(range(self.trace.cpus_count)) def make_series(cpu): series = self.signal_cpu_active(cpu) series = series[series == 1] return series.replace(1, cpu) return pd.DataFrame({ 'cpu': pd.concat(map(make_series, cpus)) }).sort_index()
[docs] @df_cpu_idle.used_events def df_cpu_idle_state_residency(self, cpu): """ Compute time spent by a given CPU in each idle state. :param cpu: CPU ID :type cpu: int :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.df_cpu_idle(cpu) # Ensure accurate time-based sum of state deltas idle_df = df_refit_index(idle_df, window=self.trace.window) # For each state, sum the time spent in it idle_df = df_add_delta(idle_df) residency = { cols['state']: state_df['delta'].sum() for cols, state_df in df_split_signals(idle_df, ['state']) } df = pd.DataFrame.from_dict(residency, orient='index', columns=['time']) df.index.name = 'idle_state' return df
[docs] @df_cpu_idle.used_events def df_cluster_idle_state_residency(self, cluster): """ Compute time spent by a given cluster in each idle state. :param cluster: list of CPU IDs :type cluster: list(int) :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.df_cpus_idle() # Create a dataframe with a column per CPU cols = { cpu: group['state'] for cpu, group in idle_df.groupby( 'cpu', sort=False, observed=True, group_keys=False, ) if cpu in cluster } cpus_df = pd.DataFrame(cols, index=idle_df.index) cpus_df.ffill(inplace=True) # Ensure accurate time-based sum of state deltas. This will extrapolate # the known cluster_state both to the left and the right. cpus_df = df_refit_index(cpus_df, window=self.trace.window) # Each core in a cluster can be in a different idle state, but the # cluster lies in the idle state with lowest ID, that is the shallowest # idle state among the idle states of its CPUs cluster_state = cpus_df.min(axis='columns') cluster_state.name = 'cluster_state' df = cluster_state.to_frame() # For each state transition, sum the time spent in it df_add_delta(df, inplace=True) # For each cluster state, take the sum of the delta column. # The resulting dataframe is indexed by group keys (cluster_state). residency = df.groupby('cluster_state', sort=False, observed=True, group_keys=False)['delta'].sum() residency.name = 'time' residency = residency.to_frame() residency.index.name = 'idle_state' return residency
############################################################################### # Plotting Methods ###############################################################################
[docs] @TraceAnalysisBase.plot_method @df_cpu_idle_state_residency.used_events def plot_cpu_idle_state_residency(self, cpu: CPU, pct: bool=False): """ Plot the idle state residency of a CPU :param cpu: The CPU :type cpu: int :param pct: Plot residencies in percentage :type pct: bool """ df = self.df_cpu_idle_state_residency(cpu) return self._plot_idle_state_residency(df, pct=pct).options( title=f"CPU{cpu} idle state residency", )
[docs] @TraceAnalysisBase.plot_method @df_cluster_idle_state_residency.used_events def plot_cluster_idle_state_residency(self, cluster: typing.Sequence[CPU], pct: bool=False): """ Plot the idle state residency of a cluster :param cluster: The cluster :type cpu: list(int) :param pct: Plot residencies in percentage :type pct: bool """ df = self.df_cluster_idle_state_residency(cluster) return self._plot_idle_state_residency(df, pct=pct).options( title=f"CPUs {cluster} idle state residency", )
[docs] @TraceAnalysisBase.plot_method @plot_cluster_idle_state_residency.used_events def plot_clusters_idle_state_residency(self, pct: bool=False): """ Plot the idle state residency of all clusters :param pct: Plot residencies in percentage :type pct: bool .. note:: This assumes clusters == frequency domains, which may not hold true... """ return reduce( operator.add, ( self.plot_cluster_idle_state_residency(cluster, pct=pct) for cluster in self.trace.plat_info['freq-domains'] ) ).cols(1)
############################################################################### # Utility Methods ############################################################################### def _plot_idle_state_residency(self, df, pct): """ A convenient helper to plot idle state residency """ if pct: df = df * 100 / df.sum() ylabel = 'Time share (%)' if pct else 'Time (s)' return hv.Bars(df['time']).options( ylabel=ylabel, xlabel='Idle state', invert_axes=True, )
# vim :set tabstop=4 shiftwidth=4 expandtab textwidth=80