. use Moodle\BehatExtension\Driver\WebDriver; /** * 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 $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->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->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), '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'] }); "); } /** * 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 { $blocking = 0; foreach ($this->longTasks as $longTask) { $blocking += $longTask['duration'] - 50; } $this->blocking = $blocking; } /** * Analyse performance logs. */ private function analysePerformanceLogs(): void { global $CFG; $scripting = 0; $styling = 0; $networking = 0; $logs = $this->driver->getWebDriver()->manage()->getLog('performance'); 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'])) { $scripting += $message->params->dur; continue; } if (in_array($messagename, ['UpdateLayoutTree', 'RecalculateStyles', 'ParseAuthorStyleSheet'])) { $styling += $message->params->dur; continue; } if (in_array($messagename, ['XHRLoad']) && !str_starts_with($message->params->args->data->url, $CFG->behat_ionic_wwwroot)) { $networking++; continue; } } $this->scripting = round($scripting / 1000); $this->styling = round($styling / 1000); $this->networking = $networking; } }