# 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.
#
""" Idle Analysis Module """
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import pandas as pd
import pylab as pl
from analysis_module import AnalysisModule
from trace import ResidencyTime, ResidencyData
from trappy.utils import listify
class IdleAnalysis(AnalysisModule):
"""
Support for plotting Idle Analysis data
:param trace: input Trace object
:type trace: :mod:`libs.utils.Trace`
"""
def __init__(self, trace):
super(IdleAnalysis, self).__init__(trace)
###############################################################################
# DataFrame Getter Methods
###############################################################################
def _dfg_cpu_idle_state_residency(self, cpu):
"""
Compute time spent by a given CPU in each idle state.
:param entity: CPU ID
:type entity: int
:returns: :mod:`pandas.DataFrame` - idle state residency dataframe
"""
if not self._trace.hasEvents('cpu_idle'):
self._log.warning('Events [cpu_idle] not found, '
'idle state residency computation not possible!')
return None
idle_df = self._dfg_trace_event('cpu_idle')
cpu_idle = idle_df[idle_df.cpu_id == cpu]
cpu_is_idle = self._trace.getCPUActiveSignal(cpu) ^ 1
# In order to compute the time spent in each idle state we
# multiply 2 square waves:
# - cpu_idle
# - idle_state, square wave of the form:
# idle_state[t] == 1 if at time t CPU is in idle state i
# idle_state[t] == 0 otherwise
available_idles = sorted(idle_df.state.unique())
# Remove non-idle state from availables
available_idles = available_idles[1:]
cpu_idle = cpu_idle.join(cpu_is_idle.to_frame(name='is_idle'),
how='outer')
cpu_idle.fillna(method='ffill', inplace=True)
# Extend the last cpu_idle event to the end of the time window under
# consideration
final_entry = pd.DataFrame([cpu_idle.iloc[-1]], index=[self._trace.x_max])
cpu_idle = cpu_idle.append(final_entry)
idle_time = []
for i in available_idles:
idle_state = cpu_idle.state.apply(
lambda x: 1 if x == i else 0
)
idle_t = cpu_idle.is_idle * idle_state
# Compute total time by integrating the square wave
idle_time.append(self._trace.integrate_square_wave(idle_t))
idle_time_df = pd.DataFrame({'time' : idle_time}, index=available_idles)
idle_time_df.index.name = 'idle_state'
return idle_time_df
def _dfg_cluster_idle_state_residency(self, cluster):
"""
Compute time spent by a given cluster in each idle state.
:param cluster: cluster name or list of CPU IDs
:type cluster: str or list(int)
:returns: :mod:`pandas.DataFrame` - idle state residency dataframe
"""
if not self._trace.hasEvents('cpu_idle'):
self._log.warning('Events [cpu_idle] not found, '
'idle state residency computation not possible!')
return None
_cluster = cluster
if isinstance(cluster, str) or isinstance(cluster, unicode):
try:
_cluster = self._platform['clusters'][cluster.lower()]
except KeyError:
self._log.warning('%s cluster not found!', cluster)
return None
idle_df = self._dfg_trace_event('cpu_idle')
# 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
cl_idle = idle_df[idle_df.cpu_id == _cluster[0]].state.to_frame(
name=_cluster[0])
for cpu in _cluster[1:]:
cl_idle = cl_idle.join(
idle_df[idle_df.cpu_id == cpu].state.to_frame(name=cpu),
how='outer'
)
cl_idle.fillna(method='ffill', inplace=True)
cl_idle = pd.DataFrame(cl_idle.min(axis=1), columns=['state'])
# Build a square wave of the form:
# cl_is_idle[t] == 1 if all CPUs in the cluster are reported
# to be idle by cpufreq at time t
# cl_is_idle[t] == 0 otherwise
cl_is_idle = self._trace.getClusterActiveSignal(_cluster) ^ 1
# In order to compute the time spent in each idle statefrequency we
# multiply 2 square waves:
# - cluster_is_idle
# - idle_state, square wave of the form:
# idle_state[t] == 1 if at time t cluster is in idle state i
# idle_state[t] == 0 otherwise
available_idles = sorted(idle_df.state.unique())
# Remove non-idle state from availables
available_idles = available_idles[1:]
cl_idle = cl_idle.join(cl_is_idle.to_frame(name='is_idle'),
how='outer')
cl_idle.fillna(method='ffill', inplace=True)
idle_time = []
for i in available_idles:
idle_state = cl_idle.state.apply(
lambda x: 1 if x == i else 0
)
idle_t = cl_idle.is_idle * idle_state
# Compute total time by integrating the square wave
idle_time.append(self._trace.integrate_square_wave(idle_t))
idle_time_df = pd.DataFrame({'time' : idle_time}, index=available_idles)
idle_time_df.index.name = 'idle_state'
return idle_time_df
###############################################################################
# Plotting Methods
###############################################################################
def plotCPUIdleStateResidency(self, cpus=None, pct=False):
"""
Plot per-CPU idle state residency. big CPUs are plotted first and then
LITTLEs.
Requires cpu_idle trace events.
:param cpus: list of CPU IDs. By default plot all CPUs
:type cpus: list(int) or int
:param pct: plot residencies in percentage
:type pct: bool
"""
if not self._trace.hasEvents('cpu_idle'):
self._log.warning('Events [cpu_idle] not found, '
'plot DISABLED!')
return
if cpus is None:
# Generate plots only for available CPUs
cpuidle_data = self._dfg_trace_event('cpu_idle')
_cpus = range(cpuidle_data.cpu_id.max() + 1)
else:
_cpus = listify(cpus)
# Split between big and LITTLE CPUs ordered from higher to lower ID
_cpus.reverse()
big_cpus = [c for c in _cpus if c in self._big_cpus]
little_cpus = [c for c in _cpus if c in self._little_cpus]
_cpus = big_cpus + little_cpus
residencies = []
xmax = 0.0
for cpu in _cpus:
r = self._dfg_cpu_idle_state_residency(cpu)
residencies.append(ResidencyData('CPU{}'.format(cpu), r))
max_time = r.max().values[0]
if xmax < max_time:
xmax = max_time
self._plotIdleStateResidency(residencies, 'cpu', xmax, pct=pct)
def plotClusterIdleStateResidency(self, clusters=None, pct=False):
"""
Plot per-cluster idle state residency in a given cluster, i.e. the
amount of time cluster `cluster` spent in idle state `i`. By default,
both 'big' and 'LITTLE' clusters data are plotted.
Requires cpu_idle following trace events.
:param clusters: name of the clusters to be plotted (all of them by
default)
:type clusters: str ot list(str)
"""
if not self._trace.hasEvents('cpu_idle'):
self._log.warning('Events [cpu_idle] not found, plot DISABLED!')
return
if 'clusters' not in self._platform:
self._log.warning('No platform cluster info. Plot DISABLED!')
return
# Sanitize clusters
if clusters is None:
_clusters = self._platform['clusters'].keys()
else:
_clusters = listify(clusters)
# Precompute residencies for each cluster
residencies = []
xmax = 0.0
for c in _clusters:
r = self._dfg_cluster_idle_state_residency(c.lower())
residencies.append(ResidencyData('{} Cluster'.format(c), r))
max_time = r.max().values[0]
if xmax < max_time:
xmax = max_time
self._plotIdleStateResidency(residencies, 'cluster', xmax, pct=pct)
###############################################################################
# Utility Methods
###############################################################################
def _plotIdleStateResidency(self, residencies, entity_name, xmax,
pct=False):
"""
Generate Idle state residency plots for the given entities.
:param residencies: list of residencies to be plot
:type residencies: list(namedtuple(ResidencyData)) - each tuple
contains:
- a label to be used as subplot title
- a dataframe with residency for each idle state
:param entity_name: name of the entity ('cpu' or 'cluster') used in the
figure name
:type entity_name: str
:param xmax: upper bound of x-axes
:type xmax: double
:param pct: plot residencies in percentage
:type pct: bool
"""
n_plots = len(residencies)
gs = gridspec.GridSpec(n_plots, 1)
fig = plt.figure()
for idx, data in enumerate(residencies):
r = data.residency
if r is None:
plt.close(fig)
return
axes = fig.add_subplot(gs[idx])
is_first = idx == 0
is_last = idx+1 == n_plots
yrange = 0.4 * max(6, len(r)) * n_plots
if pct:
duration = r.time.sum()
r_pct = r.apply(lambda x: x*100/duration)
r_pct.columns = [data.label]
r_pct.T.plot.barh(ax=axes, stacked=True, figsize=(16, yrange))
axes.legend(loc='lower center', ncol=7)
axes.set_xlim(0, 100)
else:
r.plot.barh(ax=axes, color='g',
legend=False, figsize=(16, yrange))
axes.set_xlim(0, 1.05*xmax)
axes.set_ylabel('Idle State')
axes.set_title(data.label)
axes.grid(True)
if is_last:
if pct:
axes.set_xlabel('Residency [%]')
else:
axes.set_xlabel('Time [s]')
else:
axes.set_xticklabels([])
if is_first:
legend_y = axes.get_ylim()[1]
axes.annotate('Idle State Residency Time', xy=(0, legend_y),
xytext=(-50, 45), textcoords='offset points',
fontsize=18)
figname = '{}/{}{}_idle_state_residency.png'\
.format(self._trace.plots_dir,
self._trace.plots_prefix,
entity_name)
pl.savefig(figname, bbox_inches='tight')
# vim :set tabstop=4 shiftwidth=4 expandtab