1<?php
2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
3
4/**
5 * Icinga\Application\Benchmark class
6 */
7namespace Icinga\Application;
8
9use Icinga\Util\Format;
10
11/**
12 * This class provides a simple and lightweight benchmark class
13 *
14 * <code>
15 * Benchmark::measure('Program started');
16 * // ...do something...
17 * Benchmark::measure('Task finieshed');
18 * Benchmark::dump();
19 * </code>
20 */
21class Benchmark
22{
23    const TIME   = 0x01;
24    const MEMORY = 0x02;
25
26    protected static $instance;
27    protected $start;
28    protected $measures = array();
29
30    /**
31     * Add a measurement to your benchmark
32     *
33     * The same identifier can also be used multiple times
34     *
35     * @param  string  A comment identifying the current measurement
36     * @return void
37     */
38    public static function measure($message)
39    {
40        self::getInstance()->measures[] = (object) array(
41            'timestamp'   => microtime(true),
42            'memory_real' => memory_get_usage(true),
43            'memory'      => memory_get_usage(),
44            'message'     => $message
45        );
46    }
47
48    /**
49     * Throws all measurements away
50     *
51     * This empties your measurement table and allows you to restart your
52     * benchmark from scratch
53     *
54     * @return void
55     */
56    public static function reset()
57    {
58        self::$instance = null;
59    }
60
61    /**
62     * Rerieve benchmark start time
63     *
64     * This will give you the timestamp of your first measurement
65     *
66     * @return float
67     */
68    public static function getStartTime()
69    {
70        return self::getInstance()->start;
71    }
72
73    /**
74     * Dump benchmark data
75     *
76     * Will dump a text table if running on CLI and a simple HTML table
77     * otherwise. Use Benchmark::TIME and Benchmark::MEMORY to choose whether
78     * you prefer to show either time or memory or both in your output
79     *
80     * @param  int   Whether to get time and/or memory summary
81     * @return string
82     */
83    public static function dump($what = null)
84    {
85        if (Icinga::app()->isCli()) {
86            echo self::renderToText($what);
87        } else {
88            echo self::renderToHtml($what);
89        }
90    }
91
92    /**
93     * Render benchmark data to a simple text table
94     *
95     * Use Benchmark::TIME and Icinga::MEMORY to choose whether you prefer to
96     * show either time or memory or both in your output
97     *
98     * @param  int   Whether to get time and/or memory summary
99     * @return string
100     */
101    public static function renderToText($what = null)
102    {
103        $data = self::prepareDataForRendering($what);
104        $sep = '+';
105        $title = '|';
106        foreach ($data->columns as & $col) {
107            $col->format = ' %'
108                   . ($col->align === 'right' ? '' : '-')
109                   . $col->maxlen . 's |';
110
111            $sep   .= str_repeat('-', $col->maxlen) . '--+';
112            $title .= sprintf($col->format, $col->title);
113        }
114
115        $out = $sep . "\n" . $title . "\n" . $sep . "\n";
116        foreach ($data->rows as & $row) {
117            $r = '|';
118            foreach ($data->columns as $key => & $col) {
119                $r .= sprintf($col->format, $row[$key]);
120            }
121            $out .= $r . "\n";
122        }
123
124        $out .= $sep . "\n";
125        return $out;
126    }
127
128    /**
129     * Render benchmark data to a simple HTML table
130     *
131     * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer
132     * to show either time or memory or both in your output
133     *
134     * @param  int   Whether to get time and/or memory summary
135     * @return string
136     */
137    public static function renderToHtml($what = null)
138    {
139        $data = self::prepareDataForRendering($what);
140
141        // TODO: Move formatting to CSS file
142        $html = '<table class="benchmark">' . "\n" . '<tr>';
143        foreach ($data->columns as & $col) {
144            if ($col->title === 'Time') {
145                continue;
146            }
147            $html .= sprintf(
148                '<td align="%s">%s</td>',
149                $col->align,
150                htmlspecialchars($col->title)
151            );
152        }
153        $html .= "</tr>\n";
154
155        foreach ($data->rows as & $row) {
156            $html .= '<tr>';
157            foreach ($data->columns as $key => & $col) {
158                if ($col->title === 'Time') {
159                    continue;
160                }
161                $html .= sprintf(
162                    '<td align="%s">%s</td>',
163                    $col->align,
164                    $row[$key]
165                );
166            }
167            $html .= "</tr>\n";
168        }
169        $html .= "</table>\n";
170        return $html;
171    }
172
173    /**
174     * Prepares benchmark data for output
175     *
176     * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer
177     * to have either time or memory or both in your output
178     *
179     * @param  int   Whether to get time and/or memory summary
180     * @return array
181     */
182    protected static function prepareDataForRendering($what = null)
183    {
184        if ($what === null) {
185            $what = self::TIME | self::MEMORY;
186        }
187
188        $columns = array(
189            (object) array(
190                'title'  => 'Time',
191                'align'  => 'left',
192                'maxlen' => 4
193            ),
194            (object) array(
195                'title'  => 'Description',
196                'align'  => 'left',
197                'maxlen' => 11
198            )
199        );
200        if ($what & self::TIME) {
201            $columns[] = (object) array(
202                'title'  => 'Off (ms)',
203                'align'  => 'right',
204                'maxlen' => 11
205            );
206            $columns[] = (object) array(
207                'title'  => 'Dur (ms)',
208                'align'  => 'right',
209                'maxlen' => 13
210            );
211        }
212        if ($what & self::MEMORY) {
213            $columns[] = (object) array(
214                'title'  => 'Mem (diff)',
215                'align'  => 'right',
216                'maxlen' => 10
217            );
218            $columns[] = (object) array(
219                'title'  => 'Mem (total)',
220                'align'  => 'right',
221                'maxlen' => 11
222            );
223        }
224
225        $bench = self::getInstance();
226        $last = $bench->start;
227        $rows = array();
228        $lastmem = 0;
229        foreach ($bench->measures as $m) {
230            $micro = sprintf(
231                '%03d',
232                round(($m->timestamp - floor($m->timestamp)) * 1000)
233            );
234            $vals = array(
235                date('H:i:s', $m->timestamp) . '.' . $micro,
236                $m->message
237            );
238
239            if ($what & self::TIME) {
240                $m->relative = $m->timestamp - $bench->start;
241                $m->offset   = $m->timestamp - $last;
242                $last = $m->timestamp;
243                $vals[] = sprintf('%0.3f', $m->relative * 1000);
244                $vals[] = sprintf('%0.3f', $m->offset * 1000);
245            }
246
247            if ($what & self::MEMORY) {
248                $mem = $m->memory - $lastmem;
249                $lastmem = $m->memory;
250                $vals[] = Format::bytes($mem);
251                $vals[] = Format::bytes($m->memory);
252            }
253
254            $row = & $rows[];
255            foreach ($vals as $col => $val) {
256                $row[$col] = $val;
257                $columns[$col]->maxlen = max(
258                    strlen($val),
259                    $columns[$col]->maxlen
260                );
261            }
262        }
263
264        return (object) array(
265            'columns' => $columns,
266            'rows' => $rows
267        );
268    }
269
270    /**
271     * Singleton
272     *
273     * Benchmark is run only once, but you are not allowed to directly access
274     * the getInstance() method
275     *
276     * @return self
277     */
278    protected static function getInstance()
279    {
280        if (self::$instance === null) {
281            self::$instance = new Benchmark();
282            self::$instance->start = microtime(true);
283        }
284
285        return self::$instance;
286    }
287
288    /**
289     * Constructor
290     *
291     * Singleton usage is enforced, the only way to instantiate Benchmark is by
292     * starting your measurements
293     *
294     * @return void
295     */
296    protected function __construct()
297    {
298    }
299}
300