1#    Copyright 2015-2017 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15
16"""Small functions to help with plots"""
17
18# pylint disable=star-args
19
20from matplotlib import pyplot as plt
21import os
22import re
23
24from trappy.wa import SysfsExtractor
25
26GOLDEN_RATIO = 1.618034
27
28def normalize_title(title, opt_title):
29    """Return a string with that contains the title and opt_title if it's
30not the empty string
31
32    See test_normalize_title() for usage
33
34    """
35    if opt_title is not "":
36        title = opt_title + " - " + title
37
38    return title
39
40def set_lim(lim, get_lim_f, set_lim_f):
41    """Set x or y limitis of the plot
42
43    lim can be a tuple containing the limits or the string "default"
44    or "range".  "default" does nothing and uses matplotlib default.
45    "range" extends the current margin by 10%.  This is useful since
46    the default xlim and ylim of the plots sometimes make it harder to
47    see data that is just in the margin.
48
49    """
50    if lim == "default":
51        return
52
53    if lim == "range":
54        cur_lim = get_lim_f()
55        lim = (cur_lim[0] - 0.1 * (cur_lim[1] - cur_lim[0]),
56               cur_lim[1] + 0.1 * (cur_lim[1] - cur_lim[0]))
57
58    set_lim_f(lim[0], lim[1])
59
60def set_xlim(ax, xlim):
61    """Set the xlim of the plot
62
63    See set_lim() for the details
64    """
65    set_lim(xlim, ax.get_xlim, ax.set_xlim)
66
67def set_ylim(ax, ylim):
68    """Set the ylim of the plot
69
70    See set_lim() for the details
71    """
72    set_lim(ylim, ax.get_ylim, ax.set_ylim)
73
74def pre_plot_setup(width=None, height=None, ncols=1, nrows=1):
75    """initialize a figure
76
77    width and height are the height and width of each row of plots.
78    For 1x1 plots, that's the height and width of the plot.  This
79    function should be called before any calls to plot()
80
81    """
82
83    if height is None:
84        if width is None:
85            height = 6
86            width = 10
87        else:
88            height = width / GOLDEN_RATIO
89    else:
90        if width is None:
91            width = height * GOLDEN_RATIO
92
93    height *= nrows
94
95    _, axis = plt.subplots(ncols=ncols, nrows=nrows, figsize=(width, height))
96
97    # Needed for multirow blots to not overlap with each other
98    plt.tight_layout(h_pad=3.5)
99
100    return axis
101
102def post_plot_setup(ax, title="", xlabel=None, ylabel=None, xlim="default",
103                    ylim="range"):
104    """Set xlabel, ylabel title, xlim and ylim of the plot
105
106    This has to be called after calls to .plot().  The default ylim is
107    to extend it by 10% because matplotlib default makes it hard
108    values that are close to the margins
109
110    """
111
112    if xlabel is not None:
113        ax.set_xlabel(xlabel)
114
115    if ylabel is not None:
116        ax.set_ylabel(ylabel)
117
118    if title:
119        ax.set_title(title)
120
121    set_ylim(ax, ylim)
122    set_xlim(ax, xlim)
123
124def number_freq_plots(runs, map_label):
125    """Calculate the number of plots needed for allfreq plots and frequency
126    histogram plots
127
128    """
129    num_cpu_plots = len(map_label)
130
131    has_devfreq_data = False
132    for run in runs:
133        if len(run.devfreq_in_power.data_frame) > 0:
134            has_devfreq_data = True
135            break
136
137    num_freq_plots = num_cpu_plots
138    if has_devfreq_data:
139        num_freq_plots += 1
140
141    return num_freq_plots
142
143def plot_temperature(runs, width=None, height=None, ylim="range", tz_id=None):
144    """Plot temperatures
145
146    runs is an array of FTrace() instances.  Extract the control_temp
147    from the governor data and plot the temperatures reported by the
148    thermal framework.  The governor doesn't track temperature when
149    it's off, so the thermal framework trace is more reliable.
150
151    """
152
153    ax = pre_plot_setup(width, height)
154
155    for run in runs:
156        gov_dfr = run.thermal_governor.data_frame
157        if tz_id:
158            gov_dfr = gov_dfr[gov_dfr["thermal_zone_id"] == tz_id]
159
160        try:
161            current_temp = gov_dfr["current_temperature"]
162            delta_temp = gov_dfr["delta_temperature"]
163            control_series = (current_temp + delta_temp) / 1000
164        except KeyError:
165            control_series = None
166
167        try:
168            run.thermal.plot_temperature(control_temperature=control_series,
169                                         ax=ax, legend_label=run.name,
170                                         tz_id=tz_id)
171        except ValueError:
172            run.thermal_governor.plot_temperature(ax=ax, legend_label=run.name)
173
174    post_plot_setup(ax, title="Temperature", ylim=ylim)
175    plt.legend(loc="best")
176
177def plot_hist(data, ax, title, unit, bins, xlabel, xlim, ylim):
178    """Plot a histogram"""
179
180    mean = data.mean()
181    std = data.std()
182    title += " (mean = {:.2f}{}, std = {:.2f})".format(mean, unit, std)
183    xlabel += " ({})".format(unit)
184
185    data.hist(ax=ax, bins=bins)
186    post_plot_setup(ax, title=title, xlabel=xlabel, ylabel="count", xlim=xlim,
187                    ylim=ylim)
188
189def plot_load(runs, map_label, width=None, height=None):
190    """Make a multiplot of all the loads"""
191    num_runs = len(runs)
192    axis = pre_plot_setup(width=width, height=height, ncols=num_runs, nrows=2)
193
194    if num_runs == 1:
195        axis = [axis]
196    else:
197        axis = zip(*axis)
198
199    for ax, run in zip(axis, runs):
200        run.plot_load(map_label, title=run.name, ax=ax[0])
201        run.plot_normalized_load(map_label, title=run.name, ax=ax[1])
202
203def plot_allfreqs(runs, map_label, width=None, height=None):
204    """Make a multicolumn plots of the allfreqs plots of each run"""
205    num_runs = len(runs)
206    nrows = number_freq_plots(runs, map_label)
207
208    axis = pre_plot_setup(width=width, height=height, nrows=nrows,
209                          ncols=num_runs)
210
211    if num_runs == 1:
212        if nrows == 1:
213            axis = [[axis]]
214        else:
215            axis = [axis]
216    elif nrows == 1:
217        axis = [[ax] for ax in axis]
218    else:
219        axis = zip(*axis)
220
221    for ax, run in zip(axis, runs):
222        run.plot_allfreqs(map_label, ax=ax)
223
224def plot_controller(runs, width=None, height=None):
225    """Make a multicolumn plot of the pid controller of each run"""
226    num_runs = len(runs)
227    axis = pre_plot_setup(width=width, height=height, ncols=num_runs)
228
229    if num_runs == 1:
230        axis = [axis]
231
232    for ax, run in zip(axis, runs):
233        run.pid_controller.plot_controller(title=run.name, ax=ax)
234
235def plot_weighted_input_power(runs, actor_order, width=None, height=None):
236    """Make a multicolumn plot of the weighted input power of each run"""
237
238    actor_weights = []
239    for run in runs:
240        run_path = os.path.dirname(run.trace_path)
241        sysfs = SysfsExtractor(run_path)
242
243        thermal_params = sysfs.get_parameters()
244
245        sorted_weights = []
246        for param in sorted(thermal_params):
247            if re.match(r"cdev\d+_weight", param):
248                sorted_weights.append(thermal_params[param])
249
250        actor_weights.append(zip(actor_order, sorted_weights))
251
252    # Do nothing if we don't have actor weights for any run
253    if not any(actor_weights):
254        return
255
256    num_runs = len(runs)
257    axis = pre_plot_setup(width=width, height=height, ncols=num_runs)
258
259    if num_runs == 1:
260        axis = [axis]
261
262    for ax, run, weights in zip(axis, runs, actor_weights):
263        run.thermal_governor.plot_weighted_input_power(weights, title=run.name,
264                                                       ax=ax)
265
266def plot_input_power(runs, actor_order, width=None, height=None):
267    """Make a multicolumn plot of the input power of each run"""
268    num_runs = len(runs)
269    axis = pre_plot_setup(width=width, height=height, ncols=num_runs)
270
271    if num_runs == 1:
272        axis = [axis]
273
274    for ax, run in zip(axis, runs):
275        run.thermal_governor.plot_input_power(actor_order, title=run.name,
276                                              ax=ax)
277
278    plot_weighted_input_power(runs, actor_order, width, height)
279
280def plot_output_power(runs, actor_order, width=None, height=None):
281    """Make a multicolumn plot of the output power of each run"""
282    num_runs = len(runs)
283    axis = pre_plot_setup(width=width, height=height, ncols=num_runs)
284
285    if num_runs == 1:
286        axis = [axis]
287
288    for ax, run in zip(axis, runs):
289        run.thermal_governor.plot_output_power(actor_order, title=run.name,
290                                               ax=ax)
291
292def plot_freq_hists(runs, map_label):
293    """Plot frequency histograms of multiple runs"""
294    num_runs = len(runs)
295    nrows = 2 * number_freq_plots(runs, map_label)
296    axis = pre_plot_setup(ncols=num_runs, nrows=nrows)
297
298    if num_runs == 1:
299        axis = [axis]
300    else:
301        axis = zip(*axis)
302
303    for ax, run in zip(axis, runs):
304        run.plot_freq_hists(map_label, ax=ax)
305
306def plot_temperature_hist(runs):
307    """Plot temperature histograms for all the runs"""
308    num_runs = 0
309    for run in runs:
310        if len(run.thermal.data_frame):
311            num_runs += 1
312
313    if num_runs == 0:
314        return
315
316    axis = pre_plot_setup(ncols=num_runs)
317
318    if num_runs == 1:
319        axis = [axis]
320
321    for ax, run in zip(axis, runs):
322        run.thermal.plot_temperature_hist(ax, run.name)
323