| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| declare(strict_types=1); |
|
|
| namespace Piwik\Plugins\ArchivingMetrics; |
|
|
| use Piwik\ArchiveProcessor\Rules; |
| use Piwik\Date; |
| use Piwik\Plugins\ArchivingMetrics\Clock\Clock; |
| use Piwik\Plugins\ArchivingMetrics\Clock\ClockInterface; |
| use Piwik\Plugins\ArchivingMetrics\Writer\DbWriter; |
| use Piwik\Plugins\ArchivingMetrics\Writer\WriterInterface; |
|
|
| final class Timer |
| { |
| |
| |
| |
| private $isArchivePhpTriggered; |
|
|
| |
| |
| |
| private $clock; |
|
|
| |
| |
| |
| private $writer; |
|
|
| |
| |
| |
| private $runs = []; |
|
|
| |
| |
| |
| private static $instance; |
|
|
| public function __construct(bool $isArchivePhpTriggered, ClockInterface $clock, WriterInterface $writer) |
| { |
| $this->isArchivePhpTriggered = $isArchivePhpTriggered; |
| $this->clock = $clock; |
| $this->writer = $writer; |
| } |
|
|
| public static function getInstance(bool $isArchivePhpTriggered, ?ClockInterface $clock = null, ?WriterInterface $writer = null): self |
| { |
| if (self::$instance !== null) { |
| return self::$instance; |
| } |
|
|
| if ($clock === null) { |
| $clock = new Clock(); |
| } |
|
|
| if ($writer === null) { |
| $writer = new DbWriter(); |
| } |
|
|
| self::$instance = new self($isArchivePhpTriggered, $clock, $writer); |
| return self::$instance; |
| } |
|
|
| |
| |
| |
| public static function resetInstanceForTests(): void |
| { |
| self::$instance = null; |
| } |
|
|
| public function start(Context $context): void |
| { |
| if (false === $this->isApplicableForTiming($context)) { |
| return; |
| } |
|
|
| $this->runs[$context->getKey()] = [ |
| 'context' => $context, |
| 'timeStarted' => $this->clock->microtime(), |
| ]; |
| } |
|
|
| public function complete(Context $context, array $idArchives, bool $wasCached): void |
| { |
| if (false === $this->isApplicableForTiming($context)) { |
| return; |
| } |
|
|
| if (true === $wasCached) { |
| return; |
| } |
|
|
| if (empty($idArchives) || count($idArchives) !== 1) { |
| return; |
| } |
|
|
| $idArchive = reset($idArchives); |
| $key = $context->getKey(); |
|
|
| if (!isset($this->runs[$key]['timeStarted'])) { |
| return; |
| } |
|
|
| $finishedAt = $this->clock->microtime(); |
| $totalTimeMs = ($finishedAt - $this->runs[$key]['timeStarted']); |
|
|
| $this->runs[$key]['ts_started'] = date('Y-m-d H:i:s', (int) $this->runs[$key]['timeStarted']); |
| $this->runs[$key]['totalTime'] = $totalTimeMs; |
| $this->runs[$key]['timeFinished'] = $finishedAt; |
| $this->runs[$key]['ts_finished'] = date('Y-m-d H:i:s', (int) $this->runs[$key]['timeFinished']); |
|
|
| $exclusiveTimeMs = $this->calculateExclusiveTime($key); |
| $this->runs[$key]['exclusiveTime'] = $exclusiveTimeMs; |
|
|
| |
| $storedContext = $this->runs[$key]['context']; |
| $archiveName = Rules::getDoneStringFlagFor( |
| [$storedContext->idSite], |
| $storedContext->segment, |
| $storedContext->period->getLabel(), |
| $storedContext->plugin |
| ); |
| $this->writer->write($storedContext, [ |
| 'idarchive' => $idArchive, |
| 'archive_name' => $archiveName, |
| 'ts_started' => $this->runs[$key]['ts_started'], |
| 'ts_finished' => $this->runs[$key]['ts_finished'], |
| 'total_time' => (int) round($totalTimeMs * 1000), |
| 'total_time_exclusive' => (int) round($exclusiveTimeMs * 1000), |
| 'is_temporary' => $this->isTemporaryArchive($storedContext, (int) $this->runs[$key]['timeStarted']), |
| ]); |
| } |
|
|
| private function isTemporaryArchive(Context $context, int $archiveStartTimestamp): int |
| { |
| $archiveStartInSiteTimezone = Date::factory($archiveStartTimestamp, $context->siteTimezone); |
| $periodEndInSiteTimezone = $context->period->getDateTimeEnd()->setTimezone($context->siteTimezone); |
| return (int) $archiveStartInSiteTimezone->isEarlier($periodEndInSiteTimezone); |
| } |
|
|
| private function isApplicableForTiming(Context $context): bool |
| { |
| if (false === $this->isArchivePhpTriggered) { |
| return false; |
| } |
|
|
| $doneFlag = Rules::getDoneStringFlagFor( |
| [$context->idSite], |
| $context->segment, |
| $context->period->getLabel(), |
| $context->plugin |
| ); |
| if (strpos($doneFlag, '.') !== false) { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| private function calculateExclusiveTime(string $currentKey): float |
| { |
| if (empty($this->runs[$currentKey])) { |
| return 0.0; |
| } |
|
|
| $current = $this->runs[$currentKey]; |
|
|
| $totalTimeMs = $current['totalTime']; |
|
|
| |
| if ($currentKey === array_key_last($this->runs)) { |
| return $totalTimeMs; |
| } |
|
|
| $childTotalMs = 0.0; |
|
|
| foreach ($this->runs as $otherKey => $run) { |
| if ($otherKey === $currentKey) { |
| continue; |
| } |
| if (empty($run['exclusiveTime'])) { |
| continue; |
| } |
|
|
| $childFinished = $run['timeFinished'] ?? null; |
| $childStarted = $run['timeStarted'] ?? null; |
| if ($childFinished === null || $childStarted === null) { |
| continue; |
| } |
|
|
| if ($childFinished <= $current['timeFinished'] && $childStarted >= $current['timeStarted']) { |
| $childTotalMs += $run['exclusiveTime']; |
| } |
| } |
|
|
| $exclusive = $totalTimeMs - $childTotalMs; |
| if ($exclusive < 0) { |
| return $totalTimeMs; |
| } |
|
|
| return $exclusive; |
| } |
| } |
|
|