ipynb/examples/Idle: Add extra idle state analysis

- Use cpuidle devlib module to poke CPUs to make sure cpu_idle events
  are present in the trace
- Set performance cpufreq governor to make sure there are idle periods
  during workload execution
- Pin workload to CPU 1
- Make workload period shorter and extent duty cycle range to extend
  over full idle state target residency range.
- Add an ILinePlot of the cpuidle state showing how the idle state used
  gets deeper as the workload duty cycle decreases in length.
- Add a histogram and scatter plot of idle period lengths
diff --git a/ipynb/examples/trace_analysis/TraceAnalysis_IdleStates.ipynb b/ipynb/examples/trace_analysis/TraceAnalysis_IdleStates.ipynb
index eaf5af3..3fd5f05 100644
--- a/ipynb/examples/trace_analysis/TraceAnalysis_IdleStates.ipynb
+++ b/ipynb/examples/trace_analysis/TraceAnalysis_IdleStates.ipynb
@@ -76,7 +76,12 @@
     "from trace import Trace\n",
     "\n",
     "# DataFrame support\n",
-    "from pandas import DataFrame"
+    "import pandas as pd\n",
+    "from pandas import DataFrame\n",
+    "\n",
+    "# Trappy (plots) support\n",
+    "from trappy import ILinePlot\n",
+    "from trappy.stats.grammar import Parser"
    ]
   },
   {
@@ -147,7 +152,7 @@
     "run_control": {
      "marked": false
     },
-    "scrolled": true
+    "scrolled": false
    },
    "outputs": [
     {
@@ -191,6 +196,21 @@
    ]
   },
   {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "# We're going to run quite a heavy workload to try and create short idle periods.\n",
+    "# Let's set the CPU frequency to max to make sure those idle periods exist\n",
+    "# (otherwise at a lower frequency the workload might overload the CPU\n",
+    "#  so it never went idle at all)\n",
+    "te.target.cpufreq.set_all_governors('performance')"
+   ]
+  },
+  {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -200,14 +220,25 @@
    ]
   },
   {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This experiment:\n",
+    "- Runs a periodic RT-App workload, pinned to CPU 1, that ramps down from 80% to 10% over 7.5 seconds\n",
+    "- Uses `perturb_cpus` to ensure 'cpu_idle' events are present in the trace for all CPUs\n",
+    "- Triggers and collects ftrace output"
+   ]
+  },
+  {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": null,
    "metadata": {
     "collapsed": false,
     "scrolled": true
    },
    "outputs": [],
    "source": [
+    "cpu = 1\n",
     "def experiment(te):\n",
     "\n",
     "    # Create RTApp RAMP task\n",
@@ -215,14 +246,18 @@
     "    rtapp.conf(kind='profile',\n",
     "               params={\n",
     "                    'ramp' : Ramp(\n",
-    "                        start_pct =  60,\n",
-    "                        end_pct   =  20,\n",
-    "                        delta_pct =   5,\n",
-    "                        time_s    =   0.5).get()\n",
+    "                        start_pct =  80,\n",
+    "                        end_pct   =  10,\n",
+    "                        delta_pct =  5,\n",
+    "                        time_s    =  0.5,\n",
+    "                        period_ms =  5,\n",
+    "                        cpus =       [cpu]).get()\n",
     "              })\n",
     "\n",
     "    # FTrace the execution of this workload\n",
     "    te.ftrace.start()\n",
+    "    # Momentarily wake all CPUs to ensure cpu_idle trace events are present from the beginning\n",
+    "    te.target.cpuidle.perturb_cpus()\n",
     "    rtapp.run(out_dir=te.res_dir)\n",
     "    te.ftrace.stop()\n",
     "\n",
@@ -590,6 +625,125 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "## CPU idle state over time"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Take a look at the target's idle states:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "te.target.cpuidle.get_states()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now use trappy to plot the idle state of a single CPU over time. Higher is deeper: the plot is at -1 when the CPU is active, 0 for WFI, 1 for CPU sleep, etc.\n",
+    "\n",
+    "We should see that as the workload ramps down and the idle periods become longer, the idle states used become deeper."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "p = Parser(trace.ftrace, filters = {'cpu_id': cpu})\n",
+    "idle_df = p.solve('cpu_idle:state')\n",
+    "ILinePlot(idle_df, column=cpu, drawstyle='steps-post').view()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Examine idle period lengths\n",
+    "Let's get a DataFrame showing the length of each idle period on the CPU and the index of the cpuidle state that was entered."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "def get_idle_periods(df):\n",
+    "    series = df[cpu]\n",
+    "    series = series[series.shift() != series].dropna()\n",
+    "    if series.iloc[0] == -1:\n",
+    "        series = series.iloc[1:]\n",
+    "\n",
+    "    idles = series.iloc[0::2] \n",
+    "    wakeups = series.iloc[1::2]\n",
+    "    if len(idles) > len(wakeups):\n",
+    "        idles = idles.iloc[:-1]\n",
+    "    else:\n",
+    "        wakeups = wakeups.iloc[:-1]\n",
+    "\n",
+    "    lengths = pd.Series((wakeups.index - idles.index), index=idles.index)\n",
+    "    return pd.DataFrame({\"length\": lengths, \"state\": idles})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Make a scatter plot of the length of idle periods against the state that was entered. We should see that for long idle periods, deeper states were entered (i.e. we should see a positive corellation between the X and Y axes)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "lengths = get_idle_periods(idle_df)\n",
+    "lengths.plot(kind='scatter', x='length', y='state')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "One more example: Draw a histogram of the length of idle periods shorter than 100ms in which the CPU entered cpuidle state 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "df = lengths[(lengths['state'] == 2) & (lengths['length'] < 0.010)]\n",
+    "df.hist(column='length', bins=50)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "## Per-cluster Idle State Residency"
    ]
   },