<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[176422] trunk/Websites/perf.webkit.org</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/176422">176422</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2014-11-20 15:25:07 -0800 (Thu, 20 Nov 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>New perf dashboard should provide UI to create a new analysis task
https://bugs.webkit.org/show_bug.cgi?id=138910
Reviewed by Benjamin Poulain.
This patch reverts some parts of <a href="http://trac.webkit.org/projects/webkit/changeset/175006">r175006</a> and re-introduces bugs associated with analysis tasks.
I'll add UI to show and edit bug numbers associated with an analysis task in a follow up patch.
With this patch, we can create a new analysis task by selection a range of points and opening
"analysis pane" (renamed from "bugs pane"). Each analysis task created is represented by a yellow bar
in the chart hyperlinked to the analysis task.
* init-database.sql: Redefined the bugs to be associated with an analysis task instead of a test run.
* public/api/analysis-tasks.php: Added the support for querying analysis tasks for a specific metric
on a specific platform. Also retrieve and return all bugs associated with analysis tasks.
(main):
(fetch_and_push_bugs_to_tasks): Added. Fetches all bugs associated with an array of analysis tasks
and adds the associated bugs to each task in the array.
(format_task):
* public/api/runs.php: Reverted changes made in <a href="http://trac.webkit.org/projects/webkit/changeset/175006">r175006</a>.
(fetch_runs_for_config):
(format_run):
* public/api/test-groups.php:
(fetch_test_groups_for_task): Use the newly added Database::select_rows.
* public/include/db.php:
(Database::select_first_or_last_row):
(Database::select_rows): Extracted from select_first_or_last_row.
* public/v2/analysis.js:
(App.AnalysisTask): Added "bugs" property.
(App.Bug): Added now that bugs are regular data store objects.
* public/v2/app.js:
(App.Pane._fetch): Calls this.fetchAnalyticRanges to fetch analysis tasks as well as test runs.
(App.Pane.fetchAnalyticRanges): Added. Fetches analysis tasks for the current metric on the current
platform that are associated with a specific range of runs.
(App.PaneController.actions.toggleBugsPane): Updated per showingBugsPane to showingAnalysisPane rename.
(App.PaneController.actions.associateBug): Deleted.
(App.PaneController.actions.createAnalysisTask): Replaced the pre-condition checks with assertions as
this action should never be triggered when the pre-condition is not met. Also re-fetch analysis tasks
once we've created one.
(App.PaneController.toggleSearchPane): Updated per showingBugsPane to showingAnalysisPane rename.
(App.PaneController._detailsChanged): Ditto. Removed selectedSinglePoint since it's no longer used.
(App.PaneController._showDetails): Call _updateCanAnalyze to update the status of "Analyze" button.
(App.PaneController._updateBugs): Deleted.
(App.PaneController._updateMarkedPoints): Deleted.
(App.PaneController._updateCanAnalyze): Added. Disables the button to create an analysis task when
the name is missing or when at most one point is selected.
(App.InteractiveChartComponent._constructGraphIfPossible): Update the locations of range rects.
(App.InteractiveChartComponent._relayoutDataAndAxes): Ditto.
(App.InteractiveChartComponent._mousePointInGraph): Don't return a point unless the mouse cursor is
on our svg element to avoid locking the current item when a bar shown for an analysis task is clicked.
(App.InteractiveChartComponent._rangesChanged): Added. Creates an array of objects representing
clickable bars for analysis tasks.
(App.InteractiveChartComponent._updateRangeBarRects): Computes the inline style used by each clickable
bar for analysis tasks to place them at the right location.
(App.InteractiveChartComponent.actions.openRange): Added. Forwards the action to the parent controller.
* public/v2/chart-pane.css:
(.chart .extent): Use the same color as the vertical indicator in the highlight behind the selection.
(.chart .rangeBar): Added.
* public/v2/data.js:
(TimeSeries.prototype.nextPoint): Added. Used by _rangesChanged.
* public/v2/index.html: Renamed "bugs pane" to "analysis pane" and removed the UI to associate bugs.
This ability will be reinstated in a follow up patch. Also added a container div and spans for analysis
task bars in the interactive chart component.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapianalysistasksphp">trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapirunsphp">trunk/Websites/perf.webkit.org/public/api/runs.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapitestgroupsphp">trunk/Websites/perf.webkit.org/public/api/test-groups.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludedbphp">trunk/Websites/perf.webkit.org/public/include/db.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2analysisjs">trunk/Websites/perf.webkit.org/public/v2/analysis.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appjs">trunk/Websites/perf.webkit.org/public/v2/app.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2chartpanecss">trunk/Websites/perf.webkit.org/public/v2/chart-pane.css</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2datajs">trunk/Websites/perf.webkit.org/public/v2/data.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2indexhtml">trunk/Websites/perf.webkit.org/public/v2/index.html</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -1,3 +1,79 @@
</span><ins>+2014-11-20 Ryosuke Niwa <rniwa@webkit.org>
+
+ New perf dashboard should provide UI to create a new analysis task
+ https://bugs.webkit.org/show_bug.cgi?id=138910
+
+ Reviewed by Benjamin Poulain.
+
+ This patch reverts some parts of r175006 and re-introduces bugs associated with analysis tasks.
+ I'll add UI to show and edit bug numbers associated with an analysis task in a follow up patch.
+
+ With this patch, we can create a new analysis task by selection a range of points and opening
+ "analysis pane" (renamed from "bugs pane"). Each analysis task created is represented by a yellow bar
+ in the chart hyperlinked to the analysis task.
+
+ * init-database.sql: Redefined the bugs to be associated with an analysis task instead of a test run.
+
+ * public/api/analysis-tasks.php: Added the support for querying analysis tasks for a specific metric
+ on a specific platform. Also retrieve and return all bugs associated with analysis tasks.
+ (main):
+ (fetch_and_push_bugs_to_tasks): Added. Fetches all bugs associated with an array of analysis tasks
+ and adds the associated bugs to each task in the array.
+ (format_task):
+
+ * public/api/runs.php: Reverted changes made in r175006.
+ (fetch_runs_for_config):
+ (format_run):
+
+ * public/api/test-groups.php:
+ (fetch_test_groups_for_task): Use the newly added Database::select_rows.
+
+ * public/include/db.php:
+ (Database::select_first_or_last_row):
+ (Database::select_rows): Extracted from select_first_or_last_row.
+
+ * public/v2/analysis.js:
+ (App.AnalysisTask): Added "bugs" property.
+ (App.Bug): Added now that bugs are regular data store objects.
+
+ * public/v2/app.js:
+ (App.Pane._fetch): Calls this.fetchAnalyticRanges to fetch analysis tasks as well as test runs.
+ (App.Pane.fetchAnalyticRanges): Added. Fetches analysis tasks for the current metric on the current
+ platform that are associated with a specific range of runs.
+ (App.PaneController.actions.toggleBugsPane): Updated per showingBugsPane to showingAnalysisPane rename.
+ (App.PaneController.actions.associateBug): Deleted.
+ (App.PaneController.actions.createAnalysisTask): Replaced the pre-condition checks with assertions as
+ this action should never be triggered when the pre-condition is not met. Also re-fetch analysis tasks
+ once we've created one.
+ (App.PaneController.toggleSearchPane): Updated per showingBugsPane to showingAnalysisPane rename.
+ (App.PaneController._detailsChanged): Ditto. Removed selectedSinglePoint since it's no longer used.
+ (App.PaneController._showDetails): Call _updateCanAnalyze to update the status of "Analyze" button.
+ (App.PaneController._updateBugs): Deleted.
+ (App.PaneController._updateMarkedPoints): Deleted.
+ (App.PaneController._updateCanAnalyze): Added. Disables the button to create an analysis task when
+ the name is missing or when at most one point is selected.
+
+ (App.InteractiveChartComponent._constructGraphIfPossible): Update the locations of range rects.
+ (App.InteractiveChartComponent._relayoutDataAndAxes): Ditto.
+ (App.InteractiveChartComponent._mousePointInGraph): Don't return a point unless the mouse cursor is
+ on our svg element to avoid locking the current item when a bar shown for an analysis task is clicked.
+ (App.InteractiveChartComponent._rangesChanged): Added. Creates an array of objects representing
+ clickable bars for analysis tasks.
+ (App.InteractiveChartComponent._updateRangeBarRects): Computes the inline style used by each clickable
+ bar for analysis tasks to place them at the right location.
+ (App.InteractiveChartComponent.actions.openRange): Added. Forwards the action to the parent controller.
+
+ * public/v2/chart-pane.css:
+ (.chart .extent): Use the same color as the vertical indicator in the highlight behind the selection.
+ (.chart .rangeBar): Added.
+
+ * public/v2/data.js:
+ (TimeSeries.prototype.nextPoint): Added. Used by _rangesChanged.
+
+ * public/v2/index.html: Renamed "bugs pane" to "analysis pane" and removed the UI to associate bugs.
+ This ability will be reinstated in a follow up patch. Also added a container div and spans for analysis
+ task bars in the interactive chart component.
+
</ins><span class="cx"> 2014-11-19 Ryosuke Niwa <rniwa@webkit.org>
</span><span class="cx">
</span><span class="cx"> Fix typos in r176203.
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -130,15 +130,6 @@
</span><span class="cx"> report_failure varchar(64),
</span><span class="cx"> report_failure_details text);
</span><span class="cx">
</span><del>-CREATE TABLE bugs (
- bug_id serial PRIMARY KEY,
- bug_run integer REFERENCES test_runs NOT NULL,
- bug_tracker integer REFERENCES bug_trackers NOT NULL,
- bug_number integer NOT NULL,
- CONSTRAINT bug_tracker_and_run_must_be_unique UNIQUE(bug_tracker, bug_run));
-CREATE INDEX bugs_tracker_number_index ON bugs(bug_tracker, bug_number);
-CREATE INDEX bugs_run_index ON bugs(bug_run);
-
</del><span class="cx"> CREATE TABLE analysis_tasks (
</span><span class="cx"> task_id serial PRIMARY KEY,
</span><span class="cx"> task_name varchar(256) NOT NULL,
</span><span class="lines">@@ -148,8 +139,17 @@
</span><span class="cx"> task_metric integer REFERENCES test_metrics NOT NULL,
</span><span class="cx"> task_start_run integer REFERENCES test_runs,
</span><span class="cx"> task_end_run integer REFERENCES test_runs,
</span><del>- CONSTRAINT analysis_task_should_be_unique_for_range UNIQUE(task_start_run, task_end_run));
</del><ins>+ CONSTRAINT analysis_task_should_be_unique_for_range UNIQUE(task_start_run, task_end_run)
+ CONSTRAINT analysis_task_should_not_be_associated_with_single_run
+ CHECK ((task_start_run IS NULL AND task_end_run IS NULL) OR (task_start_run IS NOT NULL AND task_end_run IS NOT NULL)));
</ins><span class="cx">
</span><ins>+CREATE TABLE bugs (
+ bug_id serial PRIMARY KEY,
+ bug_task integer REFERENCES analysis_tasks NOT NULL,
+ bug_tracker integer REFERENCES bug_trackers NOT NULL,
+ bug_number integer NOT NULL,
+ CONSTRAINT bug_task_and_tracker_must_be_unique UNIQUE(bug_task, bug_tracker));
+
</ins><span class="cx"> CREATE TABLE analysis_test_groups (
</span><span class="cx"> testgroup_id serial PRIMARY KEY,
</span><span class="cx"> testgroup_task integer REFERENCES analysis_tasks NOT NULL,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapianalysistasksphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -17,15 +17,49 @@
</span><span class="cx"> exit_with_error('TaskNotFound', array('id' => $task_id));
</span><span class="cx"> $tasks = array($task);
</span><span class="cx"> } else {
</span><del>- // FIXME: Limit the number of tasks we fetch.
- $tasks = array_reverse($db->fetch_table('analysis_tasks', 'task_created_at'));
</del><ins>+ $metric_id = array_get($_GET, 'metric');
+ $platform_id = array_get($_GET, 'platform');
+ if (!!$metric_id != !!$platform_id)
+ exit_with_error('InvalidArguments', array('metricId' => $metric_id, 'platformId' => $platform_id));
+
+ if ($metric_id)
+ $tasks = $db->select_rows('analysis_tasks', 'task', array('platform' => $platform_id, 'metric' => $metric_id));
+ else {
+ // FIXME: Limit the number of tasks we fetch.
+ $tasks = array_reverse($db->fetch_table('analysis_tasks', 'task_created_at'));
+ }
+
</ins><span class="cx"> if (!is_array($tasks))
</span><span class="cx"> exit_with_error('FailedToFetchTasks');
</span><span class="cx"> }
</span><span class="cx">
</span><del>- exit_with_success(array('analysisTasks' => array_map("format_task", $tasks)));
</del><ins>+ $tasks = array_map("format_task", $tasks);
+ $bugs = fetch_and_push_bugs_to_tasks($db, $tasks);
+
+ exit_with_success(array('analysisTasks' => $tasks, 'bugs' => $bugs));
</ins><span class="cx"> }
</span><span class="cx">
</span><ins>+function fetch_and_push_bugs_to_tasks($db, &$tasks) {
+ $task_ids = array();
+ $task_by_id = array();
+ foreach ($tasks as &$task) {
+ array_push($task_ids, $task['id']);
+ $task_by_id[$task['id']] = &$task;
+ }
+
+ $bugs = $db->query_and_fetch_all('SELECT bug_id AS "id", bug_task AS "task", bug_tracker AS "bugTracker", bug_number AS "number"
+ FROM bugs WHERE bug_task = ANY ($1)', array('{' . implode(', ', $task_ids) . '}'));
+ if (!is_array($bugs))
+ exit_with_error('FailedToFetchBugs');
+
+ foreach ($bugs as $bug) {
+ $associated_task = &$task_by_id[$bug['task']];
+ array_push($associated_task['bugs'], $bug['id']);
+ }
+
+ return $bugs;
+}
+
</ins><span class="cx"> date_default_timezone_set('UTC');
</span><span class="cx"> function format_task($task_row) {
</span><span class="cx"> return array(
</span><span class="lines">@@ -37,6 +71,7 @@
</span><span class="cx"> 'metric' => $task_row['task_metric'],
</span><span class="cx"> 'startRun' => $task_row['task_start_run'],
</span><span class="cx"> 'endRun' => $task_row['task_end_run'],
</span><ins>+ 'bugs' => array(),
</ins><span class="cx"> );
</span><span class="cx"> }
</span><span class="cx">
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapirunsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/runs.php (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/runs.php        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/api/runs.php        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -31,13 +31,11 @@
</span><span class="cx"> function fetch_runs_for_config($db, $config) {
</span><span class="cx"> $raw_runs = $db->query_and_fetch_all('
</span><span class="cx"> SELECT test_runs.*, builds.*, array_agg((commit_repository, commit_revision, commit_time)) AS revisions
</span><del>- FROM builds LEFT OUTER JOIN build_commits ON commit_build = build_id
- LEFT OUTER JOIN commits ON build_commit = commit_id,
- (SELECT test_runs.*, array_agg((bug_tracker, bug_number)) AS bugs
- FROM test_runs LEFT OUTER JOIN bugs ON bug_run = run_id WHERE run_config = $1 GROUP BY run_id) as test_runs
- WHERE run_build = build_id
- GROUP BY run_id, run_config, run_build, run_mean_cache, run_iteration_count_cache,
- run_sum_cache, run_square_sum_cache, bugs, build_id', array($config['config_id']));
</del><ins>+ FROM builds
+ LEFT OUTER JOIN build_commits ON commit_build = build_id
+ LEFT OUTER JOIN commits ON build_commit = commit_id, test_runs
+ WHERE run_build = build_id AND run_config = $1
+ GROUP BY build_id, run_id', array($config['config_id']));
</ins><span class="cx">
</span><span class="cx"> $formatted_runs = array();
</span><span class="cx"> if (!$raw_runs)
</span><span class="lines">@@ -66,19 +64,6 @@
</span><span class="cx"> return $revisions;
</span><span class="cx"> }
</span><span class="cx">
</span><del>-function parse_bugs_array($postgres_array) {
- // e.g. {"(1 /* Bugzilla */, 12345)","(2 /* Radar */, 67890)"}
- $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
- $bugs = array();
- foreach ($outer_array as $item) {
- $raw_data = explode(',', trim($item, '()'));
- if (!$raw_data[0])
- continue;
- $bugs[trim($raw_data[0], '"')] = trim($raw_data[1], '"');
- }
- return $bugs;
-}
-
</del><span class="cx"> function format_run($run) {
</span><span class="cx"> return array(
</span><span class="cx"> 'id' => intval($run['run_id']),
</span><span class="lines">@@ -87,7 +72,6 @@
</span><span class="cx"> 'sum' => floatval($run['run_sum_cache']),
</span><span class="cx"> 'squareSum' => floatval($run['run_square_sum_cache']),
</span><span class="cx"> 'revisions' => parse_revisions_array($run['revisions']),
</span><del>- 'bugs' => parse_bugs_array($run['bugs']),
</del><span class="cx"> 'buildTime' => strtotime($run['build_time']) * 1000,
</span><span class="cx"> 'buildNumber' => intval($run['build_number']),
</span><span class="cx"> 'builder' => $run['build_builder']);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapitestgroupsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/test-groups.php (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/test-groups.php        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/api/test-groups.php        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -43,8 +43,7 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> function fetch_test_groups_for_task($db, $task_id) {
</span><del>- return $db->query_and_fetch_all('SELECT * FROM analysis_test_groups WHERE testgroup_task = $1
- ORDER BY testgroup_created_at', array($task_id));
</del><ins>+ return $db->select_rows('analysis_test_groups', 'testgroup', array('task' => $task_id));
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> function fetch_build_requests_for_task($db, $task_id) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludedbphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/db.php (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/db.php        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/include/db.php        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -158,6 +158,13 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> private function select_first_or_last_row($table, $prefix, $params, $order_by, $descending_order) {
</span><ins>+ $rows = $this->select_rows($table, $prefix, $params, $order_by, $descending_order, 0, 1);
+ return $rows ? $rows[0] : NULL;
+ }
+
+ function select_rows($table, $prefix, $params,
+ $order_by = NULL, $descending_order = FALSE, $offset = NULL, $limit = NULL) {
+
</ins><span class="cx"> $placeholders = array();
</span><span class="cx"> $values = array();
</span><span class="cx"> $column_names = $this->prefixed_column_names($this->prepare_params($params, $placeholders, $values), $prefix);
</span><span class="lines">@@ -169,9 +176,12 @@
</span><span class="cx"> if ($descending_order)
</span><span class="cx"> $query .= ' DESC';
</span><span class="cx"> }
</span><del>- $rows = $this->query_and_fetch_all($query . ' LIMIT 1', $values);
</del><ins>+ if ($offset !== NULL)
+ $query .= ' OFFSET ' . intval($offset);
+ if ($limit !== NULL)
+ $query .= ' LIMIT ' . intval($limit);
</ins><span class="cx">
</span><del>- return $rows ? $rows[0] : NULL;
</del><ins>+ return $this->query_and_fetch_all($query, $values);
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> function query_and_get_affected_rows($query, $params = array()) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2analysisjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/analysis.js (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/analysis.js        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/v2/analysis.js        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -5,11 +5,19 @@
</span><span class="cx"> metric: DS.belongsTo('metric'),
</span><span class="cx"> startRun: DS.attr('number'),
</span><span class="cx"> endRun: DS.attr('number'),
</span><ins>+ bugs: DS.hasMany('bugs'),
</ins><span class="cx"> testGroups: function () {
</span><span class="cx"> return this.store.find('testGroup', {task: this.get('id')});
</span><span class="cx"> }.property(),
</span><span class="cx"> });
</span><span class="cx">
</span><ins>+App.Bug = App.NameLabelModel.extend({
+ task: DS.belongsTo('AnalysisTask'),
+ bugTracker: DS.belongsTo('BugTracker'),
+ createdAt: DS.attr('date'),
+ number: DS.attr('number'),
+});
+
</ins><span class="cx"> // FIXME: Use DS.RESTAdapter instead.
</span><span class="cx"> App.AnalysisTask.create = function (name, startMeasurement, endMeasurement)
</span><span class="cx"> {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -358,8 +358,21 @@
</span><span class="cx"> else
</span><span class="cx"> self.set('failure', 'An internal error');
</span><span class="cx"> });
</span><ins>+
+ this.fetchAnalyticRanges();
</ins><span class="cx"> }
</span><span class="cx"> }.observes('platformId', 'metricId').on('init'),
</span><ins>+ fetchAnalyticRanges: function ()
+ {
+ var platformId = this.get('platformId');
+ var metricId = this.get('metricId');
+ var self = this;
+ this.get('store')
+ .find('analysisTask', {platform: platformId, metric: metricId})
+ .then(function (tasks) {
+ self.set('analyticRanges', tasks.filter(function (task) { return task.get('startRun') && task.get('endRun'); }));
+ });
+ },
</ins><span class="cx"> _isValidId: function (id)
</span><span class="cx"> {
</span><span class="cx"> if (typeof(id) == "number")
</span><span class="lines">@@ -657,34 +670,23 @@
</span><span class="cx"> },
</span><span class="cx"> toggleBugsPane: function ()
</span><span class="cx"> {
</span><del>- if (this.toggleProperty('showingBugsPane'))
</del><ins>+ if (this.toggleProperty('showingAnalysisPane'))
</ins><span class="cx"> this.set('showingSearchPane', false);
</span><span class="cx"> },
</span><del>- associateBug: function (bugTracker, bugNumber)
- {
- var point = this.get('selectedSinglePoint');
- if (!point)
- return;
- var self = this;
- point.measurement.associateBug(bugTracker.get('id'), bugNumber).then(function () {
- self._updateBugs();
- self._updateMarkedPoints();
- }, function (error) {
- alert(error);
- });
- },
</del><span class="cx"> createAnalysisTask: function ()
</span><span class="cx"> {
</span><span class="cx"> var name = this.get('newAnalysisTaskName');
</span><span class="cx"> var points = this._selectedPoints;
</span><del>- if (!name || !points || points.length < 2)
- return;
</del><ins>+ Ember.assert('The analysis name should not be empty', name);
+ Ember.assert('There should be at least two points in the range', points && points.length >= 2);
</ins><span class="cx">
</span><span class="cx"> var newWindow = window.open();
</span><ins>+ var self = this;
</ins><span class="cx"> App.AnalysisTask.create(name, points[0].measurement, points[points.length - 1].measurement).then(function (data) {
</span><span class="cx"> // FIXME: Update the UI to show the new analysis task.
</span><span class="cx"> var url = App.Router.router.generate('analysisTask', data['taskId']);
</span><span class="cx"> newWindow.location.href = '#' + url;
</span><ins>+ self.get('model').fetchAnalyticRanges();
</ins><span class="cx"> }, function (error) {
</span><span class="cx"> newWindow.close();
</span><span class="cx"> if (error === 'DuplicateAnalysisTask') {
</span><span class="lines">@@ -701,7 +703,7 @@
</span><span class="cx"> if (!model.get('commitSearchRepository'))
</span><span class="cx"> model.set('commitSearchRepository', App.Manifest.repositoriesWithReportedCommits[0]);
</span><span class="cx"> if (this.toggleProperty('showingSearchPane'))
</span><del>- this.set('showingBugsPane', false);
</del><ins>+ this.set('showingAnalysisPane', false);
</ins><span class="cx"> },
</span><span class="cx"> searchCommit: function () {
</span><span class="cx"> var model = this.get('model');
</span><span class="lines">@@ -733,8 +735,7 @@
</span><span class="cx"> },
</span><span class="cx"> _detailsChanged: function ()
</span><span class="cx"> {
</span><del>- this.set('showingBugsPane', false);
- this.set('selectedSinglePoint', !this._hasRange && this._selectedPoints ? this._selectedPoints[0] : null);
</del><ins>+ this.set('showingAnalysisPane', false);
</ins><span class="cx"> }.observes('details'),
</span><span class="cx"> _overviewSelectionChanged: function ()
</span><span class="cx"> {
</span><span class="lines">@@ -812,58 +813,13 @@
</span><span class="cx"> buildTime: currentMeasurement.formattedBuildTime(),
</span><span class="cx"> revisions: revisions,
</span><span class="cx"> }));
</span><del>- this._updateBugs();
</del><ins>+ this._updateCanAnalyze();
</ins><span class="cx"> },
</span><del>- _updateBugs: function ()
</del><ins>+ _updateCanAnalyze: function ()
</ins><span class="cx"> {
</span><del>- if (!this._selectedPoints)
- return;
-
- var bugTrackers = App.Manifest.get('bugTrackers');
- var trackerToBugNumbers = {};
- bugTrackers.forEach(function (tracker) { trackerToBugNumbers[tracker.get('id')] = new Array(); });
-
- var points = this._hasRange ? this._selectedPoints : [this._selectedPoints[1]];
- points.map(function (point) {
- var bugs = point.measurement.bugs();
- bugTrackers.forEach(function (tracker) {
- var bugNumber = bugs[tracker.get('id')];
- if (bugNumber)
- trackerToBugNumbers[tracker.get('id')].push(bugNumber);
- });
- });
-
- this.set('details.bugTrackers', App.Manifest.get('bugTrackers').map(function (tracker) {
- var bugNumbers = trackerToBugNumbers[tracker.get('id')];
- return Ember.ObjectProxy.create({
- content: tracker,
- bugs: bugNumbers.map(function (bugNumber) {
- return {
- bugNumber: bugNumber,
- bugUrl: bugNumber && tracker.get('bugUrl') ? tracker.get('bugUrl').replace(/\$number/g, bugNumber) : null
- };
- }),
- editedBugNumber: this._hasRange ? null : bugNumbers[0],
- }); // FIXME: Create urls for new bugs.
- }));
- },
- _updateMarkedPoints: function ()
- {
- var chartData = this.get('chartData');
- if (!chartData || !chartData.current) {
- this.set('markedPoints', {});
- return;
- }
-
- var series = chartData.current.timeSeriesByCommitTime().series();
- var markedPoints = {};
- for (var i = 0; i < series.length; i++) {
- var measurement = series[i].measurement;
- if (measurement.hasBugs())
- markedPoints[measurement.id()] = true;
- }
- this.set('markedPoints', markedPoints);
- }.observes('chartData'),
</del><ins>+ var points = this._selectedPoints;
+ this.set('cannotAnalyze', !this.get('newAnalysisTaskName') || !this._hasRange || !points || points.length < 2);
+ }.observes('newAnalysisTaskName'),
</ins><span class="cx"> });
</span><span class="cx">
</span><span class="cx"> App.InteractiveChartComponent = Ember.Component.extend({
</span><span class="lines">@@ -1047,6 +1003,8 @@
</span><span class="cx"> setTimeout(this._selectedItemChanged.bind(this), 0);
</span><span class="cx">
</span><span class="cx"> this._needsConstruction = false;
</span><ins>+
+ this._rangesChanged();
</ins><span class="cx"> },
</span><span class="cx"> _updateDomain: function ()
</span><span class="cx"> {
</span><span class="lines">@@ -1142,6 +1100,7 @@
</span><span class="cx"> });
</span><span class="cx"> this._updateMarkedDots();
</span><span class="cx"> this._updateHighlightPositions();
</span><ins>+ this._updateRangeBarRects();
</ins><span class="cx">
</span><span class="cx"> if (this._brush) {
</span><span class="cx"> if (selection)
</span><span class="lines">@@ -1163,7 +1122,7 @@
</span><span class="cx"> this._yAxisUnitContainer.remove();
</span><span class="cx"> this._yAxisUnitContainer = this._yAxisLabels.append("text")
</span><span class="cx"> .attr("x", 0.5 * this._rem)
</span><del>- .attr("y", this._rem)
</del><ins>+ .attr("y", 0.2 * this._rem)
</ins><span class="cx"> .attr("dy", 0.8 * this._rem)
</span><span class="cx"> .style("text-anchor", "start")
</span><span class="cx"> .style("z-index", "100")
</span><span class="lines">@@ -1350,7 +1309,7 @@
</span><span class="cx"> _mousePointInGraph: function (event)
</span><span class="cx"> {
</span><span class="cx"> var offset = $(this.get('element')).offset();
</span><del>- if (!offset)
</del><ins>+ if (!offset || !$(event.target).closest('svg').length)
</ins><span class="cx"> return null;
</span><span class="cx">
</span><span class="cx"> var point = {
</span><span class="lines">@@ -1483,6 +1442,99 @@
</span><span class="cx"> this._updateHighlightPositions();
</span><span class="cx">
</span><span class="cx"> }.observes('highlightedItems'),
</span><ins>+ _rangesChanged: function ()
+ {
+ if (!this._currentTimeSeries)
+ return;
+
+ function midPoint(firstPoint, secondPoint) {
+ if (firstPoint && secondPoint)
+ return (+firstPoint.time + +secondPoint.time) / 2;
+ if (firstPoint)
+ return firstPoint.time;
+ return secondPoint.time;
+ }
+ var currentTimeSeries = this._currentTimeSeries;
+ var linkRoute = this.get('rangeRoute');
+ this.set('rangeBars', (this.get('ranges') || []).map(function (range) {
+ var start = currentTimeSeries.findPointByMeasurementId(range.get('startRun'));
+ var end = currentTimeSeries.findPointByMeasurementId(range.get('endRun'));
+ return Ember.Object.create({
+ startTime: midPoint(currentTimeSeries.previousPoint(start), start),
+ endTime: midPoint(end, currentTimeSeries.nextPoint(end)),
+ range: range,
+ left: null,
+ right: null,
+ rowIndex: null,
+ top: null,
+ bottom: null,
+ linkRoute: linkRoute,
+ linkId: range.get('id'),
+ });
+ }));
+
+ this._updateRangeBarRects();
+ }.observes('ranges'),
+ _updateRangeBarRects: function () {
+ var rangeBars = this.get('rangeBars');
+ if (!rangeBars || !rangeBars.length)
+ return;
+
+ var xScale = this._x;
+ var yScale = this._y;
+
+ // Expand the width of each range as needed and sort ranges by the left-edge of ranges.
+ var minWidth = 3;
+ var sortedBars = rangeBars.map(function (bar) {
+ var left = xScale(bar.get('startTime'));
+ var right = xScale(bar.get('endTime'));
+ if (right - left < minWidth) {
+ left -= minWidth / 2;
+ right += minWidth / 2;
+ }
+ bar.set('left', left);
+ bar.set('right', right);
+ return bar;
+ }).sort(function (first, second) { return first.get('left') - second.get('left'); });
+
+ // At this point, left edges of all ranges prior to a range R1 is on the left of R1.
+ // Place R1 into a row in which right edges of all ranges prior to R1 is on the left of R1 to avoid overlapping ranges.
+ var rows = [];
+ sortedBars.forEach(function (bar) {
+ var rowIndex = 0;
+ for (; rowIndex < rows.length; rowIndex++) {
+ var currentRow = rows[rowIndex];
+ if (currentRow[currentRow.length - 1].get('right') < bar.get('left')) {
+ currentRow.push(bar);
+ break;
+ }
+ }
+ if (rowIndex >= rows.length)
+ rows.push([bar]);
+ bar.set('rowIndex', rowIndex);
+ });
+ var rowHeight = 0.6 * this._rem;
+ var firstRowTop = this._contentHeight - rows.length * rowHeight;
+ var barHeight = 0.5 * this._rem;
+
+ $(this.get('element')).find('.rangeBarsContainerInlineStyle').css({
+ left: this._margin.left + 'px',
+ top: this._margin.top + firstRowTop + 'px',
+ width: this._contentWidth + 'px',
+ height: rows.length * barHeight + 'px',
+ overflow: 'hidden',
+ position: 'absolute',
+ });
+
+ var margin = this._margin;
+ sortedBars.forEach(function (bar) {
+ var top = bar.get('rowIndex') * rowHeight;
+ var height = barHeight;
+ var left = bar.get('left');
+ var width = bar.get('right') - left;
+ bar.set('inlineStyle', 'left: ' + left + 'px; top: ' + top + 'px; width: ' + width + 'px; height: ' + height + 'px;');
+ });
+ },
</ins><span class="cx"> _updateCurrentItemIndicators: function ()
</span><span class="cx"> {
</span><span class="cx"> if (!this._currentItemLine)
</span><span class="lines">@@ -1548,6 +1600,10 @@
</span><span class="cx"> this.sendAction('zoom', this._currentSelection());
</span><span class="cx"> this.set('selection', null);
</span><span class="cx"> },
</span><ins>+ openRange: function (range)
+ {
+ this.sendAction('openRange', range);
+ },
</ins><span class="cx"> },
</span><span class="cx"> });
</span><span class="cx">
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2chartpanecss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -315,7 +315,7 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> .chart .extent {
</span><del>- stroke: #9c6;
</del><ins>+ stroke: #f93;
</ins><span class="cx"> stroke-width: 1px;
</span><span class="cx"> fill: #9c6;
</span><span class="cx"> fill-opacity: .125;
</span><span class="lines">@@ -326,3 +326,9 @@
</span><span class="cx"> fill: #333;
</span><span class="cx"> stroke: none;
</span><span class="cx"> }
</span><ins>+
+.chart .rangeBar {
+ display: block;
+ background-color: #fc6;
+ position: absolute;
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2datajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/data.js        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -423,3 +423,10 @@
</span><span class="cx"> return null;
</span><span class="cx"> return this._series[point.seriesIndex - 1];
</span><span class="cx"> }
</span><ins>+
+TimeSeries.prototype.nextPoint = function (point)
+{
+ if (!point.seriesIndex)
+ return null;
+ return this._series[point.seriesIndex + 1];
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (176421 => 176422)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/index.html        2014-11-20 23:04:51 UTC (rev 176421)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html        2014-11-20 23:25:07 UTC (rev 176422)
</span><span class="lines">@@ -132,8 +132,8 @@
</span><span class="cx"> <h1 {{action "toggleDetails"}}>{{metric.fullName}} - {{ platform.name}}</h1>
</span><span class="cx"> <a href="#" title="Close" class="close-button" {{action "close"}}>{{partial "close-button"}}</a>
</span><span class="cx"> {{#if App.Manifest.bugTrackers}}
</span><del>- <a href="#" title="Bugs and Analysis" class="bugs-button" {{action "toggleBugsPane"}}>
- {{partial "bugs-button"}}
</del><ins>+ <a href="#" title="Analysis" class="bugs-button" {{action "toggleBugsPane"}}>
+ {{partial "analysis-button"}}
</ins><span class="cx"> </a>
</span><span class="cx"> {{/if}}
</span><span class="cx"> {{#if App.Manifest.repositoriesWithReportedCommits}}
</span><span class="lines">@@ -146,6 +146,7 @@
</span><span class="cx"> {{#if chartData}}
</span><span class="cx"> {{interactive-chart
</span><span class="cx"> chartData=chartData
</span><ins>+ ranges=analyticRanges
</ins><span class="cx"> domain=mainPlotDomain
</span><span class="cx"> interactive=true
</span><span class="cx"> chartPointRadius=2
</span><span class="lines">@@ -153,6 +154,7 @@
</span><span class="cx"> currentTime=sharedTime
</span><span class="cx"> selectedItem=selectedItem
</span><span class="cx"> highlightedItems=highlightedItems
</span><ins>+ rangeRoute="analysisTask"
</ins><span class="cx"> selection=timeRange
</span><span class="cx"> sharedSelection=sharedSelection
</span><span class="cx"> selectionChanged="rangeChanged"
</span><span class="lines">@@ -194,25 +196,13 @@
</span><span class="cx"> {{input action="searchCommit" placeholder="Name or email" value=commitSearchKeyword}}
</span><span class="cx"> </form>
</span><span class="cx">
</span><del>- <div {{bind-attr class=":bugs-pane showingBugsPane::hidden"}}>
</del><ins>+ <div {{bind-attr class=":bugs-pane showingAnalysisPane::hidden"}}>
</ins><span class="cx"> <table>
</span><span class="cx"> <tbody>
</span><del>- {{#if selectedSinglePoint}}
- {{#each details.bugTrackers}}
- <tr>
- <th>{{label}}</th>
- <td>
- <form {{action "associateBug" this editedBugNumber on="submit"}}>
- {{input type=text value=editedBugNumber}}
- </form>
- </td>
- </tr>
- {{/each}}
- {{/if}}
</del><span class="cx"> <tr>
</span><span class="cx"> <th>
</span><span class="cx"> <label>Name: {{input type=text value=newAnalysisTaskName}}</label>
</span><del>- <button {{action "createAnalysisTask"}}>Analyze</button>
</del><ins>+ <button {{action "createAnalysisTask"}} {{bind-attr disabled=cannotAnalyze}}>Analyze</button>
</ins><span class="cx"> </th>
</span><span class="cx"> </tr>
</span><span class="cx"> </tbody>
</span><span class="lines">@@ -237,6 +227,13 @@
</span><span class="cx"> </a>
</span><span class="cx"> </div>
</span><span class="cx"> {{/if}}
</span><ins>+ <div class="rangeBarsContainerInlineStyle">
+ {{#each rangeBars}}
+ {{#link-to linkRoute linkId}}
+ <span class="rangeBar" {{bind-attr style=inlineStyle}}></span>
+ {{/link-to}}
+ {{/each}}
+ </div>
</ins><span class="cx"> </script>
</span><span class="cx">
</span><span class="cx"> <script type="text/x-handlebars" data-template-name="chart-details">
</span><span class="lines">@@ -323,8 +320,8 @@
</span><span class="cx"> </svg>
</span><span class="cx"> </script>
</span><span class="cx">
</span><del>- <script type="text/x-handlebars" data-template-name="bugs-button">
- <svg class="bugs-button icon-button" viewBox="0 0 100 100">
</del><ins>+ <script type="text/x-handlebars" data-template-name="analysis-button">
+ <svg class="analysis-button icon-button" viewBox="0 0 100 100">
</ins><span class="cx"> <g stroke="black" stroke-width="15">
</span><span class="cx"> <circle cx="50" cy="50" r="40" fill="transparent"/>
</span><span class="cx"> <line x1="50" y1="25" x2="50" y2="55"/>
</span></span></pre>
</div>
</div>
</body>
</html>