Source of file Ralph.php
Size: 12,460 Bytes - Last Modified: 2021-12-23T10:20:54+00:00
/var/www/docs.ssmods.com/process/src/src/Ralph/Ralph.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 | <?php namespace Symbiote\Ralph; use Symbiote\Ralph\FunctionCallRecord; use Symbiote\Ralph\ProfileRecord; use Symbiote\Ralph\ClassName; use Config; use ClassInfo; use SSViewer; use ArrayList; use ArrayData; use DataList; require_once(dirname(__FILE__).'/../ss4_compat.php'); class Ralph { const MODULE_DIR = 'ralph'; /** * @var boolean */ private static $is_enabled = false; /** * @var array */ private static $default_settings = array( 'enable_cms' => false, 'dump_file' => false, 'classes' => array(), 'default_classes' => array( 'DataList' => array( '__construct' => array(null, 'postConstructCall'), 'toArray', 'count', 'max', 'min', 'avg', 'sum', 'toNestedArray', 'limit', ), ), ); /** * @var array */ protected $settings = array(); /** * @var string */ protected $class = ''; /** * Store function time * * @var array */ protected $data = array(); /** * @var boolean */ private $isSilverStripe4 = false; public function __construct() { $this->class = get_class(); $this->isSilverStripe4 = class_exists('SilverStripe\Core\CoreKernel'); } /** * Enables Ralph profiler, by default it will not be enabled in the CMS or in developer tools * * @return null */ public static function enable($settings = array()) { if (!static::$is_enabled) { $settings = array_merge(static::$default_settings, $settings); $inCMS = $settings['enable_cms']; if ($inCMS === false && (static::in_cms() || static::in_dev())) { return; } $ralph = singleton('Symbiote\\Ralph\\Ralph'); $ralph->settings = $settings; $ralph->init(); static::$is_enabled = true; } } /** * Initialize the Ralph profiler * * @return null */ public function init() { $config = null; $requestProcessorClass = $this->isSilverStripe4 ? 'SilverStripe\Control\RequestProcessor' : 'RequestProcessor'; $injectorClass = $this->isSilverStripe4 ? 'SilverStripe\Core\Injector\Injector' : 'Injector'; $config = Config::inst()->get($injectorClass, $requestProcessorClass); $config['properties']['filters'][] = '%$Symbiote\Ralph\RequestFilter'; if ($this->isSilverStripe4) { Config::modify()->set($injectorClass, $requestProcessorClass, $config); } else { Config::inst()->update($injectorClass, $requestProcessorClass, $config); } $cmp = new MetaCompiler; $cmp->process(); } /** * This is a workaround for the injector affecting subclasses * basically force any subclasses of DataList to use their original * class leave the Injector behaviour in-tact. * * @return void */ public function useCustomClass($oldClass, $newClass) { $injectorClass = $this->isSilverStripe4 ? \SilverStripe\Core\Injector\Injector::class : 'Injector'; $subclasses = ClassInfo::subclassesFor($oldClass); unset($subclasses[$oldClass]); $originalInjectorInfo = array(); foreach ($subclasses as $class => $v) { $originalInjectorInfo[$class] = Config::inst()->get($injectorClass, $class); } $injector = Config::inst()->get($injectorClass, $oldClass); $injector['class'] = $newClass; if ($this->isSilverStripe4) { Config::modify()->set($injectorClass, $oldClass, $injector); } else { Config::inst()->update($injectorClass, $oldClass, $injector); } foreach ($subclasses as $class => $v) { $originalInjector = $originalInjectorInfo[$class]; if (!isset($originalInjector[$class]['class'])) { $originalInjector[$class]['class'] = $class; } if ($this->isSilverStripe4) { Config::modify()->set($injectorClass, $class, $originalInjector[$class]); } else { Config::inst()->update($injectorClass, $class, $originalInjector[$class]); } } } /** * Whether to dump to /ralph/src_generated/* folder or not. * * This exists to test and ensure instrumentation of functions is working, however * this comes at a disk IO penalty. * * @var boolean */ public function getDumpToFile() { return (bool)$this->settings['dump_file']; } /** * Get classes to instrument with profiling time code. * * @return null */ public function getClasses() { $classes = array_merge($this->settings['default_classes'], $this->settings['classes']); return $classes; } /** * To be inserted into and called from a DataList::__construct function. * * @return null */ public function constructorStore($object) { // Get backtrace (ie. remove 'object' and 'args' as it makes make echoing/dumping it easier to read) $bt = debug_backtrace(); foreach ($bt as &$btVal) { unset($btVal['args']); unset($btVal['object']); unset($btVal); } $goUpStackIfClass = array( // SS3 (and SS4 via the compatibility layer) 'File', 'DataModel', 'Hierarchy', 'DataObject', 'Versioned', // SS3-only 'Object', 'InjectionCreator', 'Injector', // SS4-only, 'SilverStripe\ORM\DataList', 'SilverStripe\Core\Injector\InjectionCreator', 'SilverStripe\Core\Injector\Injector', 'SilverStripe\View\ViewableData', 'SilverStripe\ORM\DataObject', ); $caller = $bt[2]; foreach ($bt as $i => $stackItem) { if ( // Silverstripe 3 $stackItem['class'] === 'Object' && $stackItem['function'] === 'create' || // Silverstripe 4 $stackItem['class'] === 'ReflectionClass' && $stackItem['function'] === 'newInstanceArgs' ) { $i++; $caller = $bt[$i]; while (isset($caller['class']) && in_array($caller['class'], $goUpStackIfClass)) { $i++; $caller = $bt[$i]; // SS3-only: Handle case where 'call_user_func_array' is called in Object if (!isset($caller['class'])) { $nextCaller = isset($bt[$i+1]) ? $bt[$i+1] : null; if ($nextCaller && $nextCaller['function'] === '__call' && $nextCaller['class'] === 'Object') { $i++; $caller = $bt[$i]; } } } $caller['line'] = isset($bt[$i-1]['line']) ? $bt[$i-1]['line'] : '?'; break; } } $functionCallRecord = new FunctionCallRecord; $functionCallRecord->Class = isset($caller['class']) ? $caller['class'] : ''; $functionCallRecord->Function = isset($caller['function']) ? $caller['function'] : ''; $functionCallRecord->Line = isset($caller['line']) ? $caller['line'] : -1; $object->__constructorFunctionCall = $functionCallRecord; } /** * Detect if user is in the CMS or not. * * @return boolean */ public static function in_cms() { // NOTE(Jake): Might need to remove 'Director::absoluteBaseURL()' from beginning of $url for certain cases later $url = (isset($_GET['url']) && php_sapi_name() !== 'cli-server') ? $_GET['url'] : $_SERVER['REQUEST_URI']; return (strpos($url, '/admin') === 0); } /** * Detect if user is in the developer build tools or not * * @return boolean */ public static function in_dev() { $url = (isset($_GET['url']) && php_sapi_name() !== 'cli-server') ? $_GET['url'] : $_SERVER['REQUEST_URI']; return (strpos($url, '/dev') === 0); } public function profilerStore($object, $functionName, $time) { // Get backtrace (ie. remove 'object' as it makes make echoing/dumping it easier to read) $bt = debug_backtrace(); foreach ($bt as &$btVal) { unset($btVal['object']); unset($btVal); } unset($bt[0]); // ignore self unset($bt[1]); // ignore 1st level replacement/wrapper function $goUpStackIfClass = array( $this->class, // SS3-only 'DataList', 'Hierarchy', 'ViewableData', 'SSViewer', 'SSViewer_Scope', 'SSViewer_DataPresenter', 'PaginatedList', 'IteratorIterator', 'Object', 'SS_ListDecorator', 'DataObject', 'Versioned', // // SS4-only // 'SilverStripe\View\ViewableData', //'SilverStripe\View\SSViewer', 'SilverStripe\View\SSViewer_Scope', 'SilverStripe\View\SSViewer_DataPresenter', //'SilverStripe\Control\RequestHandler', //'SilverStripe\Control\Director', //'SilverStripe\Control\Controller', //'SilverStripe\CMS\Controllers\ContentController', //'SilverStripe\CMS\Controllers\ModelAsController', 'SilverStripe\ORM\DataObject', 'SilverStripe\ORM\DataList', //'SilverStripe\Versioned\VersionedHTTPMiddleware', //'SilverStripe\Security\AuthenticationMiddleware', //'SilverStripe\Control\Middleware\CanonicalURLMiddleware', // SS4 Multisites 'Symbiote\Multisites\Control\MultisitesRootController', ); $caller = array(); $callerIndex = -1; foreach ($bt as $i => $stackItem) { $callerIndex = $i; if (!isset($caller['class'])) { if ($stackItem['function'] === 'include') { // If calling "include()" from SSViewer, assume template rendering if (isset($stackItem['file']) && strpos($stackItem['file'], 'SSViewer.php') !== FALSE) { $caller = $bt[$i-1]; $caller['class'] = ''; $caller['function'] = basename($caller['file']); break; } } if (// SS3-only: Added to skip Object calling call_user_func_array $stackItem['function'] === 'call_user_func_array' || // Added to skip PaginatedList::getTotalItems(), calls 'count()' on DataList $stackItem['function'] === 'count') { continue; } } // Fuzzy logic to wind up the stack up to just print the *.ss template where a function/method is being called. if (($stackItem['class'] === 'SSViewer' && $stackItem['function'] === 'execute_template') || ($stackItem['class'] === 'SSViewer_Scope' && $stackItem['function'] === 'next')) { $caller = $stackItem; $caller['class'] = ''; $caller['function'] = basename($caller['file']); break; } if (!isset($stackItem['class']) || (!in_array($stackItem['class'], $goUpStackIfClass))) { $caller = $stackItem; $caller['line'] = isset($bt[$callerIndex-1]['line']) ? $bt[$callerIndex-1]['line'] : -1; break; } } if (!$caller) { throw new Exception('Class ignore rules are too strict. Unable to determine a caller.'); } $functionCallRecord = new FunctionCallRecord; $functionCallRecord->Class = isset($caller['class']) ? $caller['class'] : ''; $functionCallRecord->Function = isset($caller['function']) ? $caller['function'] : ''; $functionCallRecord->Time = $time * 1000; // Convert seconds to milliseconds $functionCallRecord->Line = isset($caller['line']) ? $caller['line'] : -1; $profileRecord = new ProfileRecord; if (isset($object->__constructorFunctionCall)) { $profileRecord->Constructor = $object->__constructorFunctionCall; } $profileRecord->Caller = $functionCallRecord; // todo(Jake): Allow sorting by constructor or caller //$functionCallRecord = $profileRecord->Constructor; $this->data[$functionCallRecord->Class][$functionCallRecord->Function][0][] = $profileRecord; } /** * @return array */ public function getData() { return $this->data; } /** * Render profiling information. * * NOTE: This is called by RequestFilter and automatically appended to the bottom * of your HTML. * * @return HTMLText */ public function forTemplate() { $list = array(); $listSortOrder = array(); foreach ($this->data as $class => $functionData) { foreach ($functionData as $function => $lineData) { foreach ($lineData as $line => $profileRecordSet) { $totalTime = 0; $callCount = 0; foreach ($profileRecordSet as $i => $profileRecord) { $totalTime += $profileRecord->Caller->Time; ++$callCount; } $str = $class.'::'.$function.'('.$line.'): '.$totalTime.'ms (Count: '.$callCount.')'; //foreach ($data as $i => $track) { // $str .= "<br/>".'-- '.$track->class.'::'.$track->function.'('.$track->line.')'; //} $list[] = array( 'Class' => $class, 'Function' => $function, 'Line' => $line, 'Records' => new ArrayList($profileRecordSet), 'Time' => $totalTime ); } } } // Sort by time $time = array(); foreach ($list as $key => $val) { $time[$key] = $val['Time']; } array_multisort($time, SORT_DESC, $list); unset($time); $templates = array(); if ($this->isSilverStripe4) { $templates = SSViewer::get_templates_by_class(static::class, '', __CLASS__); } else { $classNameWithoutNamespace = substr(__CLASS__, strrpos(__CLASS__, '\\') + 1); $templates[] = $classNameWithoutNamespace; } $template = new SSViewer($templates); $html = $template->process(new ArrayData(array('Results' => new ArrayList($list))), null); return $html; } } |