MDL-29029 move profiling to admin tools
[moodle.git] / lib / xhprof / xhprof_moodle.php
CommitLineData
6af80cae
EL
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * @package core
19 * @subpackage profiling
20 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23
24defined('MOODLE_INTERNAL') || die();
25
26// need some stuff from xhprof
27require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_lib.php');
28require_once($CFG->libdir . '/xhprof/xhprof_lib/utils/xhprof_runs.php');
29// need some stuff from moodle
30require_once($CFG->libdir.'/tablelib.php');
31
32// TODO: Change the implementation below to proper profiling class
33
34/**
35 * Returns if profiling is running, optionally setting it
36 */
37function profiling_is_running($value = null) {
38 static $running = null;
39
40 if (!is_null($value)) {
41 $running = (bool)$value;
42 }
43
44 return $running;
45}
46
47/**
48 * Returns if profiling has been saved, optionally setting it
49 */
50function profiling_is_saved($value = null) {
51 static $saved = null;
52
53 if (!is_null($value)) {
54 $saved = (bool)$value;
55 }
56
57 return $saved;
58}
59
60/**
61 * Start profiling observing all the configuration
62 */
63function profiling_start() {
64 global $CFG, $SESSION, $SCRIPT;
65
66 // If profiling isn't available, nothing to start
67 if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
68 return false;
69 }
70
71 // If profiling isn't enabled, nothing to start
c646dd26 72 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
6af80cae
EL
73 return false;
74 }
75
76 // If profiling is already running or saved, nothing to start
77 if (profiling_is_running() || profiling_is_saved()) {
78 return false;
79 }
80
c646dd26
EL
81 // Set script (from global if available, else our own)
82 $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
83
6af80cae
EL
84 // Get PGC variables
85 $check = 'PROFILEME';
86 $profileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
c646dd26 87 $profileme = $profileme && !empty($CFG->profilingallowme);
6af80cae
EL
88 $check = 'DONTPROFILEME';
89 $dontprofileme = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
c646dd26 90 $dontprofileme = $dontprofileme && !empty($CFG->profilingallowme);
6af80cae
EL
91 $check = 'PROFILEALL';
92 $profileall = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
c646dd26 93 $profileall = $profileall && !empty($CFG->profilingallowall);
6af80cae
EL
94 $check = 'PROFILEALLSTOP';
95 $profileallstop = isset($_POST[$check]) || isset($_GET[$check]) || isset($_COOKIE[$check]) ? true : false;
c646dd26 96 $profileallstop = $profileallstop && !empty($CFG->profilingallowall);
6af80cae
EL
97
98 // DONTPROFILEME detected, nothing to start
99 if ($dontprofileme) {
100 return false;
101 }
102
103 // PROFILEALLSTOP detected, clean the mark in seesion and continue
c646dd26 104 if ($profileallstop && !empty($SESSION)) {
6af80cae
EL
105 unset($SESSION->profileall);
106 }
107
108 // PROFILEALL detected, set the mark in session and continue
c646dd26 109 if ($profileall && !empty($SESSION)) {
6af80cae
EL
110 $SESSION->profileall = true;
111
112 // SESSION->profileall detected, set $profileall
113 } else if (!empty($SESSION->profileall)) {
114 $profileall = true;
115 }
116
117 // Evaluate automatic (random) profiling if necessary
118 $profileauto = false;
c646dd26 119 if (!empty($CFG->profilingautofrec)) {
6af80cae
EL
120 $profileauto = (mt_rand(1, $CFG->profilingautofrec) === 1);
121 }
122
c646dd26
EL
123 // See if the $script matches any of the included patterns
124 $included = empty($CFG->profilingincluded) ? '' : $CFG->profilingincluded;
125 $profileincluded = profiling_string_matches($script, $included);
6af80cae 126
c646dd26
EL
127 // See if the $script matches any of the excluded patterns
128 $excluded = empty($CFG->profilingexcluded) ? '' : $CFG->profilingexcluded;
129 $profileexcluded = profiling_string_matches($script, $excluded);
6af80cae
EL
130
131 // Decide if profile auto must happen (observe matchings)
132 $profileauto = $profileauto && $profileincluded && !$profileexcluded;
133
134 // Decide if profile by match must happen (only if profileauto is disabled)
c646dd26 135 $profilematch = $profileincluded && !$profileexcluded && empty($CFG->profilingautofrec);
6af80cae
EL
136
137 // If not auto, me, all, match have been detected, nothing to do
138 if (!$profileauto && !$profileme && !$profileall && !$profilematch) {
139 return false;
140 }
141
142 // Arrived here, the script is going to be profiled, let's do it
143 $ignore = array('call_user_func', 'call_user_func_array');
144 xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
145 profiling_is_running(true);
c646dd26
EL
146
147 // Started, return true
148 return true;
6af80cae
EL
149}
150
151/**
152 * Stop profiling, gathering results and storing them
153 */
154function profiling_stop() {
68fabb5c 155 global $CFG, $DB, $SCRIPT;
6af80cae
EL
156
157 // If profiling isn't available, nothing to stop
158 if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
159 return false;
160 }
161
162 // If profiling isn't enabled, nothing to stop
c646dd26 163 if (empty($CFG->profilingenabled) && empty($CFG->earlyprofilingenabled)) {
6af80cae
EL
164 return false;
165 }
166
167 // If profiling is not running or is already saved, nothing to stop
168 if (!profiling_is_running() || profiling_is_saved()) {
169 return false;
170 }
171
c646dd26
EL
172 // Set script (from global if available, else our own)
173 $script = !empty($SCRIPT) ? $SCRIPT : profiling_get_script();
174
6af80cae
EL
175 // Arrived here, profiling is running, stop and save everything
176 profiling_is_running(false);
177 $data = xhprof_disable();
178
68fabb5c
EL
179 // We only save the run after ensuring the DB table exists
180 // (this prevents problems with profiling runs enabled in
181 // config.php before Moodle is installed. Rare but...
182 $tables = $DB->get_tables();
183 if (!in_array('profiling', $tables)) {
184 return false;
185 }
186
6af80cae 187 $run = new moodle_xhprofrun();
c646dd26 188 $run->prepare_run($script);
6af80cae
EL
189 $runid = $run->save_run($data, null);
190 profiling_is_saved(true);
191
192 // Prune old runs
193 profiling_prune_old_runs($runid);
c646dd26
EL
194
195 // Finished, return true
196 return true;
6af80cae
EL
197}
198
199function profiling_prune_old_runs($exception = 0) {
200 global $CFG, $DB;
201
202 // Setting to 0 = no prune
203 if (empty($CFG->profilinglifetime)) {
204 return;
205 }
206
207 $cuttime = time() - ($CFG->profilinglifetime * 60);
208 $params = array('cuttime' => $cuttime, 'exception' => $exception);
209
210 $DB->delete_records_select('profiling', 'runreference = 0 AND
211 timecreated < :cuttime AND
212 runid != :exception', $params);
213}
214
c646dd26
EL
215/**
216 * Returns the path to the php script being requested
217 *
218 * Note this function is a partial copy of initialise_fullme() and
219 * setup_get_remote_url(), in charge of setting $FULLME, $SCRIPT and
220 * friends. To be used by early profiling runs in situations where
221 * $SCRIPT isn't defined yet
222 *
223 * @return string absolute path (wwwroot based) of the script being executed
224 */
225function profiling_get_script() {
226 global $CFG;
227
228 $wwwroot = parse_url($CFG->wwwroot);
229
230 if (!isset($wwwroot['path'])) {
231 $wwwroot['path'] = '';
232 }
233 $wwwroot['path'] .= '/';
234
235 $path = $_SERVER['SCRIPT_NAME'];
236
237 if (strpos($path, $wwwroot['path']) === 0) {
238 return substr($path, strlen($wwwroot['path']) - 1);
239 }
240 return '';
241}
242
6af80cae
EL
243function profiling_urls($report, $runid, $runid2 = null) {
244 global $CFG;
245
246 $url = '';
247 switch ($report) {
248 case 'run':
249 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run=' . $runid;
250 break;
251 case 'diff':
252 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/index.php?run1=' . $runid . '&amp;run2=' . $runid2;
253 break;
254 case 'graph':
255 $url = $CFG->wwwroot . '/lib/xhprof/xhprof_html/callgraph.php?run=' . $runid;
256 break;
257 }
258 return $url;
259}
260
261function profiling_print_run($run, $prevrunid = null) {
262 global $CFG, $OUTPUT;
263
264 $output = '';
265
266 // Prepare the runreference/runcomment form
267 $checked = $run->runreference ? ' checked=checked' : '';
268 $referenceform = "<form id=\"profiling_runreference\" action=\"index.php\" method=\"GET\">" .
269 "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\"/>".
270 "<input type=\"hidden\" name=\"runid\" value=\"$run->runid\"/>".
271 "<input type=\"hidden\" name=\"listurl\" value=\"$run->url\"/>".
272 "<input type=\"checkbox\" name=\"runreference\" value=\"1\"$checked/>&nbsp;".
273 "<input type=\"text\" name=\"runcomment\" value=\"$run->runcomment\"/>&nbsp;".
274 "<input type=\"submit\" value=\"" . get_string('savechanges') ."\"/>".
275 "</form>";
276
277 $table = new html_table();
278 $table->align = array('right', 'left');
279 $table->tablealign = 'center';
280 $table->attributes['class'] = 'profilingruntable';
281 $table->colclasses = array('label', 'value');
282 $table->data = array(
187536f6 283 array(get_string('runid', 'tool_profiling'), $run->runid),
6af80cae
EL
284 array(get_string('url'), $run->url),
285 array(get_string('date'), userdate($run->timecreated, '%d %B %Y, %H:%M')),
187536f6
PS
286 array(get_string('executiontime', 'tool_profiling'), format_float($run->totalexecutiontime / 1000, 3) . ' ms'),
287 array(get_string('cputime', 'tool_profiling'), format_float($run->totalcputime / 1000, 3) . ' ms'),
288 array(get_string('calls', 'tool_profiling'), $run->totalcalls),
289 array(get_string('memory', 'tool_profiling'), format_float($run->totalmemory / 1024, 0) . ' KB'),
290 array(get_string('markreferencerun', 'tool_profiling'), $referenceform));
6af80cae
EL
291 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary', true);
292 // Add link to details
187536f6 293 $strviewdetails = get_string('viewdetails', 'tool_profiling');
6af80cae
EL
294 $url = profiling_urls('run', $run->runid);
295 $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
296 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
297 // If there is one previous run marked as reference, add link to diff
298 if ($prevrunid) {
187536f6 299 $strviewdiff = get_string('viewdiff', 'tool_profiling');
6af80cae
EL
300 $url = 'index.php?runid=' . $run->runid . '&amp;runid2=' . $prevrunid . '&amp;listurl=' . urlencode($run->url);
301 $output.=$OUTPUT->heading('<a href="' . $url . '" title="">' . $strviewdiff . '</a>', 3, 'main profilinglink');
302 }
303
304 return $output;
305}
306
307function profiling_print_rundiff($run1, $run2) {
308 global $CFG, $OUTPUT;
309
310 $output = '';
311
312 // Prepare the reference/comment information
313 $referencetext1 = ($run1->runreference ? get_string('yes') : get_string('no')) .
314 ($run1->runcomment ? ' - ' . s($run1->runcomment) : '');
315 $referencetext2 = ($run2->runreference ? get_string('yes') : get_string('no')) .
316 ($run2->runcomment ? ' - ' . s($run2->runcomment) : '');
317
318 // Calculate global differences
319 $diffexecutiontime = profiling_get_difference($run1->totalexecutiontime, $run2->totalexecutiontime, 'ms', 1000);
320 $diffcputime = profiling_get_difference($run1->totalcputime, $run2->totalcputime, 'ms', 1000);
321 $diffcalls = profiling_get_difference($run1->totalcalls, $run2->totalcalls);
322 $diffmemory = profiling_get_difference($run1->totalmemory, $run2->totalmemory, 'KB', 1024);
323
324 $table = new html_table();
325 $table->align = array('right', 'left', 'left', 'left');
326 $table->tablealign = 'center';
327 $table->attributes['class'] = 'profilingruntable';
328 $table->colclasses = array('label', 'value1', 'value2');
329 $table->data = array(
187536f6 330 array(get_string('runid', 'tool_profiling'),
6af80cae
EL
331 '<a href="index.php?runid=' . $run1->runid . '&listurl=' . urlencode($run1->url) . '" title="">' . $run1->runid . '</a>',
332 '<a href="index.php?runid=' . $run2->runid . '&listurl=' . urlencode($run2->url) . '" title="">' . $run2->runid . '</a>'),
333 array(get_string('url'), $run1->url, $run2->url),
334 array(get_string('date'), userdate($run1->timecreated, '%d %B %Y, %H:%M'),
335 userdate($run2->timecreated, '%d %B %Y, %H:%M')),
187536f6 336 array(get_string('executiontime', 'tool_profiling'),
6af80cae
EL
337 format_float($run1->totalexecutiontime / 1000, 3) . ' ms',
338 format_float($run2->totalexecutiontime / 1000, 3) . ' ms ' . $diffexecutiontime),
187536f6 339 array(get_string('cputime', 'tool_profiling'),
6af80cae
EL
340 format_float($run1->totalcputime / 1000, 3) . ' ms',
341 format_float($run2->totalcputime / 1000, 3) . ' ms ' . $diffcputime),
187536f6
PS
342 array(get_string('calls', 'tool_profiling'), $run1->totalcalls, $run2->totalcalls . ' ' . $diffcalls),
343 array(get_string('memory', 'tool_profiling'),
6af80cae
EL
344 format_float($run1->totalmemory / 1024, 0) . ' KB',
345 format_float($run2->totalmemory / 1024, 0) . ' KB ' . $diffmemory),
187536f6 346 array(get_string('referencerun', 'tool_profiling'), $referencetext1, $referencetext2));
6af80cae
EL
347 $output = $OUTPUT->box(html_writer::table($table), 'generalbox boxwidthwide boxaligncenter profilingrunbox', 'profiling_summary', true);
348 // Add link to details
187536f6 349 $strviewdetails = get_string('viewdiffdetails', 'tool_profiling');
6af80cae 350 $url = profiling_urls('diff', $run1->runid, $run2->runid);
187536f6 351 //$url = $CFG->wwwroot . '/admin/tool/profiling/index.php?run=' . $run->runid;
6af80cae
EL
352 $output.=$OUTPUT->heading('<a href="' . $url . '" onclick="javascript:window.open(' . "'" . $url . "'" . ');' .
353 'return false;"' . ' title="">' . $strviewdetails . '</a>', 3, 'main profilinglink');
354 return $output;
355}
356
357/**
358 * Helper function that returns the HTML fragment to
359 * be displayed on listing mode, it includes actions
360 * like deletion/export/import...
361 */
362function profiling_list_controls($listurl) {
363 global $CFG, $OUTPUT;
364
365 $output = '';
366
367 return $output;
368}
369
370/**
371 * Helper function that looks for matchings of one string
372 * against an array of * wildchar patterns
373 */
374function profiling_string_matches($string, $patterns) {
375 $patterns = explode(',', $patterns);
376 foreach ($patterns as $pattern) {
377 // Trim and prepare pattern
378 $pattern = str_replace('\*', '.*', preg_quote(trim($pattern), '~'));
379 // Don't process empty patterns
380 if (empty($pattern)) {
381 continue;
382 }
383 if (preg_match('~' . $pattern . '~', $string)) {
384 return true;
385 }
386 }
387 return false;
388}
389
390/**
391 * Helper function that, given to floats, returns their numerical
392 * and percentual differences, propertly formated and cssstyled
393 */
394function profiling_get_difference($number1, $number2, $units = '', $factor = 1, $numdec = 2) {
395 $numdiff = $number2 - $number1;
396 $perdiff = 0;
397 if ($number1 != $number2) {
398 $perdiff = $number1 != 0 ? ($number2 * 100 / $number1) - 100 : 0;
399 }
400 $sign = $number2 > $number1 ? '+' : '';
401 $delta = abs($perdiff) > 0.25 ? '&Delta;' : '&asymp;';
402 $spanclass = $number2 > $number1 ? 'worse' : ($number1 > $number2 ? 'better' : 'same');
403 $importantclass= abs($perdiff) > 1 ? ' profiling_important' : '';
404 $startspan = '<span class="profiling_' . $spanclass . $importantclass . '">';
405 $endspan = '</span>';
406 $fnumdiff = $sign . format_float($numdiff / $factor, $numdec);
407 $fperdiff = $sign . format_float($perdiff, $numdec);
408 return $startspan . $delta . ' ' . $fnumdiff . ' ' . $units . ' (' . $fperdiff . '%)' . $endspan;
409}
410
411/**
412 * Custom implementation of iXHProfRuns
413 *
414 * This class is one implementation of the iXHProfRuns interface, in charge
415 * of storing and retrieve profiling run data to/from DB (profiling table)
416 *
417 * The interface only defines two methods to be defined: get_run() and
418 * save_run() we'll be implementing some more in order to keep all the
419 * rest of information in our runs properly handled.
420 */
421class moodle_xhprofrun implements iXHProfRuns {
422
423 protected $runid = null;
424 protected $url = null;
425 protected $totalexecutiontime = 0;
426 protected $totalcputime = 0;
427 protected $totalcalls = 0;
428 protected $totalmemory = 0;
429 protected $timecreated = 0;
430
431 public function __construct() {
432 $this->timecreated = time();
433 }
434
435 /**
436 * Given one runid and one type, return the run data
437 * and some extra info in run_desc from DB
438 *
439 * Note that $type is completely ignored
440 */
441 public function get_run($run_id, $type, &$run_desc) {
442 global $DB;
443
444 $rec = $DB->get_record('profiling', array('runid' => $run_id), '*', MUST_EXIST);
445
446 $this->runid = $rec->runid;
447 $this->url = $rec->url;
448 $this->totalexecutiontime = $rec->totalexecutiontime;
449 $this->totalcputime = $rec->totalcputime;
450 $this->totalcalls = $rec->totalcalls;
451 $this->totalmemory = $rec->totalmemory;
452 $this->timecreated = $rec->timecreated;
453
454 $run_desc = $this->url . ($rec->runreference ? ' (R) ' : ' ') . ' - ' . s($rec->runcomment);
455
456 return unserialize(base64_decode($rec->data));
457 }
458
459 /**
460 * Given some run data, one type and, optionally, one runid
461 * store the information in DB
462 *
463 * Note that $type is completely ignored
464 */
465 public function save_run($xhprof_data, $type, $run_id = null) {
466 global $DB;
467
468 if (is_null($this->url)) {
469 xhprof_error("Warning: You must use the prepare_run() method before saving it");
470 }
471
472 // Calculate runid if needed
473 $this->runid = is_null($run_id) ? md5($this->url . '-' . uniqid()) : $run_id;
474
475 // Calculate totals
476 $this->totalexecutiontime = $xhprof_data['main()']['wt'];
477 $this->totalcputime = $xhprof_data['main()']['cpu'];
478 $this->totalcalls = array_reduce($xhprof_data, array($this, 'sum_calls'));
479 $this->totalmemory = $xhprof_data['main()']['mu'];
480
481 // Prepare data
482 $rec = new stdClass();
483 $rec->runid = $this->runid;
484 $rec->url = $this->url;
485 $rec->data = base64_encode(serialize($xhprof_data));
486 $rec->totalexecutiontime = $this->totalexecutiontime;
487 $rec->totalcputime = $this->totalcputime;
488 $rec->totalcalls = $this->totalcalls;
489 $rec->totalmemory = $this->totalmemory;
490 $rec->timecreated = $this->timecreated;
491
492 $DB->insert_record('profiling', $rec);
493 return $this->runid;
494 }
495
496 public function prepare_run($url) {
497 $this->url = $url;
498 }
499
500 // Private API starts here
501
502 protected function sum_calls($sum, $data) {
503 return $sum + $data['ct'];
504 }
505}
506
507/**
508 * Simple subclass of {@link table_sql} that provides
509 * some custom formatters for various columns, in order
510 * to make the main profiles list nicer
511 */
512class xhprof_table_sql extends table_sql {
513
514 protected $listurlmode = false;
515
516 /**
54fb7e80 517 * Get row classes to be applied based on row contents
6af80cae 518 */
54fb7e80
EL
519 function get_row_class($row) {
520 return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
6af80cae
EL
521 }
522
523 /**
524 * Define it the table is in listurlmode or not, output will
525 * be different based on that
526 */
527 function set_listurlmode($listurlmode) {
528 $this->listurlmode = $listurlmode;
529 }
530
531 /**
532 * Format URL, so it points to last run for that url
533 */
534 protected function col_url($row) {
535 global $OUTPUT;
536
537 // Build the link to latest run for the script
187536f6 538 $scripturl = new moodle_url('/admin/tool/profiling/index.php', array('script' => $row->url, 'listurl' => $row->url));
6af80cae
EL
539 $scriptaction = $OUTPUT->action_link($scripturl, $row->url);
540
541 // Decide, based on $this->listurlmode which actions to show
542 if ($this->listurlmode) {
543 $detailsaction = '';
544 } else {
545 // Build link icon to script details (pix + url + actionlink)
187536f6
PS
546 $detailsimg = $OUTPUT->pix_icon('t/right', get_string('profilingfocusscript', 'tool_profiling', $row->url));
547 $detailsurl = new moodle_url('/admin/tool/profiling/index.php', array('listurl' => $row->url));
6af80cae
EL
548 $detailsaction = $OUTPUT->action_link($detailsurl, $detailsimg);
549 }
550
551 return $scriptaction . '&nbsp;' . $detailsaction;
552 }
553
554 /**
555 * Format profiling date, human and pointing to run
556 */
557 protected function col_timecreated($row) {
558 global $OUTPUT;
559 $fdate = userdate($row->timecreated, '%d %b %Y, %H:%M');
187536f6 560 $url = new moodle_url('/admin/tool/profiling/index.php', array('runid' => $row->runid, 'listurl' => $row->url));
6af80cae
EL
561 return $OUTPUT->action_link($url, $fdate);
562 }
563
564 /**
565 * Format execution time
566 */
567 protected function col_totalexecutiontime($row) {
568 return format_float($row->totalexecutiontime / 1000, 3) . ' ms';
569 }
570
571 /**
572 * Format cpu time
573 */
574 protected function col_totalcputime($row) {
575 return format_float($row->totalcputime / 1000, 3) . ' ms';
576 }
577
578 /**
579 * Format memory
580 */
581 protected function col_totalmemory($row) {
582 return format_float($row->totalmemory / 1024, 3) . ' KB';
583 }
584}