Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.20% covered (success)
90.20%
46 / 51
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Timing
90.20% covered (success)
90.20%
46 / 51
88.89% covered (warning)
88.89%
8 / 9
15.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mark
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 clearMarks
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 measure
73.68% covered (warning)
73.68%
14 / 19
0.00% covered (danger)
0.00%
0 / 1
4.29
 sortEntries
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getEntries
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getEntriesByType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getEntryByName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use Psr\Log\LoggerAwareInterface;
22use Psr\Log\LoggerInterface;
23use Psr\Log\NullLogger;
24
25/**
26 * An interface to help developers measure the performance of their applications.
27 * This interface closely matches the W3C's User Timing specification.
28 * The key differences are:
29 *
30 * - The reference point for all measurements which do not explicitly specify
31 *   a start time is $_SERVER['REQUEST_TIME_FLOAT'], not navigationStart.
32 * - Successive calls to mark() and measure() with the same entry name cause
33 *   the previous entry to be overwritten. This ensures that there is a 1:1
34 *   mapping between names and entries.
35 * - Because there is a 1:1 mapping, instead of getEntriesByName(), we have
36 *   getEntryByName().
37 *
38 * The in-line documentation incorporates content from the User Timing Specification
39 * https://www.w3.org/TR/user-timing/
40 * Copyright © 2013 World Wide Web Consortium, (MIT, ERCIM, Keio, Beihang).
41 * https://www.w3.org/Consortium/Legal/2015/doc-license
42 *
43 * @since 1.27
44 */
45class Timing implements LoggerAwareInterface {
46
47    /** @var array[] */
48    private $entries = [];
49
50    /** @var LoggerInterface */
51    protected $logger;
52
53    public function __construct( array $params = [] ) {
54        $this->clearMarks();
55        $this->setLogger( $params['logger'] ?? new NullLogger() );
56    }
57
58    /**
59     * Sets a logger instance on the object.
60     *
61     * @param LoggerInterface $logger
62     */
63    public function setLogger( LoggerInterface $logger ) {
64        $this->logger = $logger;
65    }
66
67    /**
68     * Store a timestamp with the associated name (a "mark")
69     *
70     * @param string $markName The name associated with the timestamp.
71     *  If there already exists an entry by that name, it is overwritten.
72     * @return array The mark that has been created.
73     */
74    public function mark( $markName ) {
75        $this->entries[$markName] = [
76            'name'      => $markName,
77            'entryType' => 'mark',
78            'startTime' => microtime( true ),
79            'duration'  => 0,
80        ];
81        return $this->entries[$markName];
82    }
83
84    /**
85     * @param string|null $markName The name of the mark that should
86     *  be cleared. If not specified, all marks will be cleared.
87     */
88    public function clearMarks( $markName = null ) {
89        if ( $markName !== null ) {
90            unset( $this->entries[$markName] );
91        } else {
92            $this->entries = [
93                'requestStart' => [
94                    'name'      => 'requestStart',
95                    'entryType' => 'mark',
96                    'startTime' => $_SERVER['REQUEST_TIME_FLOAT'],
97                    'duration'  => 0,
98                ],
99            ];
100        }
101    }
102
103    /**
104     * This method stores the duration between two marks along with
105     * the associated name (a "measure").
106     *
107     * If neither the startMark nor the endMark argument is specified,
108     * measure() will store the duration from $_SERVER['REQUEST_TIME_FLOAT'] to
109     * the current time.
110     * If the startMark argument is specified, but the endMark argument is not
111     * specified, measure() will store the duration from the most recent
112     * occurrence of the start mark to the current time.
113     * If both the startMark and endMark arguments are specified, measure()
114     * will store the duration from the most recent occurrence of the start
115     * mark to the most recent occurrence of the end mark.
116     *
117     * @param string $measureName
118     * @param string $startMark
119     * @param string|null $endMark
120     * @return array|bool The measure that has been created, or false if either
121     *  the start mark or the end mark do not exist.
122     */
123    public function measure( $measureName, $startMark = 'requestStart', $endMark = null ) {
124        $start = $this->getEntryByName( $startMark );
125        if ( $start === null ) {
126            $this->logger->error( __METHOD__ . ": The mark '$startMark' does not exist" );
127            return false;
128        }
129        $startTime = $start['startTime'];
130
131        if ( $endMark ) {
132            $end = $this->getEntryByName( $endMark );
133            if ( $end === null ) {
134                $this->logger->error( __METHOD__ . ": The mark '$endMark' does not exist" );
135                return false;
136            }
137            $endTime = $end['startTime'];
138        } else {
139            $endTime = microtime( true );
140        }
141
142        $this->entries[$measureName] = [
143            'name'      => $measureName,
144            'entryType' => 'measure',
145            'startTime' => $startTime,
146            'duration'  => $endTime - $startTime,
147        ];
148
149        return $this->entries[$measureName];
150    }
151
152    /**
153     * Sort entries in chronological order with respect to startTime.
154     */
155    private function sortEntries() {
156        uasort( $this->entries, static function ( $a, $b ) {
157            return $a['startTime'] <=> $b['startTime'];
158        } );
159    }
160
161    /**
162     * @return array[] All entries in chronological order.
163     */
164    public function getEntries() {
165        $this->sortEntries();
166        return $this->entries;
167    }
168
169    /**
170     * @param string $entryType
171     * @return array[] Entries (in chronological order) that have the same value
172     *  for the entryType attribute as the $entryType parameter.
173     */
174    public function getEntriesByType( $entryType ) {
175        $this->sortEntries();
176        $entries = [];
177        foreach ( $this->entries as $entry ) {
178            if ( $entry['entryType'] === $entryType ) {
179                $entries[] = $entry;
180            }
181        }
182        return $entries;
183    }
184
185    /**
186     * @param string $name
187     * @return array|null Entry named $name or null if it does not exist.
188     */
189    public function getEntryByName( $name ) {
190        return $this->entries[$name] ?? null;
191    }
192}