1 #!/usr/bin/python2.5 2 # 3 # Copyright 2009, The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 18 """plot_sdcard: A module to plot the results of an sdcard perf test. 19 20 Requires Gnuplot python v 1.8 21 22 Typical usage: 23 -t x axis is time 24 -i x axis is iteration 25 -p profile data generated by profile_sdcard.sh 26 27 ./plot_sdcard.py -t /tmp/data.txt 28 ./plot_sdcard.py -i /tmp/data.txt 29 ./plot_sdcard.py -p 30 31 python interpreter 32 >>> import plot_sdcard as p 33 >>> (metadata, data) = p.Parse('/tmp/data.txt') 34 >>> p.PlotIterations(metadata, data) 35 >>> p.PlotTimes(metadata, data) 36 37 """ 38 39 import getopt 40 from itertools import izip 41 import re 42 import sys 43 import Gnuplot 44 import numpy 45 46 47 class DataSet(object): 48 """Dataset holds the summary and data (time,value pairs).""" 49 50 def __init__(self, line): 51 res = re.search(('# StopWatch ([\w]+) total/cumulative ' 52 'duration ([0-9.]+). Samples: ([0-9]+)'), line) 53 self.time = [] 54 self.data = [] 55 self.name = res.group(1) 56 self.duration = float(res.group(2)) 57 self.iteration = int(res.group(3)) 58 self.summary = re.match('([a-z_]+)_total', self.name) 59 60 def __repr__(self): 61 return str(zip(self.time, self.data)) 62 63 def Add(self, time, value): 64 self.time.append(time) 65 self.data.append(value) 66 67 def RescaleTo(self, length): 68 factor = len(self.data) / length 69 70 if factor > 1: 71 new_time = [] 72 new_data = [] 73 accum = 0.0 74 idx = 1 75 for t, d in izip(self.time, self.data): 76 accum += d 77 if idx % factor == 0: 78 new_time.append(t) 79 new_data.append(accum / factor) 80 accum = 0 81 idx += 1 82 self.time = new_time 83 self.data = new_data 84 85 86 class Metadata(object): 87 def __init__(self): 88 self.kernel = '' 89 self.command_line = '' 90 self.sched = '' 91 self.name = '' 92 self.fadvise = '' 93 self.iterations = 0 94 self.duration = 0.0 95 self.complete = False 96 97 def Parse(self, line): 98 if line.startswith('# Kernel:'): 99 self.kernel = re.search('Linux version ([0-9.]+-[^ ]+)', line).group(1) 100 elif line.startswith('# Command:'): 101 self.command_line = re.search('# Command: [/\w_]+ (.*)', line).group(1) 102 self.command_line = self.command_line.replace(' --', '-') 103 self.command_line = self.command_line.replace(' -d', '') 104 self.command_line = self.command_line.replace('--test=', '') 105 elif line.startswith('# Iterations'): 106 self.iterations = int(re.search('# Iterations: ([0-9]+)', line).group(1)) 107 elif line.startswith('# Fadvise'): 108 self.fadvise = re.search('# Fadvise: ([\w]+)', line).group(1) 109 elif line.startswith('# Sched'): 110 self.sched = re.search('# Sched features: ([\w]+)', line).group(1) 111 self.complete = True 112 113 def AsTitle(self): 114 return '%s-duration:%f\\n-%s\\n%s' % ( 115 self.kernel, self.duration, self.command_line, self.sched) 116 117 def UpdateWith(self, dataset): 118 self.duration = max(self.duration, dataset.duration) 119 self.name = dataset.name 120 121 122 def Parse(filename): 123 """Parse a file with the collected data. 124 125 The data must be in 2 rows (x,y). 126 127 Args: 128 filename: Full path to the file. 129 """ 130 131 f = open(filename, 'r') 132 133 metadata = Metadata() 134 data = [] # array of dataset 135 dataset = None 136 137 for num, line in enumerate(f): 138 try: 139 line = line.strip() 140 if not line: continue 141 142 if not metadata.complete: 143 metadata.Parse(line) 144 continue 145 146 if re.match('[a-z_]', line): 147 continue 148 149 if line.startswith('# StopWatch'): # Start of a new dataset 150 if dataset: 151 if dataset.summary: 152 metadata.UpdateWith(dataset) 153 else: 154 data.append(dataset) 155 156 dataset = DataSet(line) 157 continue 158 159 if line.startswith('#'): 160 continue 161 162 # must be data at this stage 163 try: 164 (time, value) = line.split(None, 1) 165 except ValueError: 166 print 'skipping line %d: %s' % (num, line) 167 continue 168 169 if dataset and not dataset.summary: 170 dataset.Add(float(time), float(value)) 171 172 except Exception: 173 print 'Error parsing line %d' % num, sys.exc_info()[0] 174 raise 175 data.append(dataset) 176 if not metadata.complete: 177 print """Error missing metadata. Did you mount debugfs? 178 [adb shell mount -t debugfs none /sys/kernel/debug]""" 179 sys.exit(1) 180 return (metadata, data) 181 182 183 def PlotIterations(metadata, data): 184 """Plot the duration of the ops against iteration. 185 186 If you are plotting data with widely different runtimes you probably want to 187 use PlotTimes instead. 188 189 For instance when readers and writers are in the same mix, the 190 readers will go thru 100 iterations much faster than the 191 writers. The load test tries to be smart about that but the final 192 iterations of the writers will likely be done w/o any influence from 193 the readers. 194 195 Args: 196 metadata: For the graph's title. 197 data: pair of to be plotted. 198 """ 199 200 gp = Gnuplot.Gnuplot(persist=1) 201 gp('set data style lines') 202 gp.clear() 203 gp.xlabel('iterations') 204 gp.ylabel('duration in second') 205 gp.title(metadata.AsTitle()) 206 styles = {} 207 line_style = 1 208 209 for dataset in data: 210 dataset.RescaleTo(metadata.iterations) 211 x = numpy.arange(len(dataset.data), dtype='int_') 212 if not dataset.name in styles: 213 styles[dataset.name] = line_style 214 line_style += 1 215 d = Gnuplot.Data(x, dataset.data, 216 title=dataset.name, 217 with_='lines ls %d' % styles[dataset.name]) 218 else: # no need to repeat a title that exists already. 219 d = Gnuplot.Data(x, dataset.data, 220 with_='lines ls %d' % styles[dataset.name]) 221 222 gp.replot(d) 223 gp.hardcopy('/tmp/%s-%s-%f.png' % 224 (metadata.name, metadata.kernel, metadata.duration), 225 terminal='png') 226 227 228 def PlotTimes(metadata, data): 229 """Plot the duration of the ops against time elapsed. 230 231 Args: 232 metadata: For the graph's title. 233 data: pair of to be plotted. 234 """ 235 236 gp = Gnuplot.Gnuplot(persist=1) 237 gp('set data style impulses') 238 gp('set xtics 1') 239 gp.clear() 240 gp.xlabel('seconds') 241 gp.ylabel('duration in second') 242 gp.title(metadata.AsTitle()) 243 styles = {} 244 line_style = 1 245 246 for dataset in data: 247 x = numpy.array(dataset.time, dtype='float_') 248 if not dataset.name in styles: 249 styles[dataset.name] = line_style 250 line_style += 1 251 d = Gnuplot.Data(x, dataset.data, 252 title=dataset.name, 253 with_='impulses ls %d' % styles[dataset.name]) 254 else: # no need to repeat a title that exists already. 255 d = Gnuplot.Data(x, dataset.data, 256 with_='impulses ls %d' % styles[dataset.name]) 257 258 gp.replot(d) 259 gp.hardcopy('/tmp/%s-%s-%f.png' % 260 (metadata.name, metadata.kernel, metadata.duration), 261 terminal='png') 262 263 264 def PlotProfile(): 265 """Plot the time of a run against the number of processes.""" 266 (metadata, data) = Parse('/tmp/sdcard-scalability.txt') 267 gp = Gnuplot.Gnuplot(persist=1) 268 gp('set data style impulses') 269 gp('set xtics 1') 270 gp('set pointsize 2') 271 gp.clear() 272 gp.xlabel('writer process') 273 gp.ylabel('duration in second') 274 gp.title(metadata.AsTitle()) 275 276 dataset = data[0] 277 x = numpy.array(dataset.time, dtype='int_') 278 d = Gnuplot.Data(x, dataset.data, 279 title=dataset.name, 280 with_='linespoints') 281 gp.replot(d) 282 gp.hardcopy('/tmp/%s-%s-%f.png' % 283 (metadata.name, metadata.kernel, metadata.duration), 284 terminal='png') 285 286 287 def Usage(): 288 """Print this module's usage.""" 289 print """ 290 To plot the result using the iter number of the x axis: 291 292 plot_sdcard.py -i /tmp/data.txt 293 294 To plot the result using time for the x axis: 295 296 plot_sdcard.py -t /tmp/data.txt 297 298 To plot the result from the profiler: 299 300 profile_sdcard.sh 301 plot_sdcard.py -p 302 303 """ 304 sys.exit(2) 305 306 307 def main(argv): 308 try: 309 (optlist, args) = getopt.getopt(argv[1:], 310 'itp', ['iteration', 'time', 'profile']) 311 except getopt.GetoptError, err: 312 print str(err) 313 Usage() 314 315 for flag, val in optlist: 316 if flag in ('-i', '--iteration'): 317 (metadata, data) = Parse(args[0]) 318 PlotIterations(metadata, data) 319 sys.exit(0) 320 elif flag in ('-t', '--time'): 321 (metadata, data) = Parse(args[0]) 322 PlotTimes(metadata, data) 323 sys.exit(0) 324 elif flag in ('-p', '--profile'): 325 PlotProfile() 326 sys.exit(0) 327 Usage() 328 329 330 if __name__ == '__main__': 331 if Gnuplot.__version__ != "1.8": 332 print "Gnuplot should be 1.8. See REAME file" 333 sys.exit(2) 334 main(sys.argv) 335