"/api/report" does not check commit time correctly.
[WebKit-https.git] / Websites / perf.webkit.org / public / include / report-processor.php
1 <?php
2
3 require_once('../include/json-header.php');
4
5 class ReportProcessor {
6     private $db;
7     private $name_to_aggregator_id;
8     private $report_id;
9     private $runs;
10
11     function __construct($db) {
12         $this->db = $db;
13         $this->name_to_aggregator_id = array();
14     }
15
16     private function exit_with_error($message, $details = NULL) {
17         if (!$this->report_id) {
18             $details['failureStored'] = FALSE;
19             exit_with_error($message, $details);
20         }
21
22         $details['failureStored'] = $this->db->query_and_get_affected_rows(
23             'UPDATE reports SET report_failure = $1, report_failure_details = $2 WHERE report_id = $3',
24             array($message, $details ? json_encode($details) : NULL, $this->report_id)) == 1;
25         exit_with_error($message, $details);
26     }
27
28     function process($report, $existing_report_id = NULL) {
29         $this->report_id = $existing_report_id;
30         $this->runs = NULL;
31
32         array_key_exists('builderName', $report) or $this->exit_with_error('MissingBuilderName');
33         array_key_exists('buildTime', $report) or $this->exit_with_error('MissingBuildTime');
34
35         $build_data = $this->authenticate_and_construct_build_data($report, $existing_report_id);
36         if (!$existing_report_id)
37             $this->store_report($report, $build_data);
38
39         $this->ensure_aggregators();
40
41         $this->fetch_tests();
42         $this->runs = new TestRunsGenerator($this->db, $this->name_to_aggregator_id, $this->report_id);
43         $this->recursively_ensure_tests($report['tests']);
44
45         $this->runs->aggregate();
46         $this->runs->compute_caches();
47
48         $this->db->begin_transaction() or $this->exit_with_error('FailedToBeginTransaction');
49
50         $platform_id = $this->db->select_or_insert_row('platforms', 'platform', array('name' => $report['platform']));
51         if (!$platform_id)
52             $this->rollback_with_error('FailedToInsertPlatform', array('name' => $report['platform']));
53
54         // FIXME: Deprecate and unsupport "jobId".
55         $build_id = $this->resolve_build_id($build_data, array_get($report, 'revisions', array()),
56             array_get($report, 'jobId', array_get($report, 'buildRequest')));
57
58         $this->db->commit_transaction() or $this->exit_with_error('FailedToCommitTransaction');
59
60         $this->runs->commit($platform_id, $build_id);
61     }
62
63     private function authenticate_and_construct_build_data(&$report, $existing_report_id) {
64         $builder_info = array('name' => $report['builderName']);
65         $slave_name = array_get($report, 'slaveName', NULL);
66         $slave_id = NULL;
67         if (!$existing_report_id) {
68             $hash = NULL;
69             if ($slave_name && array_key_exists('slavePassword', $report)) {
70                 $universal_password = config('universalSlavePassword');
71                 if ($slave_name && $universal_password && $universal_password == $report['slavePassword'])
72                     $slave_id = $this->db->select_or_insert_row('build_slaves', 'slave', array('name' => $slave_name));
73                 else {
74                     $hash = hash('sha256', $report['slavePassword']);
75                     $slave = $this->db->select_first_row('build_slaves', 'slave', array('name' => $slave_name, 'password_hash' => $hash));
76                     if ($slave)
77                         $slave_id = $slave['slave_id'];
78                 }
79             } else if (array_key_exists('builderPassword', $report))
80                 $hash = hash('sha256', $report['builderPassword']);
81
82             if (!$hash && !$slave_id)
83                 $this->exit_with_error('BuilderNotFound');
84             if (!$slave_id)
85                 $builder_info['password_hash'] = $hash;
86         }
87
88         if (array_key_exists('builderPassword', $report))
89             unset($report['builderPassword']);
90         if (array_key_exists('slavePassword', $report))
91             unset($report['slavePassword']);
92
93         $builder_id = NULL;
94         if ($slave_id)
95             $builder_id = $this->db->select_or_insert_row('builders', 'builder', $builder_info);
96         else {
97             $builder = $this->db->select_first_row('builders', 'builder', $builder_info);
98             if (!$builder)
99                 $this->exit_with_error('BuilderNotFound', array('name' => $builder_info['name']));
100             $builder_id = $builder['builder_id'];
101             if ($slave_name)
102                 $slave_id = $this->db->select_or_insert_row('build_slaves', 'slave', array('name' => $slave_name));
103         }
104
105         return $this->construct_build_data($report, $builder_id, $slave_id);
106     }
107
108     private function construct_build_data(&$report, $builder_id, $slave_id) {
109         array_key_exists('buildNumber', $report) or $this->exit_with_error('MissingBuildNumber');
110         array_key_exists('buildTime', $report) or $this->exit_with_error('MissingBuildTime');
111
112         return array('builder' => $builder_id, 'slave' => $slave_id, 'number' => $report['buildNumber'], 'time' => $report['buildTime']);
113     }
114
115     private function store_report(&$report, $build_data) {
116         assert(!$this->report_id);
117         $this->report_id = $this->db->insert_row('reports', 'report', array(
118             'builder' => $build_data['builder'],
119             'slave' => $build_data['slave'],
120             'build_number' => $build_data['number'],
121             'content' => json_encode($report)));
122         if (!$this->report_id)
123             $this->exit_with_error('FailedToStoreRunReport');
124     }
125
126     private function ensure_aggregators() {
127         foreach (TestRunsGenerator::$aggregators as $name) {
128             $id = $this->db->select_or_insert_row('aggregators', 'aggregator', array('name' => $name));
129             if (!$id)
130                 $this->exit_with_error('FailedToInsertAggregator', array('aggregator' => $name));
131             $this->name_to_aggregator_id[$name] = $id;
132         }
133     }
134
135     private function resolve_build_id(&$build_data, $revisions, $build_request_id) {
136         $results = $this->db->query_and_fetch_all("SELECT build_id, build_slave FROM builds
137             WHERE build_builder = $1 AND build_number = $2 AND build_time <= $3 AND build_time + interval '1 day' > $3 LIMIT 2",
138             array($build_data['builder'], $build_data['number'], $build_data['time']));
139         if ($results) {
140             $first_result = $results[0];
141             if ($first_result['build_slave'] != $build_data['slave'])
142                 $this->rollback_with_error('MismatchingBuildSlave', array('storedBuild' => $results, 'reportedBuildData' => $build_data));
143             $build_id = $first_result['build_id'];
144         } else
145             $build_id = $this->db->insert_row('builds', 'build', $build_data);
146
147         if (!$build_id)
148             $this->rollback_with_error('FailedToInsertBuild', $build_data);
149
150         if ($build_request_id) {
151             if ($this->db->update_row('build_requests', 'request', array('id' => $build_request_id, 'build' => null), array('status' => 'completed', 'build' => $build_id))
152                 != $build_request_id)
153                 $this->rollback_with_error('FailedToUpdateBuildRequest', array('buildRequest' => $build_request_id, 'build' => $build_id));
154         }
155
156
157         foreach ($revisions as $repository_name => $revision_data) {
158             $repository_id = $this->db->select_or_insert_row('repositories', 'repository', array('name' => $repository_name, 'owner' => NULL));
159             if (!$repository_id)
160                 $this->rollback_with_error('FailedToInsertRepository', array('name' => $repository_name));
161
162             $commit_data = array('repository' => $repository_id, 'revision' => $revision_data['revision'], 'time' => array_get($revision_data, 'timestamp'));
163
164             $mismatching_commit = $this->db->query_and_fetch_all('SELECT * FROM build_commits, commits
165                 WHERE build_commit = commit_id AND commit_build = $1 AND commit_repository = $2 AND commit_revision != $3 LIMIT 1',
166                 array($build_id, $repository_id, $revision_data['revision']));
167             if ($mismatching_commit)
168                 $this->rollback_with_error('MismatchingCommitRevision', array('build' => $build_id, 'existing' => $mismatching_commit, 'new' => $commit_data));
169
170             $commit_row = $this->db->select_or_insert_row('commits', 'commit',
171                 array('repository' => $repository_id, 'revision' => $revision_data['revision']), $commit_data, '*');
172             if (!$commit_row)
173                 $this->rollback_with_error('FailedToRecordCommit', $commit_data);
174
175             if ($commit_data['time'] && abs(strtotime($commit_row['commit_time']) - strtotime($commit_data['time'])) > 1.0)
176                 $this->rollback_with_error('MismatchingCommitTime', array('existing' => $commit_row, 'new' => $commit_data));
177
178             if (!$this->db->select_or_insert_row('build_commits', null,
179                 array('commit_build' => $build_id, 'build_commit' => $commit_row['commit_id']), null, '*'))
180                 $this->rollback_with_error('FailedToRelateCommitToBuild', array('commit' => $commit_row, 'build' => $build_id));
181         }
182
183         return $build_id;
184     }
185
186     private function rollback_with_error($message, $details) {
187         $this->db->rollback_transaction();
188         $this->exit_with_error($message, $details);
189     }
190
191     private function fetch_tests() {
192         $test_rows = $this->db->fetch_table('tests');
193         $tests = array();
194         $test_by_id = array();
195         foreach ($test_rows as &$test) {
196             $test_by_id[$test['test_id']] = &$test;
197             $test['metrics'] = array();
198             $parent_id = $test['test_parent'];
199             if ($parent_id == NULL)
200                 $parent_id = 0;
201             $parent_array = &array_ensure_item_has_array($tests, $parent_id);
202             $parent_array[$test['test_name']] = &$test;
203         }
204         $this->tests = &$tests;
205
206         $metric_rows = $this->db->fetch_table('test_metrics');
207         foreach ($metric_rows as &$metric) {
208             $test = &$test_by_id[$metric['metric_test']];
209             $metrics_by_name = &array_ensure_item_has_array($test['metrics'], $metric['metric_name']);
210             $metrics_by_name[$metric['metric_aggregator']] = $metric['metric_id'];
211         }
212     }
213
214     private function recursively_ensure_tests(&$tests, $parent_id = NULL, $level = 0) {
215         foreach ($tests as $test_name => $test) {
216             $test_row = array_get(array_get($this->tests, $parent_id ? $parent_id : 0, array()), $test_name);
217             if ($test_row)
218                 $test_id = intval($test_row['test_id']);
219             else {
220                 $test_id = $this->db->select_or_insert_row('tests', 'test', $parent_id ? array('name' => $test_name, 'parent' => $parent_id) : array('name' => $test_name),
221                     array('name' => $test_name, 'parent' => $parent_id, 'url' => array_get($test, 'url')));
222             }
223             if (!$test_id)
224                 $this->exit_with_error('FailedToAddTest', array('name' => $test_name, 'parent' => $parent_id));
225
226             if (array_key_exists('tests', $test))
227                 $this->recursively_ensure_tests($test['tests'], $test_id, $level + 1);
228
229             foreach (array_get($test, 'metrics', array()) as $metric_name => &$aggregators_or_config_types) {
230                 $aggregators = $this->aggregator_list_if_exists($aggregators_or_config_types);
231                 if ($aggregators) {
232                     foreach ($aggregators as $aggregator_name) {
233                         $aggregator_id = array_get($this->name_to_aggregator_id, $aggregator_name, NULL);
234                         if ($aggregator_id == NULL)
235                             $this->exit_with_error('AggregatorNotFound', array('name' => $aggregator_name));
236
237                         $metrics = $test_row ? array_get($test_row['metrics'], $metric_name) : NULL;
238                         $metric_id = $metrics ? $metrics[$aggregator_id] : NULL;
239                         if (!$metric_id) {
240                             $metric_id = $this->db->select_or_insert_row('test_metrics', 'metric', array('name' => $metric_name,
241                                 'test' => $test_id, 'aggregator' => $this->name_to_aggregator_id[$aggregator_name]));
242                         }
243                         if (!$metric_id)
244                             $this->exit_with_error('FailedToAddAggregatedMetric', array('name' => $metric_name, 'test' => $test_id, 'aggregator' => $aggregator_name));
245
246                         $this->runs->add_aggregated_metric($parent_id, $test_id, $test_name, $metric_id, $metric_name, $aggregator_name, $level);
247                     }
248                 } else {
249                     $metrics = $test_row ? array_get($test_row['metrics'], $metric_name) : NULL;
250                     $metric_id = $metrics ? array_get($metrics, '') : NULL;
251                     if (!$metric_id)
252                         $metric_id = $this->db->select_or_insert_row('test_metrics', 'metric', array('name' => $metric_name, 'test' => $test_id));
253                     if (!$metric_id)
254                         $this->exit_with_error('FailedToAddMetric', array('name' => $metric_name, 'test' => $test_id));
255
256                     foreach ($aggregators_or_config_types as $config_type => &$values) {
257                         // Some tests submit groups of iterations; e.g. [[1, 2, 3, 4], [5, 6, 7, 8]]
258                         // Convert other tests to this format to simplify the computation later.
259                         if (gettype($values) !== 'array')
260                             $values = array($values);
261                         if (gettype($values[0]) !== 'array')
262                             $values = array($values);
263                         $this->runs->add_values_to_commit($metric_id, $config_type, $values);
264                         $this->runs->add_values_for_aggregation($parent_id, $test_name, $metric_name, $config_type, $values);
265                     }
266                 }
267             }
268         }
269     }
270
271     private function aggregator_list_if_exists(&$aggregators_or_config_types) {
272         if (array_key_exists(0, $aggregators_or_config_types))
273             return $aggregators_or_config_types;
274         else if (array_get($aggregators_or_config_types, 'aggregators'))
275             return $aggregators_or_config_types['aggregators'];
276         return NULL;
277     }
278 };
279
280 class TestRunsGenerator {
281     private $db;
282     private $name_to_aggregator_id;
283     private $report_id;
284     private $metrics_to_aggregate;
285     private $parent_to_values;
286     private $values_to_commit;
287
288     function __construct($db, $name_to_aggregator_id, $report_id) {
289         $this->db = $db;
290         $this->name_to_aggregator_id = $name_to_aggregator_id;
291         $this->report_id = $report_id;
292         $this->metrics_to_aggregate = array();
293         $this->parent_to_values = array();
294         $this->values_to_commit = array();
295     }
296
297     private function exit_with_error($message, $details = NULL) {
298         $details['failureStored'] = $this->db->query_and_get_affected_rows(
299             'UPDATE reports SET report_failure = $1, report_failure_details = $2 WHERE report_id = $3',
300             array($message, $details ? json_encode($details) : NULL, $this->report_id)) == 1;
301         exit_with_error($message, $details);
302     }
303
304     function add_aggregated_metric($parent_id, $test_id, $test_name, $metric_id, $metric_name, $aggregator_name, $level) {
305         array_push($this->metrics_to_aggregate, array(
306             'test_id' => $test_id,
307             'parent_id' => $parent_id,
308             'metric_id' => $metric_id,
309             'test_name' => $test_name,
310             'metric_name' => $metric_name,
311             'aggregator' => $aggregator_name,
312             'level' => $level));
313     }
314
315     function add_values_for_aggregation($parent_id, $test_name, $metric_name, $config_type, $values, $aggregator = NULL) {
316         $value_list = &array_ensure_item_has_array(array_ensure_item_has_array(array_ensure_item_has_array(array_ensure_item_has_array(
317             $this->parent_to_values, strval($parent_id)), $metric_name), $config_type), $test_name);
318         array_push($value_list, array('aggregator' => $aggregator, 'values' => $values));
319     }
320
321     function aggregate() {
322         $expressions = array();
323         foreach ($this->metrics_to_aggregate as $test_metric) {
324             $configurations = array_get(array_get($this->parent_to_values, strval($test_metric['test_id']), array()), $test_metric['metric_name']);
325             foreach ($configurations as $config_type => $test_value_list) {
326                 // FIXME: We should preserve the test order. For that, we need a new column in our database.
327                 $values_by_iteration = $this->test_value_list_to_values_by_iterations($test_value_list, $test_metric, $test_metric['aggregator']);
328                 $flattened_aggregated_values = array();
329                 for ($i = 0; $i < count($values_by_iteration['values']); ++$i)
330                     array_push($flattened_aggregated_values, $this->aggregate_values($test_metric['aggregator'], $values_by_iteration['values'][$i]));
331
332                 $grouped_values = array();
333                 foreach ($values_by_iteration['group_sizes'] as $size) {
334                     $new_group = array();
335                     for ($i = 0; $i < $size; ++$i)
336                         array_push($new_group, array_shift($flattened_aggregated_values));
337                     array_push($grouped_values, $new_group);
338                 }
339
340                 $this->add_values_to_commit($test_metric['metric_id'], $config_type, $grouped_values);
341                 $this->add_values_for_aggregation($test_metric['parent_id'], $test_metric['test_name'], $test_metric['metric_name'],
342                     $config_type, $grouped_values, $test_metric['aggregator']);
343             }
344         }
345     }
346
347     private function test_value_list_to_values_by_iterations($test_value_list, $test_metric, $aggregator) {
348         $values_by_iterations = array();
349         $group_sizes = array();
350         $first_test = TRUE;
351         foreach ($test_value_list as $test_name => $aggregators_and_values) {
352             if (count($aggregators_and_values) == 1) // Either the subtest has exactly one aggregator or is raw value (not aggregated)
353                 $values = $aggregators_and_values[0]['values'];
354             else {
355                 $values = NULL;
356                 // Find the values of the subtest aggregated by the same aggregator.
357                 foreach ($aggregators_and_values as $aggregator_and_values) {
358                     if ($aggregator_and_values['aggregator'] == $aggregator) {
359                         $values = $aggregator_and_values['values'];
360                         break;
361                     }
362                 }
363                 if (!$values) {
364                     $this->exit_with_error('NoMatchingAggregatedValueInSubtest',
365                         array('parent' => $test_metric['test_id'],
366                         'metric' => $test_metric['metric_name'],
367                         'childTest' => $test_name,
368                         'aggregator' => $aggregator,
369                         'aggregatorAndValues' => $aggregators_and_values));
370                 }
371             }
372
373             for ($group = 0; $group < count($values); ++$group) {
374                 if ($first_test) {
375                     array_push($group_sizes, count($values[$group]));
376                     for ($i = 0; $i < count($values[$group]); ++$i)
377                         array_push($values_by_iterations, array());
378                 }
379
380                 if ($group_sizes[$group] != count($values[$group])) {
381                     $this->exit_with_error('IterationGroupSizeIsInconsistent',
382                         array('parent' => $test_metric['test_id'],
383                         'metric' => $test_metric['metric_name'],
384                         'childTest' => $test_name,
385                         'groupSizes' => $group_sizes,
386                         'group' => $group,
387                         'values' => $values));
388                 }
389             }
390             $first_test = FALSE;
391
392             if (count($values) != count($group_sizes)) {
393                 // FIXME: We should support bootstrapping or just computing the mean in this case.
394                 $this->exit_with_error('IterationGroupCountIsInconsistent', array('parent' => $test_metric['test_id'],
395                     'metric' => $test_metric['metric_name'], 'childTest' => $test_name,
396                     'valuesByIterations' => $values_by_iterations, 'values' => $values));
397             }
398
399             $flattened_iteration_index = 0;
400             for ($group = 0; $group < count($values); ++$group) {
401                 for ($i = 0; $i < count($values[$group]); ++$i) {
402                     $run_iteration_value = $values[$group][$i];
403                     if (!is_numeric($run_iteration_value)) {
404                         $this->exit_with_error('NonNumeralIterationValueForAggregation', array('parent' => $test_metric['test_id'],
405                             'metric' => $test_metric['metric_name'], 'childTest' => $test_name,
406                             'valuesByIterations' => $values_by_iterations, 'values' => $values, 'index' => $i));
407                     }
408                     array_push($values_by_iterations[$flattened_iteration_index], $run_iteration_value);
409                     $flattened_iteration_index++;
410                 }
411             }
412         }
413
414         if (!$values_by_iterations)
415             $this->exit_with_error('NoIterationToAggregation', array('parent' => $test_metric['test_id'], 'metric' => $test_metric['metric_name']));
416
417         return array('values' => $values_by_iterations, 'group_sizes' => $group_sizes);
418     }
419
420     static public $aggregators = array('Arithmetic', 'Geometric', 'Harmonic', 'Total');
421
422     private function aggregate_values($aggregator, $values) {
423         switch ($aggregator) {
424         case 'Arithmetic':
425             return array_sum($values) / count($values);
426         case 'Geometric':
427             return exp(array_sum(array_map(function ($x) { return log($x); }, $values)) / count($values));
428         case 'Harmonic':
429             return count($values) / array_sum(array_map(function ($x) { return 1 / $x; }, $values));
430         case 'Total':
431             return array_sum($values);
432         case 'SquareSum': # This aggregator is only used internally to compute run_square_sum_cache in test_runs table.
433             return array_sum(array_map(function ($x) { return $x * $x; }, $values));
434         default:
435             $this->exit_with_error('UnknownAggregator', array('aggregator' => $aggregator));
436         }
437         return NULL;
438     }
439
440     function compute_caches() {
441         $expressions = array();
442         $size = count($this->values_to_commit);
443         for ($i = 0; $i < $size; ++$i) {
444             $flattened_value = array();
445             foreach ($this->values_to_commit[$i]['values'] as $group) {
446                 for ($j = 0; $j < count($group); ++$j) {
447                     $iteration_value = $group[$j];
448                     if (gettype($iteration_value) === 'array') { // [relative time, value]
449                         if (count($iteration_value) != 2) {
450                             // FIXME: Also report test and metric.
451                             $this->exit_with_error('InvalidIterationValueFormat', array('values' => $this->values_to_commit[$i]['values']));
452                         }
453                         $iteration_value = $iteration_value[1];
454                     }
455                     array_push($flattened_value, $iteration_value);
456                 }
457             }
458             $this->values_to_commit[$i]['mean'] = $this->aggregate_values('Arithmetic', $flattened_value);
459             $this->values_to_commit[$i]['sum'] = $this->aggregate_values('Total', $flattened_value);
460             $this->values_to_commit[$i]['square_sum'] = $this->aggregate_values('SquareSum', $flattened_value);
461         }
462     }
463
464     function add_values_to_commit($metric_id, $config_type, $values) {
465         array_push($this->values_to_commit, array('metric_id' => $metric_id, 'config_type' => $config_type, 'values' => $values));
466     }
467
468     function commit($platform_id, $build_id) {
469         $this->db->begin_transaction() or $this->exit_with_error('FailedToBeginTransaction');
470
471         foreach ($this->values_to_commit as $item) {
472             $config_data = array('metric' => $item['metric_id'], 'type' => $item['config_type'], 'platform' => $platform_id);
473             $config_id = $this->db->select_or_insert_row('test_configurations', 'config', $config_data);
474             if (!$config_id)
475                 $this->rollback_with_error('FailedToObtainConfiguration', $config_data);
476
477             $values = $item['values'];
478             $total_count = 0;
479             for ($group = 0; $group < count($values); ++$group)
480                 $total_count += count($values[$group]);
481             $run_data = array('config' => $config_id, 'build' => $build_id, 'iteration_count_cache' => $total_count,
482                 'mean_cache' => $item['mean'], 'sum_cache' => $item['sum'], 'square_sum_cache' => $item['square_sum']);
483             $run_id = $this->db->insert_row('test_runs', 'run', $run_data);
484             if (!$run_id)
485                 $this->rollback_with_error('FailedToInsertRun', array('metric' => $item['metric_id'], 'param' => $run_data));
486
487             $flattened_order = 0;
488             for ($group = 0; $group < count($values); ++$group) {
489                 for ($i = 0; $i < count($values[$group]); ++$i) {
490                     $iteration_value = $values[$group][$i];
491                     $relative_time = NULL;
492                     if (gettype($iteration_value) === 'array') {
493                         assert(count($iteration_value) == 2); // compute_caches checks this condition.
494                         $relative_time = $iteration_value[0];
495                         $iteration_value = $iteration_value[1];
496                     }
497                     $param = array('run' => $run_id, 'order' => $flattened_order, 'value' => $iteration_value,
498                         'group' => count($values) == 1 ? NULL : $group, 'relative_time' => $relative_time);
499                     $this->db->insert_row('run_iterations', 'iteration', $param, NULL)
500                         or $this->rollback_with_error('FailedToInsertIteration', array('config' => $config_id, 'build' => $build_id, 'param' => $param));
501                     $flattened_order++;
502                 }
503             }
504         }
505
506         $this->db->query_and_get_affected_rows("UPDATE reports
507             SET (report_committed_at, report_build) = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC', $2)
508             WHERE report_id = $1", array($this->report_id, $build_id));
509
510         $this->db->commit_transaction() or $this->exit_with_error('FailedToCommitTransaction');
511     }
512
513     private function rollback_with_error($message, $details) {
514         $this->db->rollback_transaction();
515         $this->exit_with_error($message, $details);
516     }
517 };
518
519 ?>