3 require_once('../include/json-header.php');
5 class ReportProcessor {
7 private $name_to_aggregator;
11 function __construct($db) {
13 $this->name_to_aggregator = array();
14 $aggregator_table = $db->fetch_table('aggregators');
15 if ($aggregator_table) {
16 foreach ($aggregator_table as $aggregator_row) {
17 $this->name_to_aggregator[$aggregator_row['aggregator_name']] = $aggregator_row;
22 private function exit_with_error($message, $details = NULL) {
23 if (!$this->report_id) {
24 $details['failureStored'] = FALSE;
25 exit_with_error($message, $details);
28 $details['failureStored'] = $this->db->query_and_get_affected_rows(
29 'UPDATE reports SET report_failure = $1, report_failure_details = $2 WHERE report_id = $3',
30 array($message, $details ? json_encode($details) : NULL, $this->report_id)) == 1;
31 exit_with_error($message, $details);
34 function process($report, $existing_report_id = NULL) {
35 $this->report_id = $existing_report_id;
38 array_key_exists('builderName', $report) or $this->exit_with_error('MissingBuilderName');
39 array_key_exists('buildTime', $report) or $this->exit_with_error('MissingBuildTime');
41 $builder_info = array('name' => $report['builderName']);
42 if (!$existing_report_id)
43 $builder_info['password_hash'] = hash('sha256', $report['builderPassword']);
44 if (array_key_exists('builderPassword', $report))
45 unset($report['builderPassword']);
47 $matched_builder = $this->db->select_first_row('builders', 'builder', $builder_info);
48 if (!$matched_builder)
49 $this->exit_with_error('BuilderNotFound', array('name' => $builder_info['name']));
51 $build_data = $this->construct_build_data($report, $matched_builder);
52 if (!$existing_report_id)
53 $this->store_report($report, $build_data);
55 $this->runs = new TestRunsGenerator($this->db, $this->name_to_aggregator, $this->report_id);
56 $this->recursively_ensure_tests($report['tests']);
58 $this->runs->aggregate();
59 $this->runs->compute_caches();
61 $platform_id = $this->db->select_or_insert_row('platforms', 'platform', array('name' => $report['platform']));
63 $this->exit_with_error('FailedToInsertPlatform', array('name' => $report['platform']));
65 $build_id = $this->resolve_build_id($build_data, array_get($report, 'revisions', array()));
67 $this->runs->commit($platform_id, $build_id);
70 private function construct_build_data($report, $builder) {
71 array_key_exists('buildNumber', $report) or $this->exit_with_error('MissingBuildNumber');
72 array_key_exists('buildTime', $report) or $this->exit_with_error('MissingBuildTime');
74 return array('builder' => $builder['builder_id'], 'number' => $report['buildNumber'], 'time' => $report['buildTime']);
77 private function store_report($report, $build_data) {
78 assert(!$this->report_id);
79 $this->report_id = $this->db->insert_row('reports', 'report', array('builder' => $build_data['builder'], 'build_number' => $build_data['number'],
80 'content' => json_encode($report)));
81 if (!$this->report_id)
82 $this->exit_with_error('FailedToStoreRunReport');
85 private function resolve_build_id($build_data, $revisions) {
86 // FIXME: This code has a race condition. See <rdar://problem/15876303>.
87 $results = $this->db->query_and_fetch_all("SELECT build_id FROM builds WHERE build_builder = $1 AND build_number = $2 AND build_time <= $3 AND build_time + interval '1 day' > $3",
88 array($build_data['builder'], $build_data['number'], $build_data['time']));
90 $build_id = $results[0]['build_id'];
92 $build_id = $this->db->insert_row('builds', 'build', $build_data);
94 $this->exit_with_error('FailedToInsertBuild', $build_data);
96 foreach ($revisions as $repository_name => $revision_data) {
97 $repository_id = $this->db->select_or_insert_row('repositories', 'repository', array('name' => $repository_name));
99 $this->exit_with_error('FailedToInsertRepository', array('name' => $repository_name));
101 $revision_data = array('repository' => $repository_id, 'build' => $build_id, 'value' => $revision_data['revision'],
102 'time' => array_get($revision_data, 'timestamp'));
103 $revision_row = $this->db->select_or_insert_row('build_revisions', 'revision', array('repository' => $repository_id, 'build' => $build_id), $revision_data, '*');
105 $this->exit_with_error('FailedToInsertRevision', $revision_data);
106 if ($revision_row['revision_value'] != $revision_data['value'])
107 $this->exit_with_error('MismatchingRevisionData', array('existing' => $revision_row, 'new' => $revision_data));
113 private function recursively_ensure_tests($tests, $parent_id = NULL, $level = 0) {
114 foreach ($tests as $test_name => $test) {
115 $test_id = $this->db->select_or_insert_row('tests', 'test', $parent_id ? array('name' => $test_name, 'parent' => $parent_id) : array('name' => $test_name),
116 array('name' => $test_name, 'parent' => $parent_id, 'url' => array_get($test, 'url')));
118 $this->exit_with_error('FailedToAddTest', array('name' => $test_name, 'parent' => $parent_id));
120 if (array_key_exists('tests', $test))
121 $this->recursively_ensure_tests($test['tests'], $test_id, $level + 1);
123 foreach (array_get($test, 'metrics', array()) as $metric_name => $aggregators_or_config_types) {
124 $aggregators = $this->aggregator_list_if_exists($aggregators_or_config_types);
126 foreach ($aggregators as $aggregator_name)
127 $this->runs->add_aggregated_metric($parent_id, $test_id, $test_name, $metric_name, $aggregator_name, $level);
129 $metric_id = $this->db->select_or_insert_row('test_metrics', 'metric', array('name' => $metric_name, 'test' => $test_id));
131 $this->exit_with_error('FailedToAddMetric', array('name' => $metric_name, 'test' => $test_id));
133 foreach ($aggregators_or_config_types as $config_type => $values) {
134 // Some tests submit groups of iterations; e.g. [[1, 2, 3, 4], [5, 6, 7, 8]]
135 // Convert other tests to this format to simplify the computation later.
136 if (gettype($values) !== 'array')
137 $values = array($values);
138 if (gettype($values[0]) !== 'array')
139 $values = array($values);
140 $this->runs->add_values_to_commit($metric_id, $config_type, $values);
141 $this->runs->add_values_for_aggregation($parent_id, $test_name, $metric_name, $config_type, $values);
148 private function aggregator_list_if_exists($aggregators_or_config_types) {
149 if (array_key_exists(0, $aggregators_or_config_types))
150 return $aggregators_or_config_types;
151 else if (array_get($aggregators_or_config_types, 'aggregators'))
152 return $aggregators_or_config_types['aggregators'];
157 class TestRunsGenerator {
159 private $name_to_aggregator;
161 private $metrics_to_aggregate;
162 private $parent_to_values;
163 private $values_to_commit;
165 function __construct($db, $name_to_aggregator, $report_id) {
167 $this->name_to_aggregator = $name_to_aggregator or array();
168 $this->report_id = $report_id;
169 $this->metrics_to_aggregate = array();
170 $this->parent_to_values = array();
171 $this->values_to_commit = array();
174 private function exit_with_error($message, $details = NULL) {
175 $details['failureStored'] = $this->db->query_and_get_affected_rows(
176 'UPDATE reports SET report_failure = $1, report_failure_details = $2 WHERE report_id = $3',
177 array($message, $details ? json_encode($details) : NULL, $this->report_id)) == 1;
178 exit_with_error($message, $details);
181 function add_aggregated_metric($parent_id, $test_id, $test_name, $metric_name, $aggregator_name, $level) {
182 array_key_exists($aggregator_name, $this->name_to_aggregator) or $this->exit_with_error('AggregatorNotFound', array('name' => $aggregator_name));
184 $metric_id = $this->db->select_or_insert_row('test_metrics', 'metric', array('name' => $metric_name,
185 'test' => $test_id, 'aggregator' => $this->name_to_aggregator[$aggregator_name]['aggregator_id']));
187 $this->exit_with_error('FailedToAddAggregatedMetric', array('name' => $metric_name, 'test' => $test_id, 'aggregator' => $aggregator_name));
189 array_push($this->metrics_to_aggregate, array(
190 'test_id' => $test_id,
191 'parent_id' => $parent_id,
192 'metric_id' => $metric_id,
193 'test_name' => $test_name,
194 'metric_name' => $metric_name,
195 'aggregator' => $aggregator_name,
196 'aggregator_definition' => $this->name_to_aggregator[$aggregator_name]['aggregator_definition'],
200 function add_values_for_aggregation($parent_id, $test_name, $metric_name, $config_type, $values, $aggregator = NULL) {
201 $value_list = &array_ensure_item_has_array(array_ensure_item_has_array(array_ensure_item_has_array(array_ensure_item_has_array(
202 $this->parent_to_values, strval($parent_id)), $metric_name), $config_type), $test_name);
203 array_push($value_list, array('aggregator' => $aggregator, 'values' => $values));
206 function aggregate() {
207 $expressions = array();
208 foreach ($this->metrics_to_aggregate as $test_metric) {
209 $configurations = array_get(array_get($this->parent_to_values, strval($test_metric['test_id']), array()), $test_metric['metric_name']);
210 foreach ($configurations as $config_type => $test_value_list) {
211 // FIXME: We should preserve the test order. For that, we need a new column in our database.
212 $values_by_iteration = $this->test_value_list_to_values_by_iterations($test_value_list, $test_metric, $test_metric['aggregator']);
213 $flattened_aggregated_values = array();
214 for ($i = 0; $i < count($values_by_iteration['values']); ++$i)
215 array_push($flattened_aggregated_values, $this->aggregate_values($test_metric['aggregator'], $values_by_iteration['values'][$i]));
217 $grouped_values = array();
218 foreach ($values_by_iteration['group_sizes'] as $size) {
219 $new_group = array();
220 for ($i = 0; $i < $size; ++$i)
221 array_push($new_group, array_shift($flattened_aggregated_values));
222 array_push($grouped_values, $new_group);
225 $this->add_values_to_commit($test_metric['metric_id'], $config_type, $grouped_values);
226 $this->add_values_for_aggregation($test_metric['parent_id'], $test_metric['test_name'], $test_metric['metric_name'],
227 $config_type, $grouped_values, $test_metric['aggregator']);
232 private function test_value_list_to_values_by_iterations($test_value_list, $test_metric, $aggregator) {
233 $values_by_iterations = array();
234 $group_sizes = array();
236 foreach ($test_value_list as $test_name => $aggregators_and_values) {
237 if (count($aggregators_and_values) == 1) // Either the subtest has exactly one aggregator or is raw value (not aggregated)
238 $values = $aggregators_and_values[0]['values'];
241 // Find the values of the subtest aggregated by the same aggregator.
242 foreach ($aggregators_and_values as $aggregator_and_values) {
243 if ($aggregator_and_values['aggregator'] == $aggregator) {
244 $values = $aggregator_and_values['values'];
249 $this->exit_with_error('NoMatchingAggregatedValueInSubtest',
250 array('parent' => $test_metric['test_id'],
251 'metric' => $test_metric['metric_name'],
252 'childTest' => $test_name,
253 'aggregator' => $aggregator,
254 'aggregatorAndValues' => $aggregators_and_values));
258 for ($group = 0; $group < count($values); ++$group) {
260 array_push($group_sizes, count($values[$group]));
261 for ($i = 0; $i < count($values[$group]); ++$i)
262 array_push($values_by_iterations, array());
265 if ($group_sizes[$group] != count($values[$group])) {
266 $this->exit_with_error('IterationGroupSizeIsInconsistent',
267 array('parent' => $test_metric['test_id'],
268 'metric' => $test_metric['metric_name'],
269 'childTest' => $test_name,
270 'groupSizes' => $group_sizes,
272 'values' => $values));
277 if (count($values) != count($group_sizes)) {
278 // FIXME: We should support bootstrapping or just computing the mean in this case.
279 $this->exit_with_error('IterationGroupCountIsInconsistent', array('parent' => $test_metric['test_id'],
280 'metric' => $test_metric['metric_name'], 'childTest' => $name_and_values['name'],
281 'valuesByIterations' => $values_by_iterations, 'values' => $values));
284 $flattened_iteration_index = 0;
285 for ($group = 0; $group < count($values); ++$group) {
286 for ($i = 0; $i < count($values[$group]); ++$i) {
287 $run_iteration_value = $values[$group][$i];
288 if (!is_numeric($run_iteration_value)) {
289 $this->exit_with_error('NonNumeralIterationValueForAggregation', array('parent' => $test_metric['test_id'],
290 'metric' => $test_metric['metric_name'], 'childTest' => $name_and_values['name'],
291 'valuesByIterations' => $values_by_iterations, 'values' => $values, 'index' => $i));
293 array_push($values_by_iterations[$flattened_iteration_index], $run_iteration_value);
294 $flattened_iteration_index++;
299 if (!$values_by_iterations)
300 $this->exit_with_error('NoIterationToAggregation', array('parent' => $test_metric['test_id'], 'metric' => $test_metric['metric_name']));
302 return array('values' => $values_by_iterations, 'group_sizes' => $group_sizes);
305 private function aggregate_values($aggregator, $values) {
306 switch ($aggregator) {
308 return array_sum($values) / count($values);
310 return pow(array_product($values), 1 / count($values));
312 return count($values) / array_sum(array_map(function ($x) { return 1 / $x; }, $values));
314 return array_sum($values);
316 return array_sum(array_map(function ($x) { return $x * $x; }, $values));
318 $this->exit_with_error('UnknownAggregator', array('aggregator' => $aggregator));
323 function compute_caches() {
324 $expressions = array();
325 $size = count($this->values_to_commit);
326 for ($i = 0; $i < $size; ++$i) {
327 $flattened_value = array();
328 foreach ($this->values_to_commit[$i]['values'] as $group) {
329 for ($j = 0; $j < count($group); ++$j) {
330 $iteration_value = $group[$j];
331 if (gettype($iteration_value) === 'array') { // [relative time, value]
332 if (count($iteration_value) != 2) {
333 // FIXME: Also report test and metric.
334 $this->exit_with_error('InvalidIterationValueFormat', array('values' => $this->values_to_commit[$i]['values']));
336 $iteration_value = $iteration_value[1];
338 array_push($flattened_value, $iteration_value);
341 $this->values_to_commit[$i]['mean'] = $this->aggregate_values('Arithmetic', $flattened_value);
342 $this->values_to_commit[$i]['sum'] = $this->aggregate_values('Total', $flattened_value);
343 $this->values_to_commit[$i]['square_sum'] = $this->aggregate_values('SquareSum', $flattened_value);
347 function add_values_to_commit($metric_id, $config_type, $values) {
348 array_push($this->values_to_commit, array('metric_id' => $metric_id, 'config_type' => $config_type, 'values' => $values));
351 function commit($platform_id, $build_id) {
352 $this->db->begin_transaction() or $this->exit_with_error('FailedToBeginTransaction');
354 foreach ($this->values_to_commit as $item) {
355 $config_data = array('metric' => $item['metric_id'], 'type' => $item['config_type'], 'platform' => $platform_id);
356 $config_id = $this->db->select_or_insert_row('test_configurations', 'config', $config_data);
358 $this->rollback_with_error('FailedToObtainConfiguration', $config_data);
360 $values = $item['values'];
362 for ($group = 0; $group < count($values); ++$group)
363 $total_count += count($values[$group]);
364 $run_data = array('config' => $config_id, 'build' => $build_id, 'iteration_count_cache' => $total_count,
365 'mean_cache' => $item['mean'], 'sum_cache' => $item['sum'], 'square_sum_cache' => $item['square_sum']);
366 $run_id = $this->db->insert_row('test_runs', 'run', $run_data);
368 $this->rollback_with_error('FailedToInsertRun', array('metric' => $item['metric_id'], 'param' => $run_data));
370 $flattened_order = 0;
371 for ($group = 0; $group < count($values); ++$group) {
372 for ($i = 0; $i < count($values[$group]); ++$i) {
373 $iteration_value = $values[$group][$i];
374 $relative_time = NULL;
375 if (gettype($iteration_value) === 'array') {
376 assert(count($iteration_value) == 2); // compute_caches checks this condition.
377 $relative_time = $iteration_value[0];
378 $iteration_value = $iteration_value[1];
380 $param = array('run' => $run_id, 'order' => $flattened_order, 'value' => $iteration_value,
381 'group' => count($values) == 1 ? NULL : $group, 'relative_time' => $relative_time);
382 $this->db->insert_row('run_iterations', 'iteration', $param, NULL)
383 or $this->rollback_with_error('FailedToInsertIteration', array('config' => $config_id, 'build' => $build_id, 'param' => $param));
389 $this->db->query_and_get_affected_rows("UPDATE reports SET report_committed_at = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
390 WHERE report_id = $1", array($this->report_id));
392 $this->db->commit_transaction() or $this->exit_with_error('FailedToCommitTransaction');
395 private function rollback_with_error($message, $details) {
396 $this->db->rollback_transaction();
397 $this->exit_with_error($message, $details);