[LNT] r263882 - [profile] Add profile web UI
James Molloy via llvm-commits
llvm-commits at lists.llvm.org
Sat Mar 19 07:47:04 PDT 2016
Author: jamesm
Date: Sat Mar 19 09:47:04 2016
New Revision: 263882
URL: http://llvm.org/viewvc/llvm-project?rev=263882&view=rev
Log:
[profile] Add profile web UI
I still need to work out a decent testing story for this (and javascript testing in LNT in general). However, it works pretty well as-is.
Added:
lnt/trunk/lnt/server/ui/static/lnt_profile.css
lnt/trunk/lnt/server/ui/static/lnt_profile.js
Modified:
lnt/trunk/lnt/server/db/testsuitedb.py
lnt/trunk/lnt/server/ui/profile_views.py
Modified: lnt/trunk/lnt/server/db/testsuitedb.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/testsuitedb.py?rev=263882&r1=263881&r2=263882&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/testsuitedb.py (original)
+++ lnt/trunk/lnt/server/db/testsuitedb.py Sat Mar 19 09:47:04 2016
@@ -358,7 +358,7 @@ class TestSuiteDB(object):
return d
def load(self, profileDir):
- return profile.Profile.fromFile(os.path.join(self.filename))
+ return profile.Profile.fromFile(os.path.join(profileDir, self.filename))
class Sample(self.base, ParameterizedMixin):
__tablename__ = db_key_name + '_Sample'
Modified: lnt/trunk/lnt/server/ui/profile_views.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/profile_views.py?rev=263882&r1=263881&r2=263882&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/profile_views.py (original)
+++ lnt/trunk/lnt/server/ui/profile_views.py Sat Mar 19 09:47:04 2016
@@ -1,3 +1,17 @@
+import datetime
+from flask import g
+from flask import abort
+from flask import render_template
+from flask import request
+from flask import make_response
+from flask import flash
+from flask import redirect
+from flask import current_app
+from sqlalchemy.orm.exc import NoResultFound
+import flask
+import json
+import sys, os
+
from flask import render_template, current_app
import os, json
from lnt.server.ui.decorators import v4_route, frontend
@@ -39,3 +53,205 @@ def profile_admin():
return render_template("profile_admin.html",
history=history, age=age, bucket_size=bucket_size)
+
+ at v4_route("/profile/ajax/getFunctions")
+def v4_profile_ajax_getFunctions():
+ ts = request.get_testsuite()
+ runid = request.args.get('runid')
+ testid = request.args.get('testid')
+
+ profileDir = current_app.old_config.profileDir
+
+ idx = 0
+ tlc = {}
+ sample = ts.query(ts.Sample) \
+ .filter(ts.Sample.run_id == runid) \
+ .filter(ts.Sample.test_id == testid).first()
+ if sample and sample.profile:
+ p = sample.profile.load(profileDir)
+ return json.dumps([[n, f] for n,f in p.getFunctions().items()])
+ else:
+ abort(404);
+
+ at v4_route("/profile/ajax/getTopLevelCounters")
+def v4_profile_ajax_getTopLevelCounters():
+ ts = request.get_testsuite()
+ runids = request.args.get('runids').split(',')
+ testid = request.args.get('testid')
+
+ profileDir = current_app.old_config.profileDir
+
+ idx = 0
+ tlc = {}
+ for rid in runids:
+ sample = ts.query(ts.Sample) \
+ .filter(ts.Sample.run_id == rid) \
+ .filter(ts.Sample.test_id == testid).first()
+ if sample and sample.profile:
+ p = sample.profile.load(profileDir)
+ for k,v in p.getTopLevelCounters().items():
+ tlc.setdefault(k, [None]*len(runids))[idx] = v
+ idx += 1
+
+ # If the 1'th counter is None for all keys, truncate the list.
+ if all(k[1] is None for k in tlc.values()):
+ tlc = {k: [v[0]] for k,v in tlc.items()}
+
+ return json.dumps(tlc)
+
+ at v4_route("/profile/ajax/getCodeForFunction")
+def v4_profile_ajax_getCodeForFunction():
+ ts = request.get_testsuite()
+ runid = request.args.get('runid')
+ testid = request.args.get('testid')
+ f = request.args.get('f')
+
+ profileDir = current_app.old_config.profileDir
+
+ sample = ts.query(ts.Sample) \
+ .filter(ts.Sample.run_id == runid) \
+ .filter(ts.Sample.test_id == testid).first()
+ if not sample or not sample.profile:
+ abort(404);
+
+ p = sample.profile.load(profileDir)
+ return json.dumps([x for x in p.getCodeForFunction(f)])
+
+ at v4_route("/profile/<int:testid>/<int:run1_id>")
+def v4_profile_fwd(testid, run1_id):
+ return v4_profile(testid, run1_id)
+
+ at v4_route("/profile/<int:testid>/<int:run1_id>/<int:run2_id>")
+def v4_profile_fwd2(testid, run1_id, run2_id=None):
+ return v4_profile(testid, run1_id, run2_id)
+
+def v4_profile(testid, run1_id, run2_id=None):
+ ts = request.get_testsuite()
+ profileDir = current_app.old_config.profileDir
+
+ try:
+ test = ts.query(ts.Test).filter(ts.Test.id == testid).one()
+ run1 = ts.query(ts.Run).filter(ts.Run.id == run1_id).one()
+ sample1 = ts.query(ts.Sample) \
+ .filter(ts.Sample.run_id == run1_id) \
+ .filter(ts.Sample.test_id == testid).first()
+ if run2_id is not None:
+ run2 = ts.query(ts.Run).filter(ts.Run.id == run2_id).one()
+ sample2 = ts.query(ts.Sample) \
+ .filter(ts.Sample.run_id == run2_id) \
+ .filter(ts.Sample.test_id == testid).first()
+ else:
+ run2 = None
+ sample2 = None
+ except NoResultFound:
+ # FIXME: Make this a nicer error page.
+ abort(404)
+
+ if sample1.profile:
+ profile1 = sample1.profile
+ else:
+ profile1 = None
+
+ if sample2 and sample2.profile:
+ profile2 = sample2.profile
+ else:
+ profile2 = None
+
+ json_run1 = {
+ 'id': run1.id,
+ 'order': run1.order.llvm_project_revision,
+ 'machine': run1.machine.name,
+ 'sample': sample1.id if sample1 else None
+ }
+ if run2:
+ json_run2 = {
+ 'id': run2.id,
+ 'order': run2.order.llvm_project_revision,
+ 'machine': run2.machine.name,
+ 'sample': sample2.id if sample2 else None
+ }
+ else:
+ json_run2 = {}
+ urls = {
+ 'search': v4_url_for('v4_profile_search'),
+ 'singlerun_template': v4_url_for('v4_profile_fwd',
+ testid=1111,
+ run1_id=2222) \
+ .replace('1111', '<testid>').replace('2222', '<run1id>'),
+ 'comparison_template': v4_url_for('v4_profile_fwd2',
+ testid=1111,
+ run1_id=2222,
+ run2_id=3333) \
+ .replace('1111', '<testid>').replace('2222', '<run1id>') \
+ .replace('3333', '<run2id>'),
+
+ 'getTopLevelCounters': v4_url_for('v4_profile_ajax_getTopLevelCounters'),
+ 'getFunctions': v4_url_for('v4_profile_ajax_getFunctions'),
+ 'getCodeForFunction': v4_url_for('v4_profile_ajax_getCodeForFunction'),
+
+ }
+ return render_template("v4_profile.html",
+ ts=ts, test=test,
+ run1=json_run1, run2=json_run2,
+ urls=urls)
+
+ at v4_route("/profile/search")
+def v4_profile_search():
+ def _isint(i):
+ try:
+ int(i)
+ return True
+ except:
+ return False
+
+ ts = request.get_testsuite()
+ query = request.args.get('q')
+ l = request.args.get('l', 8)
+ #default_machine = request.args.get('m')
+
+ machine_queries = []
+ order_query = None
+ for q in query.split(' '):
+ if not q:
+ continue
+ if q.startswith('#'):
+ order_query = q[1:]
+ elif _isint(q):
+ order_query = q
+ else:
+ machine_queries.append(q)
+
+ if not machine_queries and order_query is None:
+ return "{}"
+
+ if machine_queries:
+ machines = []
+ for m in ts.query(ts.Machine).all():
+ if all(q in m.name for q in machine_queries):
+ machines.append(m.id)
+ if not machines:
+ return "{}"
+ else:
+ # FIXME:
+ return "{}"
+
+ q = ts.query(ts.Run).filter(ts.Run.machine_id.in_(machines))
+ if order_query:
+ # FIXME: Is this generating a million billion queries under my feet?
+ # I hate ORMs :( I know this column exists, but because it's
+ # dynamically generated SQLAlchemy can't filter on it.
+ # Perhaps there's a way to provide it a manual column name?
+ # I want to do this:
+ # filter(ts.Run.order.llvm_project_revision.like('%' + order_query + '%'))
+ oq = str(order_query)
+ data = [i
+ for i in q.all()
+ if oq in str(i.order.llvm_project_revision)]
+
+ else:
+ data = q.limit(l).all()
+
+ return json.dumps(
+ [('%s #%s' % (r.machine.name, r.order.llvm_project_revision),
+ r.id)
+ for r in data])
Added: lnt/trunk/lnt/server/ui/static/lnt_profile.css
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/static/lnt_profile.css?rev=263882&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/static/lnt_profile.css (added)
+++ lnt/trunk/lnt/server/ui/static/lnt_profile.css Sat Mar 19 09:47:04 2016
@@ -0,0 +1,103 @@
+
+/***********************************************************************/
+/* "Addon" support for placing an icon inside an input box
+
+/* enable absolute positioning */
+.inner-addon {
+ position: relative;
+}
+
+/* style icon */
+.inner-addon .icon {
+ position: absolute;
+/* padding: 10px;
+ width: 12px;
+ height: 12px;*/
+ pointer-events: none;
+}
+
+/* align icon */
+.left-addon .icon {
+ left: 4px;
+ top: 50%;
+ margin-top: -12px;
+}
+.right-addon .icon {
+ right: 4px;
+ top: 50%;
+ margin-top: -12px;
+}
+
+/* add padding */
+.left-addon input { padding-left: 22px; }
+.right-addon input { padding-right: 22px; }
+
+/***********************************************************************/
+/* Throbber ("Loading...") image/text */
+
+#throbber {
+ font-size: 10pt;
+ font-weight: normal;
+ color: gray;
+ display: none;
+}
+
+/***********************************************************************/
+/* Stats table (underneath the run/fn dropdowns) */
+
+#stats th, td {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+/* Dropdown */
+.fn-box-holder ul {
+ max-height: 200px;
+ overflow: scroll;
+ text-align: left;
+}
+
+/***********************************************************************/
+/* All rows (runs, stats and function) */
+
+.runrow {
+ border-top: 1px solid #ddd;
+ padding-top: 10px;
+ padding-bottom: 5px;
+}
+
+.statsrow {
+ background-color: #fafafa;
+ border: 1px solid #ddd;
+ border-left: none;
+ border-right: none;
+ padding-top: 10px;
+ padding-bottom: 5px;
+}
+
+.fnrow {
+ border-bottom: 1px solid #ddd;
+ padding-top: 10px;
+ padding-bottom: 5px;
+}
+
+/***********************************************************************/
+/* Profiles themselves */
+
+#profile1, #profile2 {
+ white-space: pre;
+ font-family: monospace;
+ overflow: scroll;
+}
+
+.address {
+ color: gray;
+ font-size: small;
+}
+
+.address a {
+ color: gray;
+}
+.address a:hover {
+ text-decoration: none;
+}
Added: lnt/trunk/lnt/server/ui/static/lnt_profile.js
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/static/lnt_profile.js?rev=263882&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/static/lnt_profile.js (added)
+++ lnt/trunk/lnt/server/ui/static/lnt_profile.js Sat Mar 19 09:47:04 2016
@@ -0,0 +1,710 @@
+//////////////////////////////////////////////////////////////////////
+// This script provides the functionality for the LNT profile page
+// (v4_profile.html).
+
+
+function Profile(element, runid, testid, unique_id) {
+ this.element = $(element);
+ this.runid = runid;
+ this.testid = testid;
+ this.unique_id = unique_id;
+ this.function_name = null;
+ $(element).html('<center><i>Select a run and function above<br> ' +
+ 'to view a performance profile</i></center>');
+}
+
+Profile.prototype = {
+ reset: function() {
+ $(this.element).empty();
+ $(this.element).html('<center><i>Select a run and function above<br> ' +
+ 'to view a performance profile</i></center>');
+ },
+ go: function(function_name, counter_name) {
+ this.counter_name = counter_name
+ if (this.function_name != function_name)
+ this._fetch_and_go(function_name);
+ else
+ this._go();
+ },
+
+ _fetch_and_go: function(fname, then) {
+ this.function_name = fname;
+ this_ = this;
+ $.ajax(g_urls.getCodeForFunction, {
+ dataType: "json",
+ data: {'runid': this.runid, 'testid': this.testid,
+ 'f': fname},
+ success: function(data) {
+ this_.data = data;
+ this_._go();
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ pf_flash_error('accessing URL ' + g_urls.getCodeForFunction +
+ '; ' + errorThrown);
+ }
+ });
+ },
+
+ _go: function() {
+ this.element.empty();
+ for (i in this.data) {
+ line = this.data[i];
+
+ row = $('<tr></tr>');
+
+ counter = this.counter_name;
+ if (counter in line[0] && line[0][counter] > 0.0)
+ row.append(this._labelTd(line[0][counter]));
+ else
+ row.append($('<td></td>'));
+
+ address = line[1].toString(16);
+ id = this.unique_id + address;
+ a = $('<a id="' + id + '" href="#' + id + '"></a>').text(address);
+ row.append($('<td></td>').addClass('address').append(a));
+ row.append($('<td></td>').text(line[2]));
+ this.element.append(row);
+ }
+ },
+
+ _labelTd: function(value) {
+ var labelPct = function(value) {
+ // Colour scheme: Black up until 1%, then yellow fading to red at 10%
+ var bg = '#fff';
+ var hl = '#fff';
+ if (value > 1.0 && value < 10.0) {
+ hue = lerp(50.0, 0.0, (value - 1.0) / 9.0);
+ bg = 'hsl(' + hue.toFixed(0) + ', 100%, 50%)';
+ hl = 'hsl(' + hue.toFixed(0) + ', 100%, 30%)';
+ } else if (value >= 10.0) {
+ bg = 'hsl(0, 100%, 50%)';
+ hl = 'hsl(0, 100%, 30%)';
+ }
+ return $('<td style="background-color:' + bg + '; border-right: 1px solid ' + hl + ';"></td>')
+ .text(value.toFixed(2) + '%');
+ }
+
+ // FIXME: Implement absolute numbering.
+ return labelPct(value);
+ },
+};
+
+function StatsBar(element, testid) {
+ this.element = $(element);
+ this.runid = null;
+ this.testid = testid;
+
+ $(element).html('<center><i>Select one or two runs above ' +
+ 'to view performance counters</i></center>');
+}
+
+StatsBar.prototype = {
+ go: function (runids) {
+ if (runids == this.runids)
+ return;
+ this.runids = runids;
+ this.element.empty();
+ var this_ = this;
+
+ $.ajax(g_urls.getTopLevelCounters, {
+ dataType: "json",
+ data: {'runids': this.runids.join(), 'testid': this.testid},
+ success: function(data) {
+ var t = $('<table align="center" valign="middle"></table>');
+ var r = $('<tr></tr>');
+ var i = 0;
+ for (counter in data) {
+ if (i > 0 && i % 4 == 0) {
+ t.append(r);
+ r = $('<tr></tr>');
+ }
+
+ var cell = this_._formatCell(counter, data[counter]);
+ r.append(cell);
+ }
+ if (r.children().length > 0)
+ t.append(r);
+ this_.element.html(t);
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ pf_flash_error('accessing URL ' + g_urls.getTopLevelCounters +
+ '; ' + errorThrown);
+ }
+ });
+
+ },
+
+ _formatCell: function(counter, values) {
+ if (values.length > 1) {
+ // We have both counters, so we can compare them.
+ var data1 = values[0];
+ var data2 = values[1];
+ percent = (data2 * 100.0 / data1) - 100.0;
+ if (percent > 0.0) {
+ // Make sure 2% is formatted as +2%.
+ percent = '+' + percent.toFixed(2);
+ } else if (percent < 0.0) {
+ percent = percent.toFixed(2);
+ }
+ // FIXME: Add colors here.
+ var element = '<th>'+counter+':</th>';
+ element += '<td>' + add_commas(data1) + '<br>' + add_commas(data2);
+ if (percent != 0.0)
+ element += ' (' + percent + '%)';
+ element += '</td>';
+ return $(element);
+ } else {
+ var element = '<th>'+counter+':</th>';
+ return $(element + '<td>' + add_commas(values[0]) + '</td>');
+ }
+ },
+};
+
+function RunTypeahead(element, options) {
+ this.element = element;
+ this.options = options;
+ this.id = null;
+
+ this_ = this;
+ element.typeahead({
+ source: function(query, process) {
+ $.ajax(options.searchURL, {
+ dataType: "json",
+ data: {'q': query},
+ success: function(data) {
+ process(data);
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ pf_flash_error('accessing URL ' + options.searchURL +
+ '; ' + errorThrown);
+ }
+ });
+ },
+ // These identity functions are required because the defaults
+ // assume items are strings, whereas they're objects here.
+ sorter: function(items) {
+ // The results should be sorted on the server.
+ return items;
+ },
+ matcher: function(item) {
+ return item;
+ },
+ updater: function(item) {
+ // FIXME: the item isn't passed in as json any more, it's
+ // been rendered. Lame. To get around this, hack the
+ // components of the 2-tuple back apart.
+ name = item.split(',')[0];
+ id = item.split(',')[1];
+ this_.id = id;
+
+ if (options.updated)
+ options.updated(name, id);
+ return name;
+ },
+ highlighter: function(item) {
+ // item is a 2-tuple [name, obj].
+ item = item[0];
+
+ // This loop highlights each search term (split by space)
+ // individually. The content of the for loop is lifted from
+ // bootstrap.js (it's the original implementation of
+ // highlighter()). In particular I have no clue what that regex
+ // is doing, so don't bother asking.
+ var arr = this.query.split(' ');
+ for (i in arr) {
+ query = arr[i];
+ if (!query)
+ continue;
+ var q = query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,
+ '\\$&')
+ item = item.replace(new RegExp('(' + q + ')', 'ig'), function ($1, match) {
+ // We want to replace with <strong>match</strong here,
+ // but it's possible another search term will then
+ // match part of <strong>.
+ //
+ // Therefore, replace with two replaceable tokens that
+ // no search query is very likely to match...
+ return '%%%' + match + '£££'
+ });
+ }
+ return item
+ .replace(/%%%/g, '<strong>')
+ .replace(/£££/g, '</strong>');
+ }
+ });
+ // Bind an event disabling the function box and removing the profile
+ // if the run box is emptied.
+ element.change(function() {
+ if (!element.val()) {
+ this_.id = null;
+ if (options.cleared)
+ options.cleared();
+ }
+ });
+}
+
+RunTypeahead.prototype = {
+ update: function (name, id) {
+ this.element.val(name);
+ this.id = id;
+ if (this.options.updated)
+ this.options.updated(name, id);
+ },
+ getSelectedRunId: function() {
+ return this.id;
+ }
+};
+
+function FunctionTypeahead(element, options) {
+ this.element = element;
+ this.options = options;
+ var _this = this;
+
+ element.typeahead({
+ minLength: 0,
+ items: 64,
+ source: _this._source,
+ matcher: function(item) {
+ // This is basically the same as typeahead.matcher(), apart
+ // from indexing into item[0] (as item is a 2-tuple
+ // [name, obj]).
+ return item[0].toLowerCase().indexOf(this.query) > -1;
+ },
+ sorter: function(items) {
+ // Sort items in descending order based on the value of the
+ // current counter.
+
+ c = options.getCounter();
+ return items.sort(function(a, b) {
+ // Note that this comparator needs to return -ve, 0, +ve,
+ // NOT boolean. Therefore subtracting one from the other
+ // gives the desired effect.
+ var aval = -1; // Make sure undefined values get sorted
+ var bval = -1; // to the end.
+ if ('counters' in a[1] && c in a[1].counters) {
+ aval = a[1].counters[c];
+ }
+ if ('counters' in b[1] && c in b[1].counters) {
+ bval = b[1].counters[c];
+ }
+ return bval - aval;
+ });
+ return items;
+ },
+ updater: function(item) {
+ // FIXME: the item isn't passed in as json any more, it's
+ // been rendered. Lame. Hack around this by splitting apart
+ // the ','-concatenated 2-tuple again.
+ fname = item.split(',')[0];
+
+ options.updated(fname);
+ return fname;
+ },
+ highlighter: function(item) {
+ // Highlighting functions is a bit arduous - do it in
+ // a helper function instead.
+ return _this._renderItem(item, this.query);
+ }
+ });
+ // A typeahead box will normally only offer suggestions when the input
+ // is non-empty (at least one character).
+ //
+ // As we want to provide a view on the functions without having to
+ // type anything (enumerate functions), add a focus handler to show
+ // the dropdown.
+ element.focus(function() {
+ // If the box is not empty, do nothing to avoid getting in the
+ // way of typeahead's own handlers.
+ if (!element.data().typeahead.$element.val())
+ element.data().typeahead.lookup();
+ });
+ // Given the above, this is a copy of typeahead.lookup() but with
+ // a check for "this.query != ''" removed, so lookups occur even with
+ // empty queries.
+ element.data().typeahead.lookup = function (event) {
+ this.query = this.$element.val();
+
+ var items = $.isFunction(this.source)
+ ? this.source(this.query, $.proxy(this.process, this))
+ : this.source;
+
+ return items ? this.process(items) : this;
+ };
+}
+
+FunctionTypeahead.prototype = {
+ update: function (name) {
+ this.element.val(name);
+ if (this.options.updated)
+ this.options.updated(name);
+ },
+ changeSourceRun: function(rid, tid) {
+ var this_ = this;
+ $.ajax(g_urls.getFunctions, {
+ dataType: "json",
+ data: {'runid': rid, 'testid': tid},
+ success: function(data) {
+ this_.data = data;
+
+ if (this_.options.sourceRunUpdated)
+ this_.options.sourceRunUpdated(data);
+ }
+ });
+ },
+ _source: function () {
+ console.log(this.$element.data('functionTypeahead').data);
+ return this.$element.data('functionTypeahead').data;
+ },
+ _renderItem: function (fn, query) {
+ // Given a function name and the current query, return HTML for putting in
+ // the function list dropdown.
+ name = fn[0];
+ counters = fn[1].counters;
+
+ selected_ctr = this.options.getCounter();
+ if (counters && selected_ctr in counters) {
+ // We have counter information, so show it as a badge.
+ //
+ // Make the badge's background color depend on the counter %age.
+ var value = counters[selected_ctr];
+
+ var bg = '#fff';
+ var hue = lerp(50.0, 0.0, value / 100.0);
+
+ bg = 'hsl(' + hue.toFixed(0) + ', 100%, 50%)';
+
+ counter_txt = '<span class="label label-inverse pull-left" ' +
+ 'style="background-color:' + bg + '; text-align: center; width: 40px; margin-right: 10px;">' + value.toFixed(1) + '%</span>';
+ } else {
+ // We don't have counter information :(
+ counter_txt = '<span class="label label-inverse pull-left" style="text-align: center; width: 40%; margin-right: 10px;">'
+ + '<i>no data</i></span>';
+ }
+
+ // This regex and code is taken from typeahead.highlighter(). If I knew
+ // how to call typeahead.highlighter() from here, I would.
+ var q = query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
+ name_txt = name.replace(new RegExp('(' + q + ')', 'ig'), function ($1, match) {
+ return '<strong>' + match + '</strong>'
+ });
+
+ return name_txt + counter_txt;
+ }
+};
+
+$(document).ready(function () {
+ jQuery.fn.extend({
+ profile: function(arg1, arg2, arg3) {
+ if (arg1 == 'go')
+ this.data('profile').go(arg2, arg3);
+ else if (arg1 && !arg2)
+ this.data('profile',
+ new Profile(this,
+ arg1.runid,
+ arg1.testid,
+ arg1.uniqueid));
+
+ return this.data('profile');
+ },
+ statsBar: function(arg1, arg2) {
+ if (arg1 == 'go')
+ this.data('statsBar').go(arg2);
+ else if (arg1 && !arg2)
+ this.data('statsBar',
+ new StatsBar(this,
+ arg1.testid));
+
+ return this.data('statsBar');
+ },
+ runTypeahead: function(options) {
+ if (options)
+ this.data('runTypeahead',
+ new RunTypeahead(this, options));
+ return this.data('runTypeahead');
+ },
+ functionTypeahead: function(options) {
+ if (options)
+ this.data('functionTypeahead',
+ new FunctionTypeahead(this, options));
+ return this.data('functionTypeahead');
+ }
+
+ });
+});
+
+//////////////////////////////////////////////////////////////////////
+// Global variables
+
+// A dict of URLs we want to AJAX to, by some identifying key. This allows
+// us to use v4_url_for() in profile_views.py and propagate that down to
+// JS without hackery.
+var g_urls;
+// The test ID - this remains constant.
+var g_testid;
+
+// pf_make_stub: Given a machine name and run order, make the stub
+// that goes in the "run" box (machine #order).
+function pf_make_stub(machine, order) {
+ return machine + " #" + order
+}
+
+// pf_init: Called with the request parameters to initialize the page.
+// This not only sets up defaults but also sets up the typeahead instances.
+function pf_init(run1, run2, testid, urls) {
+ g_urls = urls;
+
+ $('#fn1_box')
+ .prop('disabled', true)
+ .functionTypeahead({
+ getCounter: function() {
+ return pf_get_counter();
+ },
+ updated: function(fname) {
+ $('#profile1').profile('go', fname, pf_get_counter());
+ },
+ sourceRunUpdated: function(data) {
+ pf_set_default_counter(data);
+
+ var r1 = $('#run1_box').runTypeahead().getSelectedRunId();
+ var r2 = $('#run2_box').runTypeahead().getSelectedRunId();
+ var ids = [];
+ if (r1)
+ ids.push(r1);
+ if (r2)
+ ids.push(r2);
+
+ $('#fn1_box').prop('disabled', false);
+ $('#stats')
+ .statsBar({testid: testid})
+ .go(ids);
+ $('#profile1').profile({runid: r1,
+ testid: testid,
+ uniqueid: 'l'});
+ }
+ });
+
+ $('#fn2_box')
+ .prop('disabled', true)
+ .functionTypeahead({
+ getCounter: function() {
+ return pf_get_counter();
+ },
+ updated: function(fname) {
+ $('#profile2').profile('go', fname, pf_get_counter());
+ },
+ sourceRunUpdated: function(data) {
+ pf_set_default_counter(data);
+
+ var r1 = $('#run1_box').runTypeahead().getSelectedRunId();
+ var r2 = $('#run2_box').runTypeahead().getSelectedRunId();
+ var ids = [];
+ if (r1)
+ ids.push(r1);
+ if (r2)
+ ids.push(r2);
+
+ $('#fn2_box').prop('disabled', false);
+ $('#stats')
+ .statsBar({testid: testid})
+ .go(ids);
+ $('#profile2').profile({runid: r1,
+ testid: testid,
+ uniqueid: 'r'});
+
+ }
+ });
+
+ $('#run1_box')
+ .runTypeahead({
+ searchURL: g_urls.search,
+ updated: function(name, id) {
+ // Kick the functions dropdown to repopulate.
+ $('#fn1_box')
+ .functionTypeahead()
+ .changeSourceRun(id, testid);
+ pf_update_history();
+ },
+ cleared: function(name, id) {
+ $('#fn1_box').val('').prop('disabled', true);
+ $('#profile1').profile().reset();
+ }
+ })
+ .update(pf_make_stub(run1.machine, run1.order), run1.id);
+
+ var r2 = $('#run2_box')
+ .runTypeahead({
+ searchURL: g_urls.search,
+ updated: function(name, id) {
+ // Kick the functions dropdown to repopulate.
+ $('#fn2_box')
+ .functionTypeahead()
+ .changeSourceRun(id, testid);
+ pf_update_history();
+ },
+ cleared: function(name, id) {
+ $('#fn2_box').val('').prop('disabled', true);
+ $('#profile2').profile().reset();
+ }
+ });
+ if (!$.isEmptyObject(run2))
+ r2.update(pf_make_stub(run2.machine, run2.order), run2.id);
+
+ // Bind change events for the counter dropdown so that profiles are
+ // updated when it is modified.
+ $('#counters').change(function () {
+ g_counter = $('#counters').val();
+ if ($('#fn1_box').val())
+ $('#profile1').profile('go', $('#fn1_box').val(), g_counter);
+ if ($('#fn2_box').val())
+ $('#profile2').profile('go', $('#fn2_box').val(), g_counter);
+ });
+
+ // FIXME: Implement navigating to an address properly.
+ // var go_to_hash = function () {
+ // s = document.location.hash.substring(1);
+
+ // var element = $('#address' + s);
+ // var header_offset = $('#header').height();
+ // $('html, body').animate({
+ // scrollTop: element.offset().top - header_offset
+ // }, 500);
+ // };
+}
+
+var g_throbber_count = 0;
+// pf_ajax_takeoff - An ajax request has started. Show the throbber if it
+// wasn't shown before.
+function pf_ajax_takeoff() {
+ g_throbber_count ++;
+ if (g_throbber_count == 1) {
+ $('#throbber').show();
+ }
+}
+// pf_ajax_land - An ajax request has finished (success or failure). If
+// there are no more ajax requests in flight (flight! get it? take off,
+// land? ha!), hide the throbber.
+function pf_ajax_land() {
+ g_throbber_count --;
+ if (g_throbber_count == 0) {
+ $('#throbber').hide();
+ }
+}
+
+// pf_flash_error - show an error message, dismissable by the user.
+function pf_flash_error(msg) {
+ txt = '<div class="alert alert-error">' +
+ '<button type="button" class="close" data-dismiss="alert">×</button>' +
+ '<strong>Error</strong> ' + msg + '</div>';
+ $('#flashes').append(txt);
+}
+
+// pf_flash_warning - show a warning message, dismissable by the user.
+function pf_flash_warning(msg) {
+ txt = '<div class="alert">' +
+ '<button type="button" class="close" data-dismiss="alert">×</button>' +
+ '<strong>Warning</strong> ' + msg + '</div>';
+ $('#flashes').append(txt);
+}
+
+var g_counter;
+var g_all_counters = [];
+// FIXME: misnomer?
+// pf_set_default_counter - set g_all_counters to all unique performance
+// counters found in 'data'.
+//
+// If g_counter is not yet set, select a default counter and set it.
+function pf_set_default_counter(data) {
+
+ var all_counters = g_all_counters.slice(); // Copy
+ // Ghetto solution for creating a set. ES5 Set doesn't appear to be
+ // available on Chrome yet.
+ for (i in data) {
+ f = data[i][1];
+ for (j in f.counters) {
+ all_counters.push(j);
+ }
+ }
+ // FIXME: Replace with a sort_and_unique() method? that'd be more
+ // efficient.
+ all_counters = unique_array(all_counters);
+ all_counters.sort();
+
+ // Only perform any updates if the counters have changed.
+ if (g_all_counters != all_counters) {
+ // Blow away all previous counter options and re-add them.
+ box = $('#counters').empty();
+ for (i in all_counters) {
+ var ctr = all_counters[i];
+ box.append(
+ $('<option></option>').text(ctr)
+ );
+ }
+ // Re-select the previous value if it existed.
+ if (g_counter != null) {
+ box.val(g_counter);
+ }
+
+ g_all_counters = all_counters;
+ }
+
+ if (g_counter == null) {
+ // Select a default. If 'cycles' exists, we pick that, else we
+ // pick the first we see.
+ if (g_all_counters.indexOf('cycles') != -1)
+ g_counter = 'cycles';
+ else
+ g_counter = g_all_counters[0];
+ $('#counters').val(g_counter);
+ }
+}
+
+// pf_get_counter - Poor encapsulation of the g_counter object.
+function pf_get_counter() {
+ return g_counter;
+}
+
+// pf_update_history - Push a new history entry, as we've just navigated
+// to what could be a new bookmarkable page.
+function pf_update_history() {
+ var url;
+ if (g_runids[1]) {
+ url = g_urls.comparison_template
+ .replace('<testid>', g_testid)
+ .replace('<run1id>', g_runids[0])
+ .replace('<run2id>', g_runids[1]);
+ } else {
+ url = g_urls.singlerun_template
+ .replace('<testid>', g_testid)
+ .replace('<run1id>', g_runids[0]);
+ }
+ history.pushState({}, document.title, url);
+}
+
+//////////////////////////////////////////////////////////////////////
+// Helper functions
+
+function unique_array(a) {
+ var unique = [];
+ for (var i = 0; i < a.length; i++) {
+ if (unique.indexOf(a[i]) == -1) {
+ unique.push(a[i]);
+ }
+ }
+ return unique;
+}
+
+function add_commas(nStr) {
+ nStr += '';
+ x = nStr.split('.');
+ x1 = x[0];
+ x2 = x.length > 1 ? '.' + x[1] : '';
+ var rgx = /(\d+)(\d{3})/;
+ while (rgx.test(x1)) {
+ x1 = x1.replace(rgx, '$1' + ',' + '$2');
+ }
+ return x1 + x2;
+}
+
+function lerp(s, e, x) {
+ return s + (e - s) * x;
+}
More information about the llvm-commits
mailing list