forked from CIT/Vmeda.Online
		
	
		
			
				
	
	
		
			347 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| // This file is part of Moodle - http://moodle.org/
 | |
| //
 | |
| // Moodle is free software: you can redistribute it and/or modify
 | |
| // it under the terms of the GNU General Public License as published by
 | |
| // the Free Software Foundation, either version 3 of the License, or
 | |
| // (at your option) any later version.
 | |
| //
 | |
| // Moodle is distributed in the hope that it will be useful,
 | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| // GNU General Public License for more details.
 | |
| //
 | |
| // You should have received a copy of the GNU General Public License
 | |
| // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| use Behat\Mink\Exception\DriverException;
 | |
| use Facebook\WebDriver\Exception\InvalidArgumentException;
 | |
| use Moodle\BehatExtension\Driver\WebDriver;
 | |
| 
 | |
| require_once(__DIR__ . '/../behat_app.php');
 | |
| 
 | |
| /**
 | |
|  * Performance measures for one particular metric.
 | |
|  */
 | |
| class performance_measure implements behat_app_listener {
 | |
| 
 | |
|     const STORAGE_FOLDER = '/behatperformancemeasures/';
 | |
| 
 | |
|     /**
 | |
|      * @var string
 | |
|      */
 | |
|     public $name;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $start;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $end;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $duration;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $scripting;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $styling;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|      public $blocking;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $databaseStart;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $database;
 | |
| 
 | |
|     /**
 | |
|      * @var int
 | |
|      */
 | |
|     public $networking;
 | |
| 
 | |
|     /**
 | |
|      * @var array
 | |
|      */
 | |
|     private $longTasks = [];
 | |
| 
 | |
|     /**
 | |
|      * @var Closure
 | |
|      */
 | |
|     private $behatAppUnsubscribe;
 | |
| 
 | |
|     /**
 | |
|      * @var Moodle\BehatExtension\Driver\WebDriver
 | |
|      */
 | |
|     private $driver;
 | |
| 
 | |
|     public function __construct(string $name, WebDriver $driver) {
 | |
|         $this->name = $name;
 | |
|         $this->driver = $driver;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Start timing.
 | |
|      */
 | |
|     public function start(): void {
 | |
|         $this->start = $this->now();
 | |
| 
 | |
|         $this->observeLongTasks();
 | |
|         $this->startDatabaseCount();
 | |
| 
 | |
|         $this->behatAppUnsubscribe = behat_app::listen($this);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Stop timing.
 | |
|      */
 | |
|     public function end(): void {
 | |
|         $this->end = $this->now();
 | |
| 
 | |
|         $this->stopLongTasksObserver();
 | |
| 
 | |
|         call_user_func($this->behatAppUnsubscribe);
 | |
|         $this->behatAppUnsubscribe = null;
 | |
| 
 | |
|         $this->analyseDuration();
 | |
|         $this->analyseLongTasks();
 | |
|         $this->analyseDatabaseUsage();
 | |
|         $this->analysePerformanceLogs();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Persist measure logs in storage.
 | |
|      */
 | |
|     public function store(): void {
 | |
|         global $CFG;
 | |
| 
 | |
|         $storagefolderpath = $CFG->dirroot . static::STORAGE_FOLDER;
 | |
| 
 | |
|         if (!file_exists($storagefolderpath)) {
 | |
|             mkdir($storagefolderpath);
 | |
|         }
 | |
| 
 | |
|         $data = [
 | |
|             'name' => $this->name,
 | |
|             'start' => $this->start,
 | |
|             'end' => $this->end,
 | |
|             'duration' => $this->duration,
 | |
|             'scripting' => $this->scripting,
 | |
|             'styling' => $this->styling,
 | |
|             'blocking' => $this->blocking,
 | |
|             'longTasks' => count($this->longTasks),
 | |
|             'database' => $this->database,
 | |
|             'networking' => $this->networking,
 | |
|         ];
 | |
| 
 | |
|         file_put_contents($storagefolderpath . time() . '.json', json_encode($data));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @inheritdoc
 | |
|      */
 | |
|     public function on_app_load(): void {
 | |
|         if (is_null($this->start) || !is_null($this->end)) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         $this->observeLongTasks();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @inheritdoc
 | |
|      */
 | |
|     public function on_app_unload(): void {
 | |
|         $this->stopLongTasksObserver();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get current time.
 | |
|      *
 | |
|      * @return int Current time in milliseconds.
 | |
|      */
 | |
|     private function now(): int {
 | |
|         return $this->driver->evaluateScript('Date.now()');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Start observing long tasks.
 | |
|      */
 | |
|     private function observeLongTasks(): void {
 | |
|         $this->driver->executeScript("
 | |
|             if (window.MA_PERFORMANCE_OBSERVER) return;
 | |
| 
 | |
|             window.MA_LONG_TASKS = [];
 | |
|             window.MA_PERFORMANCE_OBSERVER = new PerformanceObserver(list => {
 | |
|                 for (const entry of list.getEntries()) {
 | |
|                     window.MA_LONG_TASKS.push(entry);
 | |
|                 }
 | |
|             });
 | |
|             window.MA_PERFORMANCE_OBSERVER.observe({ entryTypes: ['longtask'] });
 | |
|         ");
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Record how many database queries have been logged so far.
 | |
|      */
 | |
|     private function startDatabaseCount(): void {
 | |
|         try {
 | |
|             $this->databaseStart = $this->driver->evaluateScript('dbProvider.queryLogs.length') ?? 0;
 | |
|         } catch (Exception $e) {
 | |
|             $this->databaseStart = 0;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Flush Performance observer.
 | |
|      */
 | |
|     private function stopLongTasksObserver(): void {
 | |
|         $newLongTasks = $this->driver->evaluateScript("
 | |
|             return (function() {
 | |
|                 if (!window.MA_PERFORMANCE_OBSERVER) {
 | |
|                     return [];
 | |
|                 }
 | |
| 
 | |
|                 window.MA_PERFORMANCE_OBSERVER.disconnect();
 | |
| 
 | |
|                 const observer = window.MA_PERFORMANCE_OBSERVER;
 | |
|                 const longTasks = window.MA_LONG_TASKS;
 | |
| 
 | |
|                 delete window.MA_PERFORMANCE_OBSERVER;
 | |
|                 delete window.MA_LONG_TASKS;
 | |
| 
 | |
|                 return [...longTasks, ...observer.takeRecords()];
 | |
|             })();
 | |
|         ");
 | |
| 
 | |
|         if ($newLongTasks) {
 | |
|             $this->longTasks = array_merge($this->longTasks, $newLongTasks);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Analyse duration.
 | |
|      */
 | |
|     private function analyseDuration(): void {
 | |
|         $this->duration = $this->end - $this->start;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Analyse long tasks.
 | |
|      */
 | |
|     private function analyseLongTasks(): void {
 | |
|         $blockingDuration = 0;
 | |
| 
 | |
|         foreach ($this->longTasks as $longTask) {
 | |
|             $blockingDuration += $longTask['duration'] - 50;
 | |
|         }
 | |
| 
 | |
|         $this->blocking = $blockingDuration;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Analyse database usage.
 | |
|      */
 | |
|     private function analyseDatabaseUsage(): void {
 | |
|         $this->database = $this->driver->evaluateScript('dbProvider.queryLogs.length') - $this->databaseStart;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Analyse performance logs.
 | |
|      */
 | |
|     private function analysePerformanceLogs(): void {
 | |
|         global $CFG;
 | |
| 
 | |
|         $scriptingDuration = 0;
 | |
|         $stylingDuration = 0;
 | |
|         $networkingCount = 0;
 | |
|         $logs = $this->getPerformanceLogs();
 | |
| 
 | |
|         foreach ($logs as $log) {
 | |
|             // TODO this should filter by end time as well, but it seems like the timestamps are not
 | |
|             // working as expected.
 | |
|             if ($log['timestamp'] < $this->start) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             $message = json_decode($log['message'])->message;
 | |
|             $messagename = $message->params->name ?? '';
 | |
| 
 | |
|             if (in_array($messagename, ['FunctionCall', 'GCEvent', 'MajorGC', 'MinorGC', 'EvaluateScript'])) {
 | |
|                 $scriptingDuration += $message->params->dur;
 | |
| 
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             if (in_array($messagename, ['UpdateLayoutTree', 'RecalculateStyles', 'ParseAuthorStyleSheet'])) {
 | |
|                 $stylingDuration += $message->params->dur;
 | |
| 
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             if (in_array($messagename, ['XHRLoad']) && !str_starts_with($message->params->args->data->url, $CFG->behat_ionic_wwwroot)) {
 | |
|                 $networkingCount++;
 | |
| 
 | |
|                 continue;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         $this->scripting = round($scriptingDuration / 1000);
 | |
|         $this->styling = round($stylingDuration / 1000);
 | |
|         $this->networking = $networkingCount;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get performance logs.
 | |
|      *
 | |
|      * @return array Performance logs.
 | |
|      */
 | |
|     private function getPerformanceLogs(): array {
 | |
|         try {
 | |
|             return $this->driver->getWebDriver()->manage()->getLog('performance');
 | |
|         } catch (InvalidArgumentException $e) {
 | |
|             throw new DriverException(
 | |
|                 implode("\n", [
 | |
|                     "It wasn't possible to get performance logs, make sure that you have configured the following capabilities:",
 | |
|                     "",
 | |
|                     "\$CFG->behat_profiles = [",
 | |
|                     "    'default' => [",
 | |
|                     "        'browser' => 'chrome',",
 | |
|                     "        'wd_host' => 'http://selenium:4444/wd/hub',",
 | |
|                     "        'capabilities' => [",
 | |
|                     "            'extra_capabilities' => [",
 | |
|                     "                'goog:loggingPrefs' => ['performance' => 'ALL'],",
 | |
|                     "                'chromeOptions' => [",
 | |
|                     "                    'perfLoggingPrefs' => [",
 | |
|                     "                        'traceCategories' => 'devtools.timeline',",
 | |
|                     "                    ],",
 | |
|                     "                ],",
 | |
|                     "            ],",
 | |
|                     "        ],",
 | |
|                     "    ],",
 | |
|                     "];",
 | |
|                     "",
 | |
|                 ])
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
| }
 |