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