1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4/** 5 * Icinga\Application\Benchmark class 6 */ 7namespace Icinga\Application; 8 9use Icinga\Util\Format; 10 11/** 12 * This class provides a simple and lightweight benchmark class 13 * 14 * <code> 15 * Benchmark::measure('Program started'); 16 * // ...do something... 17 * Benchmark::measure('Task finieshed'); 18 * Benchmark::dump(); 19 * </code> 20 */ 21class Benchmark 22{ 23 const TIME = 0x01; 24 const MEMORY = 0x02; 25 26 protected static $instance; 27 protected $start; 28 protected $measures = array(); 29 30 /** 31 * Add a measurement to your benchmark 32 * 33 * The same identifier can also be used multiple times 34 * 35 * @param string A comment identifying the current measurement 36 * @return void 37 */ 38 public static function measure($message) 39 { 40 self::getInstance()->measures[] = (object) array( 41 'timestamp' => microtime(true), 42 'memory_real' => memory_get_usage(true), 43 'memory' => memory_get_usage(), 44 'message' => $message 45 ); 46 } 47 48 /** 49 * Throws all measurements away 50 * 51 * This empties your measurement table and allows you to restart your 52 * benchmark from scratch 53 * 54 * @return void 55 */ 56 public static function reset() 57 { 58 self::$instance = null; 59 } 60 61 /** 62 * Rerieve benchmark start time 63 * 64 * This will give you the timestamp of your first measurement 65 * 66 * @return float 67 */ 68 public static function getStartTime() 69 { 70 return self::getInstance()->start; 71 } 72 73 /** 74 * Dump benchmark data 75 * 76 * Will dump a text table if running on CLI and a simple HTML table 77 * otherwise. Use Benchmark::TIME and Benchmark::MEMORY to choose whether 78 * you prefer to show either time or memory or both in your output 79 * 80 * @param int Whether to get time and/or memory summary 81 * @return string 82 */ 83 public static function dump($what = null) 84 { 85 if (Icinga::app()->isCli()) { 86 echo self::renderToText($what); 87 } else { 88 echo self::renderToHtml($what); 89 } 90 } 91 92 /** 93 * Render benchmark data to a simple text table 94 * 95 * Use Benchmark::TIME and Icinga::MEMORY to choose whether you prefer to 96 * show either time or memory or both in your output 97 * 98 * @param int Whether to get time and/or memory summary 99 * @return string 100 */ 101 public static function renderToText($what = null) 102 { 103 $data = self::prepareDataForRendering($what); 104 $sep = '+'; 105 $title = '|'; 106 foreach ($data->columns as & $col) { 107 $col->format = ' %' 108 . ($col->align === 'right' ? '' : '-') 109 . $col->maxlen . 's |'; 110 111 $sep .= str_repeat('-', $col->maxlen) . '--+'; 112 $title .= sprintf($col->format, $col->title); 113 } 114 115 $out = $sep . "\n" . $title . "\n" . $sep . "\n"; 116 foreach ($data->rows as & $row) { 117 $r = '|'; 118 foreach ($data->columns as $key => & $col) { 119 $r .= sprintf($col->format, $row[$key]); 120 } 121 $out .= $r . "\n"; 122 } 123 124 $out .= $sep . "\n"; 125 return $out; 126 } 127 128 /** 129 * Render benchmark data to a simple HTML table 130 * 131 * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer 132 * to show either time or memory or both in your output 133 * 134 * @param int Whether to get time and/or memory summary 135 * @return string 136 */ 137 public static function renderToHtml($what = null) 138 { 139 $data = self::prepareDataForRendering($what); 140 141 // TODO: Move formatting to CSS file 142 $html = '<table class="benchmark">' . "\n" . '<tr>'; 143 foreach ($data->columns as & $col) { 144 if ($col->title === 'Time') { 145 continue; 146 } 147 $html .= sprintf( 148 '<td align="%s">%s</td>', 149 $col->align, 150 htmlspecialchars($col->title) 151 ); 152 } 153 $html .= "</tr>\n"; 154 155 foreach ($data->rows as & $row) { 156 $html .= '<tr>'; 157 foreach ($data->columns as $key => & $col) { 158 if ($col->title === 'Time') { 159 continue; 160 } 161 $html .= sprintf( 162 '<td align="%s">%s</td>', 163 $col->align, 164 $row[$key] 165 ); 166 } 167 $html .= "</tr>\n"; 168 } 169 $html .= "</table>\n"; 170 return $html; 171 } 172 173 /** 174 * Prepares benchmark data for output 175 * 176 * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer 177 * to have either time or memory or both in your output 178 * 179 * @param int Whether to get time and/or memory summary 180 * @return array 181 */ 182 protected static function prepareDataForRendering($what = null) 183 { 184 if ($what === null) { 185 $what = self::TIME | self::MEMORY; 186 } 187 188 $columns = array( 189 (object) array( 190 'title' => 'Time', 191 'align' => 'left', 192 'maxlen' => 4 193 ), 194 (object) array( 195 'title' => 'Description', 196 'align' => 'left', 197 'maxlen' => 11 198 ) 199 ); 200 if ($what & self::TIME) { 201 $columns[] = (object) array( 202 'title' => 'Off (ms)', 203 'align' => 'right', 204 'maxlen' => 11 205 ); 206 $columns[] = (object) array( 207 'title' => 'Dur (ms)', 208 'align' => 'right', 209 'maxlen' => 13 210 ); 211 } 212 if ($what & self::MEMORY) { 213 $columns[] = (object) array( 214 'title' => 'Mem (diff)', 215 'align' => 'right', 216 'maxlen' => 10 217 ); 218 $columns[] = (object) array( 219 'title' => 'Mem (total)', 220 'align' => 'right', 221 'maxlen' => 11 222 ); 223 } 224 225 $bench = self::getInstance(); 226 $last = $bench->start; 227 $rows = array(); 228 $lastmem = 0; 229 foreach ($bench->measures as $m) { 230 $micro = sprintf( 231 '%03d', 232 round(($m->timestamp - floor($m->timestamp)) * 1000) 233 ); 234 $vals = array( 235 date('H:i:s', $m->timestamp) . '.' . $micro, 236 $m->message 237 ); 238 239 if ($what & self::TIME) { 240 $m->relative = $m->timestamp - $bench->start; 241 $m->offset = $m->timestamp - $last; 242 $last = $m->timestamp; 243 $vals[] = sprintf('%0.3f', $m->relative * 1000); 244 $vals[] = sprintf('%0.3f', $m->offset * 1000); 245 } 246 247 if ($what & self::MEMORY) { 248 $mem = $m->memory - $lastmem; 249 $lastmem = $m->memory; 250 $vals[] = Format::bytes($mem); 251 $vals[] = Format::bytes($m->memory); 252 } 253 254 $row = & $rows[]; 255 foreach ($vals as $col => $val) { 256 $row[$col] = $val; 257 $columns[$col]->maxlen = max( 258 strlen($val), 259 $columns[$col]->maxlen 260 ); 261 } 262 } 263 264 return (object) array( 265 'columns' => $columns, 266 'rows' => $rows 267 ); 268 } 269 270 /** 271 * Singleton 272 * 273 * Benchmark is run only once, but you are not allowed to directly access 274 * the getInstance() method 275 * 276 * @return self 277 */ 278 protected static function getInstance() 279 { 280 if (self::$instance === null) { 281 self::$instance = new Benchmark(); 282 self::$instance->start = microtime(true); 283 } 284 285 return self::$instance; 286 } 287 288 /** 289 * Constructor 290 * 291 * Singleton usage is enforced, the only way to instantiate Benchmark is by 292 * starting your measurements 293 * 294 * @return void 295 */ 296 protected function __construct() 297 { 298 } 299} 300