├── HOWTO.md ├── README.md ├── XDebugTrace.php ├── XDebugTraceExtension.php ├── content.latte ├── error.latte └── screenshot.png /HOWTO.md: -------------------------------------------------------------------------------- 1 | How to use XDebugTrace panel for Nette 2 | ====================================== 3 | 4 | **Source code:** http://github.com/milo/XDebugTracePanel 5 | 6 | 1. INSTALLATION 7 | --------------- 8 | **Automatic:** Use [Composer](http://getcomposer.org/) 9 | ```sh 10 | # For Nette 2.0.x 11 | $ composer require milo/xdebug-trace-panel "2.0.*@dev" 12 | 13 | # For Nette 2.1.x and newer 14 | $ composer require milo/xdebug-trace-panel@dev 15 | ``` 16 | 17 | **Manual:** Copy files into directory, where `RobotLoader` have access, e.g. 18 | ``` 19 | libs/Panels/XDebugTrace/XDebugTrace.php 20 | libs/Panels/XDebugTrace/XDebugTraceExtension.php 21 | libs/Panels/XDebugTrace/content.latte 22 | libs/Panels/XDebugTrace/error.latte 23 | ``` 24 | 25 | 26 | 27 | 2. REGISTER PANEL 28 | ----------------- 29 | a) The shortest way for **Nette 2.1** (add as extension in config.neon): 30 | ```yml 31 | extensions: 32 | xtrace: Panel\XDebugTraceExtension 33 | 34 | # Optionally 35 | xtrace: 36 | traceFile: '%tempDir%/trace.xt' 37 | onCreate: Helpers::setupXTracePanel # Called when service is created 38 | statistics: TRUE # Perform time statistics 39 | 40 | # or 41 | statistics: [TRUE, deltaTime] # and sort them by deltaTime 42 | ``` 43 | 44 | b) Still short way for **Nette 2.0.x** which works for **2.1.x** too (add as extension). In `bootstrap.php`: 45 | ```php 46 | $configurator->onCompile[] = function($configurator, $compiler) { 47 | $compiler->addExtension('xtrace', new Panel\XDebugTraceExtension); 48 | }; 49 | ``` 50 | 51 | and optionally adjust configuration in config.neon. 52 | ```yml 53 | xtrace: 54 | traceFile: '%tempDir%/trace.xt' 55 | onCreate: Helpers::setupXTracePanel # Called when service is created 56 | statistics: TRUE # Perform time statistics 57 | 58 | # or 59 | statistics: [TRUE, deltaTime] # and sort them by deltaTime 60 | ``` 61 | 62 | c) Long way (manual installation), works always. Register panel in `bootstrap.php`. Provide path to temporary trace file in XDebugTrace constructor. 63 | ```php 64 | $xtrace = new Panel\XDebugTrace(__DIR__ . '/../temp/trace.xt'); 65 | Nette\Diagnostics\Debugger::addPanel($xtrace); 66 | 67 | # Optionally 68 | $xtrace->enableStatistics(TRUE, 'deltaTime'); 69 | ``` 70 | 71 | 72 | 73 | 3. START-PAUSE-STOP TRACING 74 | --------------------------- 75 | Now, when panel is registered, you can start tracing. 76 | ```php 77 | $xtrace->start(); 78 | $router = $container->router; 79 | $router[] = new Route('index.php', 'Homepage:default', Route::ONE_WAY); 80 | $router[] = new Route('/[/]', 'Homepage:default'); 81 | $xtrace->pause(); 82 | 83 | $xtrace->start('Application run'); 84 | $application->run(); 85 | $xtrace->stop(); 86 | ``` 87 | 88 | If you use Nette extension, you find the XDebugTrace object as system container service. In presenters: 89 | ```php 90 | public function createComponentForm() 91 | { 92 | $this->context->xtrace->start(__METHOD__); 93 | ... 94 | ... 95 | ... 96 | $this->context->xtrace->stop(); 97 | } 98 | ``` 99 | 100 | Because of `xdebug_trace_start()` can runs only once, the only one instance of XDebugTrace class can exists. And you can call all methods statically as `XDebugTrace::callMethodName()`, e.g.: 101 | ```php 102 | Panel\XDebugTrace::callStart('Application run'); 103 | Panel\XDebugTrace::callPause(); 104 | Panel\XDebugTrace::callStop(); 105 | ``` 106 | 107 | 108 | 109 | 5. TRACE RECORDS FILTERING 110 | -------------------------- 111 | Filtering is the most ambitious work with this panel. Without this, HTML output can be huge (megabytes). Panel provides mechanism for records filtering. You can use prepared filters (methods starts by `trace` prefix). 112 | ```php 113 | # Trace everything. Be careful, HTML output can be huge! 114 | $xtrace->traceAll(); 115 | 116 | 117 | # Trace single function... 118 | $xtrace->traceFunction('weirdFunction'); 119 | # ... and all inside calls too... 120 | $xtrace->traceFunction('weirdFunction', TRUE); 121 | # ... and PHP internals too 122 | $xtrace->traceFunction('weirdFunction', TRUE, TRUE); 123 | 124 | 125 | # Trace static method... 126 | $xtrace->traceFunction('MyClass::weirdFunction'); 127 | # ... or dynamic... 128 | $xtrace->traceFunction('MyClass->weirdFunction'); 129 | # ... or both 130 | $xtrace->traceFunction(array('MyClass', 'weirdFunction')); 131 | 132 | 133 | # Trace functions by PCRE regular expression 134 | $xtrace->traceFunctionRe('/^weird/i'); 135 | 136 | 137 | # Trace only functions running over the 15 miliseconds... 138 | $xtrace->traceDeltaTime('15ms'); 139 | # ... or function which consumes more then 20kB 140 | $xtrace->traceDeltaMemory('20kB'); 141 | ``` 142 | 143 | If you want use own filters, at first, take a look on `XDebugTrace::defaultFilterCb()` source code. This is a default filtering callback. It is used when you don't register own one. And take a look on `XDebugTrace::trace.....()` methods source code. 144 | 145 | At second, is good to know xdebug trace file structure: 146 | ``` 147 | # Entry record 148 | level id 0 time memory functionName ... 149 | 150 | # Exit record 151 | level id 1 time memory 152 | 153 | # An example 154 | 3 121 0 0.012 401442 myFunction - myFunction() enter 155 | 4 122 0 0.014 401442 strpos - strpos() enter 156 | 4 122 1 0.015 401454 - strpos() exit 157 | 4 123 0 0.016 401454 substr - substr() enter 158 | 4 123 1 0.018 401505 - substr() exit 159 | 3 121 1 0.020 401442 - myFunction() exit 160 | ``` 161 | Detailed on http://xdebug.org/docs/execution_trace 162 | 163 | Use these functions for set-up own filters: 164 | ```php 165 | $xtrace->addFilterCallback($callback, $flags); 166 | $xtrace->setFilterCallback($callback, $flags); 167 | ``` 168 | 169 | `$flags` is a bitmask of `Panel\XDebugTrace::FILTER_*` constants. 170 | ``` 171 | FILTER_ENTRY - call filter on entry records (default) 172 | FILTER_EXIT - call filter on exit records 173 | FILTER_BOTH = FILTER_ENTRY | FILTER_EXIT 174 | 175 | FILTER_APPEND_ENTRY - append filter behind others (default is prepend) 176 | FILTER_APPEND_EXIT - append filter behind others (default is prepend) 177 | FILTER_APPEND = FILTER_APPEND_ENTRY | FILTER_APPEND_EXIT 178 | 179 | FILTER_REPLACE_ENTRY - remove all entry filters 180 | FILTER_REPLACE_EXIT - remove all exit filters 181 | FILTER_REPLACE = FILTER_REPLACE_ENTRY | FILTER_REPLACE_EXIT 182 | ``` 183 | 184 | ``` 185 | Your callback should return bitmask of flags: 186 | Panel\XDebugTrace::SKIP - skip this record, don't print it in the panel 187 | Panel\XDebugTrace::STOP - don't call remain filters 188 | 189 | or return NULL. NULL means record passed and will be printed in bar. 190 | ``` 191 | 192 | Simple example follows: 193 | ```php 194 | # Display everything except for internal functions 195 | $xtrace->setFilterCallback(function($record) { 196 | return $record->isInternal ? Panel\XDebugTrace::SKIP : NULL; 197 | }); 198 | ``` 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | !WARNING! 2 | ========= 3 | This addon is abandoned. You can use it, It still works with nette 2.2.x but I'm writing new addon with completely new API. 4 | 5 | XDebugTrace Panel 6 | ================= 7 | Debugger panel for [Nette Framework](http://nette.org) 8 | 9 | **Author:** Miloslav Hůla 10 | **Licence:** [LGPL Licence](http://www.gnu.org/licenses/lgpl.html) 11 | 12 | 13 | Installation & Usage 14 | -------------------- 15 | Panel works with Nette 2.0.x, 2.1.x and I'm trying to keep it works with Nette dev. If you find some incompatibility, please, open an [issue](https://github.com/milo/XDebugTracePanel/issues). 16 | Take a look in [HOWTO](https://github.com/milo/XDebugTracePanel/blob/master/HOWTO.md). 17 | 18 | 19 | Screenshot 20 | ---------- 21 | ![Panel screenshot](https://github.com/milo/XDebugTracePanel/raw/master/screenshot.png) 22 | -------------------------------------------------------------------------------- /XDebugTrace.php: -------------------------------------------------------------------------------- 1 | 15 | * @see http://github.com/milo/XDebugTracePanel 16 | * @licence LGPL 17 | */ 18 | class XDebugTrace extends Nette\Object implements Nette\Diagnostics\IBarPanel 19 | { 20 | /** Tracing states */ 21 | const 22 | STATE_STOP = 0, 23 | STATE_RUN = 1, 24 | STATE_PAUSE = 2; 25 | 26 | /** Filter callback action bitmask */ 27 | const 28 | STOP = 0x01, 29 | SKIP = 0x02; 30 | 31 | /** Adding filter bitmask flags */ 32 | const 33 | FILTER_ENTRY = 1, 34 | FILTER_EXIT = 2, 35 | FILTER_BOTH = 3, 36 | FILTER_APPEND_ENTRY = 4, 37 | FILTER_APPEND_EXIT = 8, 38 | FILTER_APPEND = 12, 39 | FILTER_REPLACE_ENTRY = 16, 40 | FILTER_REPLACE_EXIT = 32, 41 | FILTER_REPLACE = 48; 42 | 43 | /** @internal */ 44 | const 45 | WRITE_OK = 'write-ok'; 46 | 47 | /** 48 | * @var int maximal length of line in trace file 49 | */ 50 | public static $traceLineLength = 4096; 51 | 52 | /** 53 | * @var bool delete trace file in destructor or not 54 | */ 55 | public $deleteTraceFile = FALSE; 56 | 57 | /** 58 | * @var \Panel\XDebugTrace 59 | */ 60 | private static $instance; 61 | 62 | /** 63 | * @var bool perform function time statistics 64 | */ 65 | protected $performStatistics = FALSE; 66 | 67 | /** 68 | * @var string by which column sort the statistics 69 | */ 70 | protected $sortBy = 'averageTime'; 71 | 72 | /** 73 | * @var int tracing state 74 | */ 75 | private $state = self::STATE_STOP; 76 | 77 | /** 78 | * @var string path to trace file 79 | */ 80 | private $traceFile; 81 | 82 | /** 83 | * @var stdClass[] 84 | */ 85 | protected $traces = array(); 86 | 87 | /** 88 | * @var reference to $this->traces 89 | */ 90 | protected $trace; 91 | 92 | /** 93 | * @var string[] trace titles 94 | */ 95 | protected $titles = array(); 96 | 97 | /** 98 | * @var array[level => indent size] 99 | */ 100 | protected $indents = array(); 101 | 102 | /** 103 | * @var reference to $this->indents 104 | */ 105 | protected $indent; 106 | 107 | /** 108 | * @var array[function => stdClass] 109 | */ 110 | protected $statistics = array(); 111 | 112 | /** 113 | * @var reference to $this->statistics 114 | */ 115 | protected $statistic; 116 | 117 | /** 118 | * @var bool internal error occured, error template will be rendered 119 | */ 120 | protected $isError = FALSE; 121 | 122 | /** 123 | * @var string 124 | */ 125 | protected $errMessage = ''; 126 | 127 | /** 128 | * @var string 129 | */ 130 | protected $errFile; 131 | 132 | /** 133 | * @var int 134 | */ 135 | protected $errLine; 136 | 137 | /** 138 | * @var callback[] called when entry record from trace file is parsed 139 | */ 140 | protected $filterEntryCallbacks = array(); 141 | 142 | /** 143 | * @var callback[] called when exit record from trace file is parsed 144 | */ 145 | protected $filterExitCallbacks = array(); 146 | 147 | /** 148 | * @var array[setting => bool] default filter setting 149 | */ 150 | protected $skipOver = array( 151 | 'phpInternals' => TRUE, 152 | 'XDebugTrace' => TRUE, 153 | 'Nette' => TRUE, 154 | 'Composer' => TRUE, 155 | 'callbacks' => TRUE, 156 | 'includes' => TRUE, 157 | ); 158 | 159 | /** 160 | * @var \Nette\Templating\FileTemplate 161 | */ 162 | protected $lazyTemplate; 163 | 164 | /** 165 | * @var \Nette\Templating\FileTemplate 166 | */ 167 | protected $lazyErrorTemplate; 168 | 169 | 170 | 171 | /** 172 | * @param string path to trace file, extension .xt is optional 173 | * @throws \Nette\InvalidStateException 174 | */ 175 | public function __construct($traceFile) 176 | { 177 | if (self::$instance !== NULL) { 178 | throw new \Nette\InvalidStateException('Class ' . get_class($this) . ' can be instantized only once, xdebug_start_trace() can runs only once.'); 179 | } 180 | self::$instance = $this; 181 | 182 | if (substr_compare($traceFile, '.xt', -3, 3, TRUE) === 0) { 183 | $traceFile = substr($traceFile, 0, -3); 184 | } 185 | 186 | if (!extension_loaded('xdebug')) { 187 | $this->setError('XDebug extension is not loaded'); 188 | 189 | } elseif (@file_put_contents($traceFile . '.xt', self::WRITE_OK) === FALSE) { 190 | $this->setError("Cannot create trace file '$traceFile.xt'", error_get_last()); 191 | 192 | } else { 193 | $this->traceFile = $traceFile; 194 | } 195 | 196 | $this->addFilterCallback(array($this, 'defaultFilterCb')); 197 | } 198 | 199 | 200 | 201 | public function __destruct() 202 | { 203 | if ($this->deleteTraceFile && is_file($this->traceFile . '.xt')) { 204 | @unlink($this->traceFile . '.xt'); 205 | } 206 | } 207 | 208 | 209 | 210 | /** 211 | * Shortcut for \Panel\XDebugTrace::getInstance()->method() 212 | * as \Panel\XDebugTrace::callMethod(); 213 | */ 214 | public static function __callStatic($name, $args) 215 | { 216 | $instance = self::getInstance(); 217 | 218 | if (preg_match('/^call([A-Z].*)/', $name, $match)) { 219 | $method = lcfirst($match[1]); 220 | if (method_exists($instance, $method)) { 221 | return call_user_func_array(array($instance, $method), $args); 222 | } 223 | } 224 | 225 | parent::__callStatic($name, $args); 226 | } 227 | 228 | 229 | 230 | /** 231 | * Access to class instance. 232 | * 233 | * @return \Panel\XDebugTrace 234 | * @throws \Nette\InvalidStateException 235 | */ 236 | public static function getInstance() 237 | { 238 | if (self::$instance === NULL) { 239 | throw new \Nette\InvalidStateException(get_called_class() . ' has not been instantized yet.'); 240 | } 241 | 242 | return self::$instance; 243 | } 244 | 245 | 246 | 247 | /** 248 | * Enable or disable function time statistics. 249 | * 250 | * @param bool enable statistics 251 | * @param string sort by column 'count', 'deltaTime' or 'averageTime' 252 | * @return \Panel\XDebugTrace 253 | * @throws \Nette\InvalidArgumentException 254 | */ 255 | public function enableStatistics($enable = TRUE, $sortBy = NULL) 256 | { 257 | $sortBy = $sortBy ?: 'averageTime'; 258 | 259 | if (!in_array($sortBy, array('count', 'deltaTime', 'averageTime'))) { 260 | throw new \Nette\InvalidArgumentException("Cannot sort statistics by '$sortBy' column."); 261 | } 262 | 263 | $this->performStatistics = (bool) $enable; 264 | $this->sortBy = $sortBy; 265 | 266 | return $this; 267 | } 268 | 269 | 270 | 271 | /* ~~~ Start/Pause/Stop tracing part ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 272 | /** 273 | * Start or continue tracing. 274 | * 275 | * @param string|NULL trace title 276 | */ 277 | public function start($title = NULL) 278 | { 279 | if (!$this->isError) { 280 | if ($this->state === self::STATE_RUN) { 281 | $this->pause(); 282 | } 283 | 284 | if ($this->state === self::STATE_STOP) { 285 | $this->titles = array($title); 286 | xdebug_start_trace($this->traceFile, XDEBUG_TRACE_COMPUTERIZED); 287 | 288 | } elseif ($this->state === self::STATE_PAUSE) { 289 | $this->titles[] = $title; 290 | xdebug_start_trace($this->traceFile, XDEBUG_TRACE_COMPUTERIZED | XDEBUG_TRACE_APPEND); 291 | } 292 | 293 | $this->state = self::STATE_RUN; 294 | } 295 | } 296 | 297 | 298 | 299 | /** 300 | * Pause tracing. 301 | */ 302 | public function pause() 303 | { 304 | if ($this->state === self::STATE_RUN) { 305 | xdebug_stop_trace(); 306 | $this->state = self::STATE_PAUSE; 307 | } 308 | } 309 | 310 | 311 | 312 | /** 313 | * Stop tracing. 314 | */ 315 | public function stop() 316 | { 317 | if ($this->state === self::STATE_RUN) { 318 | xdebug_stop_trace(); 319 | } 320 | 321 | $this->state = self::STATE_STOP; 322 | } 323 | 324 | 325 | 326 | /*~~~ Rendering part ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 327 | /** 328 | * Lazy error template cooking. 329 | */ 330 | public function getErrorTemplate() 331 | { 332 | if ($this->lazyErrorTemplate === NULL) { 333 | $this->lazyErrorTemplate = new FileTemplate(__DIR__ . '/error.latte'); 334 | $this->lazyErrorTemplate->registerFilter(new Engine); 335 | } 336 | 337 | return $this->lazyErrorTemplate; 338 | } 339 | 340 | 341 | 342 | /** 343 | * Lazy content template cooking. 344 | */ 345 | public function getTemplate() 346 | { 347 | if ($this->lazyTemplate === NULL) { 348 | $this->lazyTemplate = new FileTemplate(__DIR__ . '/content.latte'); 349 | $this->lazyTemplate->registerFilter(new Engine); 350 | 351 | // Before [https://github.com/nette/nette/commit/ba80a1923e39cd56c3c35a6bbe26d44f1c52ff04] compatibility 352 | $helpersClass = class_exists('Nette\Templating\Helpers') ? 'Nette\Templating\Helpers::loader' : 'Nette\Templating\DefaultHelpers::loader'; 353 | $this->lazyTemplate->registerHelperLoader($helpersClass); 354 | 355 | $this->lazyTemplate->registerHelper('time', array($this, 'timeHelper')); 356 | $this->lazyTemplate->registerHelper('timeClass', array($this, 'timeClassHelper')); 357 | $this->lazyTemplate->registerHelper('basename', array($this, 'basenameHelper')); 358 | } 359 | 360 | return $this->lazyTemplate; 361 | } 362 | 363 | 364 | 365 | /** 366 | * Template helper converts seconds to ns, us, ms, s. 367 | * 368 | * @param float time interval in seconds 369 | * @param decimal part precision 370 | * @return string formated time 371 | */ 372 | public function timeHelper($time, $precision = 0) 373 | { 374 | $units = 's'; 375 | if ($time < 1e-6) { // <1us 376 | $units = 'ns'; 377 | $time *= 1e9; 378 | 379 | } elseif ($time < 1e-3) { // <1ms 380 | $units = "\xc2\xb5s"; 381 | $time *= 1e6; 382 | 383 | } elseif ($time < 1) { // <1s 384 | $units = 'ms'; 385 | $time *= 1e3; 386 | } 387 | 388 | return round($time, $precision) . ' ' . $units; 389 | } 390 | 391 | 392 | 393 | /** 394 | * Template helper converts seconds to HTML class string. 395 | * 396 | * @param float time interval in seconds 397 | * @param float over this value is interval classified as slow 398 | * @param float under this value is interval classified as fast 399 | * @return string 400 | */ 401 | public function timeClassHelper($time, $slow = NULL, $fast = NULL) 402 | { 403 | $slow = $slow ?: 0.02; // 20ms 404 | $fast = $fast ?: 1e-3; // 1ms 405 | 406 | if ($time <= $fast) { 407 | return 'timeFast'; 408 | 409 | } elseif ($time <= $slow) { 410 | return 'timeMedian'; 411 | } 412 | 413 | return 'timeSlow'; 414 | } 415 | 416 | 417 | 418 | /** 419 | * Template helper extracts base filename from file path. 420 | * 421 | * @param string path to file 422 | * @return string 423 | */ 424 | public function basenameHelper($path) 425 | { 426 | return basename($path); 427 | } 428 | 429 | 430 | 431 | /** 432 | * Sets internal error variables. 433 | * 434 | * @param string error message 435 | * @param array error_get_last() 436 | */ 437 | protected function setError($message, array $lastError = NULL) 438 | { 439 | $this->isError = TRUE; 440 | $this->errMessage = $message; 441 | 442 | if ($lastError !== NULL) { 443 | $this->errMessage .= ': ' . $lastError['message']; 444 | $this->errFile = $lastError['file']; 445 | $this->errLine = $lastError['line']; 446 | } 447 | } 448 | 449 | 450 | 451 | /** 452 | * Render error message. 453 | * 454 | * @return string rendered error template 455 | */ 456 | protected function renderError() 457 | { 458 | $template = $this->getErrorTemplate(); 459 | $template->errMessage = $this->errMessage; 460 | $template->errFile = $this->errFile; 461 | $template->errLine = $this->errLine; 462 | 463 | ob_start(); 464 | $template->render(); 465 | return ob_get_clean(); 466 | } 467 | 468 | 469 | 470 | /** 471 | * Implements \Nette\Diagnostics\IBarPanel 472 | */ 473 | public function getTab() 474 | { 475 | $dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAB2ElEQVQ4jaWS309SYRjHvemura2fE8tQJA/ojiBEoQ3n2uqm2mytixot10VbF7W6a5ZmukiWoUaGGCXnMAacAxM6tIPZD50X9ld94rSBMU4/lhffd++zd89nz/f7Pi1Ay25Uv4g2S7Uyrn9vsuzbQwNg6m6Q71qKK34b7m7rbyH2I3u5dSmAEn/JSMBFHTAo2tn+rLEuR3h17zJD3p4mSF/HIbKJKJW1dTR5kSGPcwdgyG1rZaOskow8YcGAeBx1iGg9iLoSQynqqEsznO7tbLRQU7+9jW+lDInwI6IPruJzWnEc3U9ejpMpaCiLzxhwCQ3TNfl0d1nQlSTS7GNSU7fJJpcplCrkY8+bmk0BNciq9AatXKFU2SAVmcDrtJmGawrwO9tRpTgl/StqLkf6dYh+4fi/AbzdbajJJQrlT+RiYZTJGyTmQ2RjL/CLXX+2cMrRTjH9DrXq+ePKHK7Ow3gEKzN3RpibHiMdDTHYZzcP0Scco5iRyH9YQ5cWMOram/Gly2M3mX54n7ezTxn4ZZKfx7DXwfYXna3NzWrzPP7e5sDOuAXkiVHkyDjx8DhnfT07ixS8EGBr9T3B8ycRT5iHZaij9QDXzvlQo5NcvzjcuIn/q10DfgAvvbB+PnEGHAAAAABJRU5ErkJggg=='; 476 | return "XDebugTrace"; 477 | } 478 | 479 | 480 | 481 | /** 482 | * Implements \Nette\Diagnostics\IBarPanel 483 | */ 484 | public function getPanel() 485 | { 486 | $this->stop(); 487 | 488 | if ($this->isError) { 489 | return $this->renderError(); 490 | } 491 | 492 | $parsingStart = microtime(TRUE); 493 | 494 | if (($traceFileSize = @filesize($this->traceFile . '.xt')) <= strlen(self::WRITE_OK)) { 495 | if ($traceFileSize === FALSE) { 496 | $this->setError("Cannot read trace file '$this->traceFile.xt' size", error_get_last()); 497 | 498 | } elseif ($traceFileSize === 0) { 499 | $this->setError("Trace file '$this->traceFile.xt' is empty"); 500 | 501 | } elseif (@file_get_contents($this->traceFile . '.xt') === self::WRITE_OK) { 502 | $this->setError('Tracing did not start'); 503 | } 504 | 505 | } elseif (($fd = @fopen($this->traceFile . '.xt', 'rb')) === FALSE) { 506 | $this->setError("Cannot open trace file '$this->traceFile.xt'", error_get_last()); 507 | 508 | } elseif (!preg_match('/^Version: 2\..*/', (string) fgets($fd, self::$traceLineLength))) { 509 | $this->setError('Trace file version line mischmasch'); 510 | 511 | } elseif (!preg_match('/^File format: 2/', (string) fgets($fd, self::$traceLineLength))) { 512 | $this->setError('Trace file format line mischmasch'); 513 | 514 | } else { 515 | while (($line = fgets($fd, self::$traceLineLength)) !== FALSE) { 516 | if (strncmp($line, 'TRACE START', 11) === 0) { // TRACE START line 517 | $this->openTrace(); 518 | 519 | } elseif (strncmp($line, 'TRACE END', 9) === 0) { // TRACE END line 520 | $this->closeTrace(); 521 | 522 | } elseif ($this->isTraceOpened()) { 523 | $line = rtrim($line, "\r\n"); 524 | 525 | $cols = explode("\t", $line); 526 | if (!strlen($cols[0]) && count($cols) === 5) { // last line before TRACE END 527 | /* 528 | $record = (object) array( 529 | 'time' => (float) $cols[3], 530 | 'memory' => (float) $cols[4], 531 | ); 532 | $this->addRecord($record, TRUE); 533 | */ 534 | continue; 535 | 536 | } else { 537 | $record = (object) array( 538 | 'level' => (int) $cols[0], 539 | 'id' => (float) $cols[1], 540 | 'isEntry' => !$cols[2], 541 | 'exited' => FALSE, 542 | 'time' => (float) $cols[3], 543 | 'exitTime' => NULL, 544 | 'deltaTime' => NULL, 545 | 'memory' => (float) $cols[4], 546 | 'exitMemory' => NULL, 547 | 'deltaMemory' => NULL, 548 | ); 549 | 550 | if ($record->isEntry) { 551 | $record->function = $cols[5]; 552 | $record->isInternal = !$cols[6]; 553 | $record->includeFile = strlen($cols[7]) ? $cols[7] : NULL; 554 | $record->filename = $cols[8]; 555 | $record->line = $cols[9]; 556 | $record->evalInfo = ''; 557 | 558 | if (strcmp(substr($record->filename, -13), "eval()'d code") === 0) { 559 | preg_match('/(.*)\(([0-9]+)\) : eval\(\)\'d code$/', $record->filename, $match); 560 | $record->evalInfo = "- eval()'d code ($record->line)"; 561 | $record->filename = $match[1]; 562 | $record->line = $match[2]; 563 | } 564 | } 565 | 566 | $this->addRecord($record); 567 | } 568 | } 569 | } 570 | 571 | $this->closeTrace(); // in case of non-complete trace file 572 | } 573 | 574 | if ($this->isError) { 575 | return $this->renderError(); 576 | } 577 | 578 | $template = $this->getTemplate(); 579 | $template->traces = $this->traces; 580 | $template->indents = $this->indents; 581 | $template->titles = $this->titles; 582 | $template->parsingTime = microtime(TRUE) - $parsingStart; 583 | $template->traceFileSize = $traceFileSize; 584 | 585 | if ($this->performStatistics) { 586 | $template->statistics = $this->statistics; 587 | } 588 | 589 | ob_start(); 590 | $template->render(); 591 | return ob_get_clean(); 592 | } 593 | 594 | 595 | 596 | /** 597 | * Sets trace and indent references. 598 | */ 599 | protected function openTrace() 600 | { 601 | $index = count($this->traces); 602 | 603 | $this->traces[$index] = array(); 604 | $this->trace =& $this->traces[$index]; 605 | 606 | $this->indents[$index] = array(); 607 | $this->indent =& $this->indents[$index]; 608 | 609 | if ($this->performStatistics) { 610 | $this->statistics[$index] = array(); 611 | $this->statistic =& $this->statistics[$index]; 612 | } 613 | } 614 | 615 | 616 | 617 | /** 618 | * Unset trace and indent references and compute indents. 619 | */ 620 | protected function closeTrace() 621 | { 622 | if ($this->trace !== NULL) { 623 | foreach ($this->trace as $id => $record) { 624 | if (!$record->exited) { // last chance to filter non-exited records by FILTER_EXIT callback 625 | $remove = FALSE; 626 | foreach ($this->filterExitCallbacks as $callback) { 627 | $result = (int) call_user_func($callback, $record, FALSE, $this); 628 | if ($result & self::SKIP) { 629 | $remove = TRUE; 630 | } 631 | 632 | if ($result & self::STOP) { 633 | break; 634 | } 635 | } 636 | 637 | if ($remove) { 638 | unset($this->trace[$id]); 639 | continue; 640 | } 641 | } 642 | 643 | $this->indent[$record->level] = 1; 644 | } 645 | 646 | if (count($this->indent)) { 647 | ksort($this->indent); 648 | $this->indent = array_combine(array_keys($this->indent), range(0, count($this->indent) - 1)); 649 | } 650 | 651 | $null = NULL; 652 | $this->trace =& $null; 653 | $this->indent =& $null; 654 | 655 | if ($this->performStatistics) { 656 | foreach ($this->statistic as $statistic) { 657 | $statistic->averageTime = $statistic->deltaTime / $statistic->count; 658 | } 659 | 660 | $sortBy = $this->sortBy; 661 | uasort($this->statistic, function($a, $b) use ($sortBy) { 662 | return $a->{$sortBy} < $b->{$sortBy}; 663 | }); 664 | 665 | $this->statistic =& $null; 666 | } 667 | } 668 | } 669 | 670 | 671 | 672 | /** 673 | * Check if internal references are sets. 674 | * @return bool 675 | */ 676 | protected function isTraceOpened() 677 | { 678 | return $this->trace !== NULL; 679 | } 680 | 681 | 682 | 683 | /** 684 | * Push parsed trace file line into trace stack. 685 | * 686 | * @param \stdClass parsed trace file line 687 | */ 688 | protected function addRecord(\stdClass $record) 689 | { 690 | if ($record->isEntry) { 691 | $add = TRUE; 692 | foreach ($this->filterEntryCallbacks as $callback) { 693 | $result = (int) call_user_func($callback, $record, TRUE, $this); 694 | if ($result & self::SKIP) { 695 | $add = FALSE; 696 | } 697 | 698 | if ($result & self::STOP) { 699 | break; 700 | } 701 | } 702 | 703 | if ($add) { 704 | $this->trace[$record->id] = $record; 705 | } 706 | 707 | } elseif (isset($this->trace[$record->id])) { 708 | $entryRecord = $this->trace[$record->id]; 709 | 710 | $entryRecord->exited = TRUE; 711 | $entryRecord->exitTime = $record->time; 712 | $entryRecord->deltaTime = $record->time - $entryRecord->time; 713 | $entryRecord->exitMemory = $record->memory; 714 | $entryRecord->deltaMemory = $record->memory - $entryRecord->memory; 715 | 716 | $remove = FALSE; 717 | foreach ($this->filterExitCallbacks as $callback) { 718 | $result = (int) call_user_func($callback, $entryRecord, FALSE, $this); 719 | if ($result & self::SKIP) { 720 | $remove = TRUE; 721 | } 722 | 723 | if ($result & self::STOP) { 724 | break; 725 | } 726 | } 727 | 728 | if ($remove) { 729 | unset($this->trace[$record->id]); 730 | 731 | } elseif ($this->performStatistics) { 732 | if (!isset($this->statistic[$entryRecord->function])) { 733 | $this->statistic[$entryRecord->function] = (object) array( 734 | 'count' => 1, 735 | 'deltaTime' => $entryRecord->deltaTime, 736 | ); 737 | 738 | } else { 739 | $this->statistic[$entryRecord->function]->count += 1; 740 | $this->statistic[$entryRecord->function]->deltaTime += $entryRecord->deltaTime; 741 | } 742 | } 743 | } 744 | } 745 | 746 | 747 | 748 | /* ~~~ Trace records filtering ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 749 | /** 750 | * Default filter (self::defaultFilterCb()) setting. 751 | * 752 | * @param string 753 | * @param bool skip or not 754 | * @return \Panel\XDebugTrace 755 | */ 756 | public function skip($type, $skip) 757 | { 758 | if (!array_key_exists($type, $this->skipOver)) { 759 | throw new Nette\InvalidArgumentException("Unknown skip type '$type'. Use one of [" . implode(', ', array_keys($this->skipOver)) . ']'); 760 | } 761 | 762 | $this->skipOver[$type] = (bool) $skip; 763 | return $this; 764 | } 765 | 766 | 767 | 768 | /** 769 | * Shortcut to self::skip('phpInternals', bool) 770 | * 771 | * @param bool skip PHP internal functions? 772 | * @return \Panel\XDebugTrace 773 | */ 774 | public function skipInternals($skip) 775 | { 776 | return $this->skip('phpInternals', $skip); 777 | } 778 | 779 | 780 | 781 | /** 782 | * Default filtering callback. 783 | * 784 | * @param \stdClass trace file record 785 | * @return int bitmask of self::SKIP, self::STOP 786 | */ 787 | protected function defaultFilterCb(\stdClass $record) 788 | { 789 | if ($this->skipOver['phpInternals'] && $record->isInternal) { 790 | return self::SKIP; 791 | } 792 | 793 | if ($this->skipOver['XDebugTrace']) { 794 | if ($record->filename === __FILE__) { 795 | return self::SKIP; 796 | } 797 | 798 | if (strncmp($record->function, 'Panel\\XDebugTrace::', 19) === 0) { 799 | return self::SKIP; 800 | } 801 | 802 | if (strncmp($record->function, 'Panel\\XDebugTrace->', 19) === 0) { 803 | return self::SKIP; 804 | } 805 | } 806 | 807 | if ($this->skipOver['Nette']) { 808 | if (strncmp($record->function, 'Nette\\', 6) === 0) { 809 | return self::SKIP; 810 | } 811 | } 812 | 813 | if ($this->skipOver['Composer']) { 814 | if (strncmp($record->function, 'Composer\\', 9) === 0) { 815 | return self::SKIP; 816 | } 817 | } 818 | 819 | if ($this->skipOver['callbacks']) { 820 | if ($record->function === 'callback' || $record->function === '{closure}') { 821 | return self::SKIP; 822 | } 823 | } 824 | 825 | if ($this->skipOver['includes']) { 826 | if ($record->includeFile !== NULL) { 827 | return self::SKIP; 828 | } 829 | } 830 | } 831 | 832 | 833 | 834 | /** 835 | * Register own filter callback. 836 | * 837 | * @param callback(\stdClass $record, bool $isEntry, \Panel\XDebugTrace $this) 838 | * @param int bitmask of self::FILTER_* 839 | */ 840 | public function addFilterCallback($callback, $flags = NULL) 841 | { 842 | $flags = (int) $flags; 843 | 844 | if ($flags & self::FILTER_REPLACE_ENTRY) { 845 | $this->filterEntryCallbacks = array(); 846 | } 847 | 848 | if ($flags & self::FILTER_REPLACE_EXIT) { 849 | $this->filterExitCallbacks = array(); 850 | } 851 | 852 | // Called when entry records came 853 | if (($flags & self::FILTER_ENTRY) || !($flags & self::FILTER_EXIT)) { 854 | if ($flags & self::FILTER_APPEND_ENTRY) { 855 | $this->filterEntryCallbacks[] = $callback; 856 | 857 | } else { 858 | array_unshift($this->filterEntryCallbacks, $callback); 859 | } 860 | } 861 | 862 | // Called when exit records came 863 | if ($flags & self::FILTER_EXIT) { 864 | if ($flags & self::FILTER_APPEND_EXIT) { 865 | $this->filterExitCallbacks[] = $callback; 866 | 867 | } else { 868 | array_unshift($this->filterExitCallbacks, $callback); 869 | } 870 | } 871 | } 872 | 873 | 874 | 875 | /** 876 | * Replace all filter callbacks by this one. 877 | * 878 | * @param callback(\stdClass $record, bool $isEntry, \Panel\XDebugTrace $this) 879 | * @param int bitmask of self::FILTER_* 880 | */ 881 | public function setFilterCallback($callback, $flags = NULL) 882 | { 883 | $flags = ((int) $flags) | self::FILTER_REPLACE; 884 | return $this->addFilterCallback($callback, $flags); 885 | } 886 | 887 | 888 | 889 | /* ~~~ Filtering callback shortcuts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ 890 | /** 891 | * Trace all. 892 | */ 893 | public function traceAll() 894 | { 895 | /* 896 | $cb = function () { 897 | return NULL; 898 | }; 899 | 900 | $this->setFilterCallback($cb, self::FILTER_BOTH); 901 | */ 902 | $this->filterEntryCallbacks = array(); 903 | $this->filterExitCallbacks = array(); 904 | } 905 | 906 | 907 | 908 | /** 909 | * Trace function by name. 910 | * 911 | * @param string|array name of function or pair array(class, method) 912 | * @param bool show inside function trace too 913 | * @param bool show internals in inside function trace 914 | */ 915 | public function traceFunction($name, $deep = FALSE, $showInternals = FALSE) 916 | { 917 | if (is_array($name)) { 918 | $name1 = implode('::', $name); 919 | $name2 = implode('->', $name); 920 | } else { 921 | $name1 = $name2 = (string) $name; 922 | } 923 | 924 | $cb = function(\stdClass $record, $isEntry) use ($name1, $name2, $deep, $showInternals) { 925 | static $cnt = 0; 926 | 927 | if ($record->function === $name1 || $record->function === $name2) { 928 | $cnt += $isEntry ? 1 : -1; 929 | return NULL; 930 | } 931 | 932 | return ($deep && $cnt && ($showInternals || !$record->isInternal)) ? NULL : XDebugTrace::SKIP; 933 | }; 934 | 935 | $this->setFilterCallback($cb, self::FILTER_BOTH); 936 | } 937 | 938 | 939 | 940 | /** 941 | * Trace function which name is expressed by PCRE reqular expression. 942 | * 943 | * @param string regular expression 944 | * @param bool show inside function trace too 945 | * @param bool show internals in inside function trace 946 | */ 947 | public function traceFunctionRe($re, $deep = FALSE, $showInternals = FALSE) 948 | { 949 | $cb = function(\stdClass $record, $isEntry) use ($re, $deep, $showInternals) { 950 | static $cnt = 0; 951 | 952 | if (preg_match($re, $record->function)) { 953 | $cnt += $isEntry ? 1 : -1; 954 | return NULL; 955 | } 956 | 957 | return ($deep && $cnt && ($showInternals || !$record->isInternal)) ? NULL : XDebugTrace::SKIP; 958 | }; 959 | 960 | $this->setFilterCallback($cb, self::FILTER_BOTH); 961 | } 962 | 963 | 964 | 965 | /** 966 | * Trace functions running over/under the time. 967 | * 968 | * @param float delta time 969 | * @param bool TRUE = over the delta time, FALSE = under the delta time 970 | */ 971 | public function traceDeltaTime($delta, $over = TRUE) 972 | { 973 | if (is_string($delta)) { 974 | static $units = array( 975 | 'ns' => 1e-9, 976 | 'us' => 1e-6, 977 | 'ms' => 1e-3, 978 | 's' => 1, 979 | ); 980 | 981 | foreach ($units as $unit => $multipler) { 982 | $length = strlen($unit); 983 | if (substr_compare($delta, $unit, -$length, $length, TRUE) === 0) { 984 | $delta = substr($delta, 0, -$length) * $multipler; 985 | break; 986 | } 987 | } 988 | } 989 | $delta = (float) $delta; 990 | 991 | $cb = function(\stdClass $record) use ($delta, $over) { 992 | if ($over) { 993 | if ($record->deltaTime < $delta) { 994 | return XDebugTrace::SKIP; 995 | } 996 | } else { 997 | if ($record->deltaTime > $delta) { 998 | return XDebugTrace::SKIP; 999 | } 1000 | } 1001 | }; 1002 | 1003 | $this->setFilterCallback($cb, self::FILTER_EXIT); 1004 | } 1005 | 1006 | 1007 | 1008 | /** 1009 | * Trace functions which consumes over/under the memory. 1010 | * 1011 | * @param float delta memory 1012 | * @param bool TRUE = over the delta memory, FALSE = under the delta memory 1013 | */ 1014 | public function traceDeltaMemory($delta, $over = TRUE) 1015 | { 1016 | if (is_string($delta)) { 1017 | static $units = array( 1018 | 'MB' => 1048576, // 1024 * 1024 1019 | 'kB' => 1024, 1020 | 'B' => 1, 1021 | ); 1022 | 1023 | foreach ($units as $unit => $multipler) { 1024 | $length = strlen($unit); 1025 | if (substr_compare($delta, $unit, -$length, $length, TRUE) === 0) { 1026 | $delta = substr($delta, 0, -$length) * $multipler; 1027 | break; 1028 | } 1029 | } 1030 | } 1031 | $delta = (float) $delta; 1032 | 1033 | $cb = function(\stdClass $record) use ($delta, $over) { 1034 | if ($over) { 1035 | if ($record->deltaMemory < $delta) { 1036 | return XDebugTrace::SKIP; 1037 | } 1038 | } else { 1039 | if ($record->deltaMemory > $delta) { 1040 | return XDebugTrace::SKIP; 1041 | } 1042 | } 1043 | }; 1044 | 1045 | $this->setFilterCallback($cb, self::FILTER_EXIT); 1046 | } 1047 | 1048 | } 1049 | -------------------------------------------------------------------------------- /XDebugTraceExtension.php: -------------------------------------------------------------------------------- 1 | 17 | * @see http://github.com/milo/XDebugTracePanel 18 | * @licence LGPL 19 | */ 20 | class XDebugTraceExtension extends CompilerExtension 21 | { 22 | private $defaults = array( 23 | 'traceFile' => '%tempDir%/xdebugTrace.xt', 24 | 'onCreate' => NULL, 25 | 'statistics' => NULL, 26 | ); 27 | 28 | 29 | public function afterCompile(ClassType $class) 30 | { 31 | $config = $this->getConfig($this->defaults); 32 | 33 | $name = Container::getMethodName($this->name); 34 | $class->addMethod($name); 35 | 36 | $method = $class->methods[$name]; 37 | $method->setBody(''); 38 | $method->addBody('$service = new Panel\XDebugTrace(?);', array($config['traceFile'])); 39 | 40 | if (!empty($config['onCreate'])) { 41 | $method->addBody('call_user_func(?, $service);', array($config['onCreate'])); 42 | } 43 | 44 | if (!empty($config['statistics'])) { 45 | $args = is_array($config['statistics']) ? ($config['statistics'] + array(TRUE, NULL)) : array($config['statistics'], NULL); 46 | $method->addBody('$service->enableStatistics(?, ?);', $args); 47 | } 48 | 49 | $method->addBody('return $service;'); 50 | $method->documents = array('@return Panel\XDebugTrace'); 51 | 52 | foreach ($class->documents as $k => $v) { 53 | if (preg_match('~@property.*\$' . preg_quote($this->name, '~') . '$~', $v)) { 54 | $class->documents[$k] = '@property Panel\XDebugTrace $' . $this->name; 55 | break; 56 | } 57 | } 58 | 59 | $class->methods['initialize']->addBody('Nette\Diagnostics\Debugger::addPanel($this->getService(?));', array($this->name)); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /content.latte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 |

XDebugTrace

38 | 39 |
40 | 41 | {var $indent = $indents[$traceNo]} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |

{$titles[$traceNo]} trace

IDLevelFunctionΔ TimeΔ MemoryFile (Line)
{$record->id}{$record->level}{$record->function}{$record->deltaTime|time}{$record->deltaMemory|bytes}{$record->filename|basename} ({$record->line}) {$record->evalInfo}
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |

{$titles[$traceNo]} statistics

CountΔ Time⌀ TimeFunction
{$statistic->count}{$statistic->deltaTime|time}{$statistic->averageTime|time}{$function}
87 |
88 |
89 |

90 | Trace file size {$traceFileSize|bytes:2} parsed in {$parsingTime|time:3} 91 |

92 |
93 | -------------------------------------------------------------------------------- /error.latte: -------------------------------------------------------------------------------- 1 |
2 |

XDebugTrace

3 | 4 |
5 |

XDebugTrace error occured{if isset($errFile, $errLine)} in file {$errFile} ({$errLine}){/if}

6 |

{$errMessage}

7 |
8 |
9 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milo/XDebugTracePanel/2a0910034b3e2a511fc537d08cbecb3c2d00f727/screenshot.png --------------------------------------------------------------------------------