summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorprimiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-03-12 21:13:34 +0000
committerprimiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-03-12 21:13:34 +0000
commitc2028b5519fd3bf95c864e6e0c12b1c946b7220f (patch)
treea5e6fb2d073e5c2a04388f0a5708e80c00c01af4 /tools
parentfba1eb214a613eefad7dde40eefbb50a12320a45 (diff)
downloadchromium_src-c2028b5519fd3bf95c864e6e0c12b1c946b7220f.zip
chromium_src-c2028b5519fd3bf95c864e6e0c12b1c946b7220f.tar.gz
chromium_src-c2028b5519fd3bf95c864e6e0c12b1c946b7220f.tar.bz2
Add HTML frontend to memory_inspector.
This adds the basic infrastructure for the web based ui: - A python-based www server. - The HTML/JS client. At the current state, the only functionality available is listing processes/device stats. BUG=340294 NOTRY=true Review URL: https://codereview.chromium.org/190853010 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@256646 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools')
-rw-r--r--tools/memory_inspector/memory_inspector/core/backends.py5
-rw-r--r--tools/memory_inspector/memory_inspector/data/file_storage.py3
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/__init__.py0
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/index.html159
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/js/devices.js79
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js183
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js41
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/js/timers.js38
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/js/webservice.js30
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css143
-rw-r--r--tools/memory_inspector/memory_inspector/frontends/www_server.py311
-rwxr-xr-xtools/memory_inspector/start_web_ui17
12 files changed, 1008 insertions, 1 deletions
diff --git a/tools/memory_inspector/memory_inspector/core/backends.py b/tools/memory_inspector/memory_inspector/core/backends.py
index 747875a..aa4c6c6 100644
--- a/tools/memory_inspector/memory_inspector/core/backends.py
+++ b/tools/memory_inspector/memory_inspector/core/backends.py
@@ -11,6 +11,11 @@ def Register(backend):
_backends[backend.name] = backend
+def ListBackends():
+ """Enumerates all the backends."""
+ return _backends.itervalues()
+
+
def ListDevices():
"""Enumerates all the devices from all the registered backends."""
for backend in _backends.itervalues():
diff --git a/tools/memory_inspector/memory_inspector/data/file_storage.py b/tools/memory_inspector/memory_inspector/data/file_storage.py
index 407076f..ff888ba 100644
--- a/tools/memory_inspector/memory_inspector/data/file_storage.py
+++ b/tools/memory_inspector/memory_inspector/data/file_storage.py
@@ -48,7 +48,8 @@ class Storage(object):
assert(isinstance(settings, dict))
file_path = os.path.join(self._root, Storage._SETTINGS_FILE % name)
if not settings:
- os.unlink(file_path)
+ if os.path.exists(file_path):
+ os.unlink(file_path)
return
with open(file_path, 'w') as f:
return json.dump(settings, f)
diff --git a/tools/memory_inspector/memory_inspector/frontends/__init__.py b/tools/memory_inspector/memory_inspector/frontends/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/__init__.py
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/index.html b/tools/memory_inspector/memory_inspector/frontends/www_content/index.html
new file mode 100644
index 0000000..eaf8a3f
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/index.html
@@ -0,0 +1,159 @@
+<!doctype html>
+<!--
+ -- Copyright 2014 The Chromium Authors. All rights reserved.
+ -- Use of this source code is governed by a BSD-style license that can be
+ -- found in the LICENSE file.
+ -->
+<html lang="en-us">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+ <title>Memory Inspector</title>
+ <link href='//fonts.googleapis.com/css?family=Coda' rel='stylesheet' type='text/css'>
+ <link href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/flick/jquery-ui.css" rel="stylesheet">
+ <link href="/rootUi.css" rel="stylesheet" type="text/css">
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script>
+ <script src="//www.google.com/jsapi"></script>
+ <script type="text/javascript">
+ google.load('visualization', '1',
+ { packages: ['corechart', 'table', 'orgchart', 'treemap'] });
+ </script>
+ <script src="/js/devices.js"></script>
+ <script src="/js/rootUi.js"></script>
+ <script src="/js/processes.js"></script>
+ <script src="/js/timers.js"></script>
+ <script src="/js/webservice.js"></script>
+</head>
+<body>
+ <div id="wrapper">
+ <h1>Memory Inspector</h1>
+ <div id="tabs">
+ <ul>
+ <li><a href="#tabs-ps">Processes</a></li>
+ <li><a href="#tabs-mm">Aggregated Memory maps</a></li>
+ <li><a href="#tabs-mm-table">Memory maps table</a></li>
+ <li><a href="#tabs-native_alloc">Aggregated native allocs.</a></li>
+ <li><a href="#tabs-archive">Archive</a></li>
+ <li><a href="#tabs-settings">Settings</a></li>
+ </ul>
+
+ <div id="tabs-ps">
+ <div>
+ Device:
+ <select id="devices"></select>
+ <input type="button" id="refresh-devices" value="&#10226;">
+ </div>
+ <div id="device_tabs">
+ <ul>
+ <li><a href="#device_tabs-osstats">Device stats</a></li>
+ <li><a href="#device_tabs-procstats">Selected process stats</a></li>
+ </ul>
+ <div id="device_tabs-osstats">
+ <div id="os-mem_chart"></div>
+ <div id="os-cpu_chart"></div>
+ </div>
+ <div id="device_tabs-procstats">
+ <div id="proc-cpu_chart"></div>
+ <div id="proc-mem_chart"></div>
+ </div>
+ </div>
+
+ <div id="ps-table-wrapper">
+ <input type="checkbox" id="ps-show_all">
+ <label for="ps-show_all">Show all system processes</label>
+ <div id="ps-table"></div>
+ </div>
+ </div>
+
+ <div id="tabs-mm">
+ <header id="mm-options">
+ <span>
+ Current metric:
+ <select id="mm-cur-serie"></select>
+ </span>
+ <span>
+ Current snapshot: <select id="mm-cur-snap"></select>
+ of <span id="mm-nsnapshots"></span>
+ </span>
+ </header>
+ <h2>Hierarchical view of selected snapshot</h2>
+ <div id="mm-chart-hierarchy"></div>
+
+ <div id="mm-chart-area"></div>
+
+ <div id="mm-chart-treemap"></div>
+
+ </div>
+
+ <div id="tabs-mm-table">
+ <div id="mm-table-wrapper">
+ <header>
+ <b>Filters: </b>
+ <span>
+ Prot. flags: <input type="text" id="mm-filter-prot" />
+ File name: <input type="text" id="mm-filter-file" />
+ </span>
+ </header>
+ <header>
+ <b>Totals: </b>
+ <b>Priv dirty (Kb): </b><span id="memmaps-totals-priv-dirty">0</span>
+ <b>Priv clean (Kb): </b><span id="memmaps-totals-priv-clean">0</span>
+ <b>Shared dirty (Kb): </b><span id="memmaps-totals-shared-dirty">0</span>
+ <b>Shared clean (Kb): </b><span id="memmaps-totals-shared-clean">0</span>
+ </header>
+ <header>
+ Note: totals from this filtered table might not match the totals in the treemap, as table filtering is not hierarchical.
+ </header>
+ <div id="mm-table"></div>
+ </div>
+ </div>
+
+ <div id="tabs-native_alloc">
+ <header id="nh-options">
+ <span>
+ Current metric:
+ <select id="nh-cur-serie"></select>
+ </span>
+ <span>
+ Current snapshot: <select id="nh-cur-snap"></select>
+ of <span id="nh-nsnapshots"></span>
+ </span>
+ </header>
+ <h2>Hierarchical view of selected snapshot</h2>
+ <div id="nh-chart-hierarchy"></div>
+ <div id="nh-chart-area"></div>
+ <div id="nh-chart-treemap"></div>
+ <div id="native_alloc_chart"></div>
+ <div id="native_alloc_table"></div>
+ </div>
+
+ <div id="tabs-archive">
+ <input type="button" value="Refresh" id="archive-refresh" />
+ <input type="button" value="Analyze Memory maps" id="archive-classify_mmaps" />
+ <input type="button" value="Analyze Native Heap" id="archive-classify_native" />
+ <div id="archive">
+ </div>
+ </div>
+
+ <div id="tabs-settings">
+ <header>
+ <input type="button" value="Reload" id="settings-load" />
+ <input type="button" value="Store" id="settings-store" />
+ </header>
+ <div id="settings-container">
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="status_bar">
+ <div id="status_messages"></div>
+ <div id="progress_bar"><div id="progress_bar-label">Progress...</div></div>
+ </div>
+
+ <div id="js_loading_banner">
+ Loading JavaScript content. If you see this message something has probably gone wrong. Check JS console.
+ </div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/devices.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/devices.js
new file mode 100644
index 0000000..b4c4ce2
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/devices.js
@@ -0,0 +1,79 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+devices = new (function() {
+
+this.backends_ = []; // ['Android', 'Linux']
+this.devices_ = {}; // 'Android/a1b2' -> {.backend, .name, .id}
+this.selDeviceUri_ = null;
+
+this.onDomReady_ = function() {
+ $('#refresh-devices').on('click', this.refresh.bind(this));
+ $('#devices').on('change', this.onDeviceSelectionChange_.bind(this));
+ this.refresh();
+};
+
+this.getSelectedURI = function() {
+ return this.selDeviceUri_;
+};
+
+this.getAllBackends = function() {
+ // Returns a list of the registered backends, e.g., ['Android', 'Linux'].
+ return this.backends_;
+};
+
+this.getAllDevices = function() {
+ // Returns a list of devices [{backend:'Android', name:'N7', id:'1234'}].
+ return Object.keys(this.devices_).map(function(k) {
+ return this.devices_[k];
+ }, this);
+};
+
+this.refresh = function() {
+ webservice.ajaxRequest('/devices', this.onDevicesAjaxResponse_.bind(this));
+ webservice.ajaxRequest('/backends', this.onBackendsAjaxResponse_.bind(this));
+};
+
+this.onBackendsAjaxResponse_ = function(data) {
+ if (!data || !data.length)
+ {
+ rootUi.ShowDialog('No backends detected! Memory Inspector looks terribly' +
+ ' broken. Please file a bug');
+ }
+ this.backends_ = data;
+};
+
+this.onDevicesAjaxResponse_ = function(data) {
+ var devList = $('#devices');
+ devList.empty();
+ this.devices_ = {};
+ data.forEach(function(device) {
+ var deviceUri = device.backend + '/' + device.id;
+ var deviceFullTime = device.backend + ' : ' +
+ device.name + ' [' + device.id + ']';
+ devList.append($('<option/>').val(deviceUri).text(deviceFullTime));
+ this.devices_[deviceUri] = device;
+ }, this);
+
+ this.onDeviceSelectionChange_(); // start monitoring the first device.
+};
+
+this.onDeviceSelectionChange_ = function() {
+ this.selDeviceUri_ = $('#devices').val();
+ if (!this.selDeviceUri_)
+ return;
+
+ // Initialize device and start processes / OS stats.
+ webservice.ajaxRequest('/initialize/' + this.selDeviceUri_,
+ this.onDeviceInitializationComplete_.bind(this));
+};
+
+this.onDeviceInitializationComplete_ = function() {
+ processes.startPsTable();
+ processes.startDeviceStats();
+};
+
+$(document).ready(this.onDomReady_.bind(this));
+
+})(); \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js
new file mode 100644
index 0000000..08cc48a
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js
@@ -0,0 +1,183 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+processes = new (function() {
+
+this.PS_INTERVAL_SEC_ = 2;
+this.DEV_STATS_INTERVAL_SEC_ = 2;
+this.PROC_STATS_INTERVAL_SEC_ = 1;
+
+this.selProcUri_ = null;
+this.psTable_ = null;
+this.psTableData_ = null;
+this.memChart_ = null;
+this.memChartData_ = null;
+this.cpuChart_ = null;
+this.cpuChartData_ = null;
+this.procCpuChart_ = null;
+this.procCpuChartData_ = null;
+this.procMemChart_ = null;
+this.procMemChartData_ = null;
+
+this.onDomReady_ = function() {
+ $('#device_tabs').tabs();
+ $('#device_tabs').on('tabsactivate', this.redrawPsStats_.bind(this));
+ $('#device_tabs').on('tabsactivate', this.redrawDevStats_.bind(this));
+};
+
+this.getSelectedProcessURI = function() {
+ return this.selProcUri_;
+};
+
+this.refreshPsTable = function() {
+ var targetDevUri = devices.getSelectedURI();
+ if (!targetDevUri)
+ return this.stopPsTable();
+
+ if (!this.psTable_) {
+ this.psTable_ = new google.visualization.Table($('#ps-table')[0]);
+ google.visualization.events.addListener(
+ this.psTable_, 'select', this.onPsTableRowSelect_.bind(this));
+ };
+
+ var showAllParam = $('#ps-show_all').prop('checked') ? '?all=1' : '';
+ webservice.ajaxRequest('/ps/' + targetDevUri + showAllParam,
+ this.onPsAjaxResponse_.bind(this),
+ this.stopPsTable.bind(this));
+};
+
+this.startPsTable = function() {
+ timers.start('ps_table',
+ this.refreshPsTable.bind(this),
+ this.PS_INTERVAL_SEC_);
+};
+
+this.stopPsTable = function() {
+ this.selProcUri_ = null;
+ timers.stop('ps_table');
+};
+
+this.onPsTableRowSelect_ = function() {
+ var targetDevUri = devices.getSelectedURI();
+ if (!targetDevUri)
+ return;
+
+ var sel = this.psTable_.getSelection();
+ if (!sel.length || !this.psTableData_)
+ return;
+ var pid = this.psTableData_.getValue(sel[0].row, 0);
+ this.selProcUri_ = targetDevUri + '/' + pid;
+ this.startSelectedProcessStats();
+};
+
+this.onPsAjaxResponse_ = function(data) {
+ // Redraw table preserving sorting info.
+ var sort = this.psTable_.getSortInfo() || {column: -1, ascending: false};
+ this.psTableData_ = new google.visualization.DataTable(data);
+ this.psTable_.draw(this.psTableData_, {sortColumn: sort.column,
+ sortAscending: sort.ascending});
+};
+
+this.refreshDeviceStats = function() {
+ var targetDevUri = devices.getSelectedURI();
+ if (!targetDevUri)
+ return this.stopDeviceStats();
+
+ webservice.ajaxRequest('/stats/' + targetDevUri,
+ this.onDevStatsAjaxResponse_.bind(this),
+ this.stopDeviceStats.bind(this));
+};
+
+this.startDeviceStats = function() {
+ timers.start('device_stats',
+ this.refreshDeviceStats.bind(this),
+ this.DEV_STATS_INTERVAL_SEC_);
+};
+
+
+this.stopDeviceStats = function() {
+ timers.stop('device_stats');
+};
+
+this.onDevStatsAjaxResponse_ = function(data) {
+ this.memChartData_ = new google.visualization.DataTable(data.mem);
+ this.cpuChartData_ = new google.visualization.DataTable(data.cpu);
+ this.redrawDevStats_();
+};
+
+this.redrawDevStats_ = function(data) {
+ if (!this.memChartData_ || !this.cpuChartData_)
+ return;
+
+ if (!this.memChart_) {
+ this.memChart_ = new google.visualization.PieChart($('#os-mem_chart')[0]);
+ this.cpuChart_ = new google.visualization.BarChart($('#os-cpu_chart')[0]);
+ }
+
+ this.memChart_.draw(this.memChartData_,
+ {title: 'System Memory Usage (MB)', is3D: true});
+ this.cpuChart_.draw(this.cpuChartData_,
+ {title: 'CPU Usage',
+ isStacked: true,
+ hAxis: {maxValue: 100, viewWindow: {max: 100}}});
+};
+
+this.refreshSelectedProcessStats = function() {
+ if (!this.selProcUri_)
+ return this.stopSelectedProcessStats();
+
+ webservice.ajaxRequest('/stats/' + this.selProcUri_,
+ this.onPsStatsAjaxResponse_.bind(this),
+ this.stopSelectedProcessStats.bind(this));
+};
+
+this.startSelectedProcessStats = function() {
+ timers.start('proc_stats',
+ this.refreshSelectedProcessStats.bind(this),
+ this.PROC_STATS_INTERVAL_SEC_);
+ $('#device_tabs').tabs('option', 'active', 1);
+};
+
+this.stopSelectedProcessStats = function() {
+ timers.stop('proc_stats');
+};
+
+this.onPsStatsAjaxResponse_ = function(data) {
+ this.procCpuChartData_ = new google.visualization.DataTable(data.cpu);
+ this.procMemChartData_ = new google.visualization.DataTable(data.mem);
+ this.redrawPsStats_();
+};
+
+this.redrawPsStats_ = function() {
+ if (!this.procCpuChartData_ || !this.procMemChartData_)
+ return;
+
+ if (!this.procCpuChart_) {
+ this.procCpuChart_ =
+ new google.visualization.ComboChart($('#proc-cpu_chart')[0]);
+ this.procMemChart_ =
+ new google.visualization.ComboChart($('#proc-mem_chart')[0]);
+ }
+
+ this.procCpuChart_.draw(this.procCpuChartData_, {
+ title: 'CPU stats for ' + this.selProcUri_,
+ seriesType: 'line',
+ vAxes: {0: {title: 'CPU %', maxValue: 100}, 1: {title: '# Threads'}},
+ series: {1: {type: 'bars', targetAxisIndex: 1}},
+ hAxis: {title: 'Run Time'},
+ legend: {alignment: 'end'},
+ });
+ this.procMemChart_.draw(this.procMemChartData_, {
+ title: 'Memory stats for ' + this.selProcUri_,
+ seriesType: 'line',
+ vAxes: {0: {title: 'VM Rss KB'}, 1: {title: '# Page Faults'}},
+ series: {1: {type: 'bars', targetAxisIndex: 1}},
+ hAxis: {title: 'Run Time'},
+ legend: {alignment: 'end'},
+ });
+};
+
+$(document).ready(this.onDomReady_.bind(this));
+
+})(); \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js
new file mode 100644
index 0000000..94d4d30
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js
@@ -0,0 +1,41 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+rootUi = new (function() {
+
+this.onDomReady_ = function() {
+ $('#js_loading_banner').hide();
+ $('#tabs').tabs();
+ $('#tabs').css('visibility', 'visible');
+
+ // Initialize the status bar.
+ var statusBar = $('#statusBar');
+ var statusMessages = $('#statusMessages');
+ statusMessages.mouseenter(function() {
+ statusBar.addClass('expanded');
+ statusMessages.scrollTop(statusMessages.height());
+ });
+ statusMessages.mouseleave(function() {
+ statusBar.removeClass('expanded');
+ });
+
+ var progressBar = $('#progressBar');
+ var progressLabel = $('#progressBar-label');
+ progressBar.progressbar({
+ value: 1,
+ change: function() {
+ progressLabel.text(progressBar.progressbar('value') + '%' );
+ }
+ });
+};
+
+this.showTab = function(tabId) {
+ var index = $('#tabs-' + tabId).index();
+ if (index > 0)
+ $('#tabs').tabs('option', 'active', index - 1);
+};
+
+$(document).ready(this.onDomReady_.bind(this));
+
+})(); \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/timers.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/timers.js
new file mode 100644
index 0000000..b269557
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/timers.js
@@ -0,0 +1,38 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+timers = new (function() {
+
+this.timers_ = {};
+
+this.start = function(name, callback, intervalSeconds) {
+ this.stop(name);
+
+ var timerId = setInterval(callback, intervalSeconds * 1000);
+
+ this.timers_[name] = {
+ name: name,
+ callback: callback,
+ timerId: timerId,
+ intervalSeconds: intervalSeconds
+ };
+
+ callback();
+};
+
+this.stop = function(name) {
+ if (name in this.timers_) {
+ clearInterval(this.timers_[name].timerId);
+ delete this.timers_[name];
+ }
+};
+
+this.stopAll = function() {
+ for (var name in this.timers_) {
+ clearInterval(this.timers_[name].timerId);
+ }
+ this.timers_ = {};
+};
+
+})(); \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/webservice.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/webservice.js
new file mode 100644
index 0000000..437d3a4
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/webservice.js
@@ -0,0 +1,30 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+webservice = new (function() {
+
+this.AJAX_BASE_URL_ = '/ajax';
+
+this.ajaxRequest = function(path, responseCallback, errorCallback, postArgs) {
+ var reqType = postArgs ? 'POST' : 'GET';
+ var reqData = postArgs ? JSON.stringify(postArgs) : '';
+
+ $.ajax({
+ url: this.AJAX_BASE_URL_ + path,
+ type: reqType,
+ data: reqData,
+ success: responseCallback,
+ dataType: 'json',
+ error: function (xhr, ajaxOptions, thrownError) {
+ console.log('------------------------');
+ console.log('AJAX error (req: ' + path + ').');
+ console.log('HTTP response: ' + xhr.status + ' ' + thrownError);
+ console.log(xhr.responseText);
+ if (errorCallback)
+ errorCallback(xhr.status, xhr.responseText);
+ }
+ });
+};
+
+})(); \ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css b/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css
new file mode 100644
index 0000000..80b805d
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css
@@ -0,0 +1,143 @@
+/* Copyright 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+html {
+ font-family: Arial, sans-serif;
+ background: #f2f2f2;
+ font-size: 14px;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+body {
+ margin-bottom: 3em;
+}
+
+h1 {
+ text-align: center;
+ font-size: 2.5em;
+ font-family: 'Coda', sans-serif;
+ text-shadow: 0.1em 0.1em 0.2em #666;
+ margin: 0.5em;
+ line-height: 1em;
+}
+
+input[type="text"] {
+ border: 1px solid #999;
+ box-shadow: inset -1px -1px 7px #ddd;
+}
+
+#load_banner {
+ text-align: center;
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 20em;
+ z-index: -1;
+}
+
+#wrapper {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 2em;
+ overflow: auto;
+}
+
+#tabs {
+ width: 90%;
+ margin: 1em auto;
+ position: relative;
+ box-shadow: 0 0 1.5em #999;
+ visibility: hidden; /* Will be shown by JS after loading. */
+}
+
+#tabs > div > div {
+ margin-bottom: 1em;
+}
+
+#tabs > div header {
+ margin-bottom: 0.5em;
+}
+
+#status_bar {
+ display: block;
+ position: fixed;
+ bottom: 0;
+ top: auto;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 2em;
+ line-height: 2em;
+ overflow: hidden;
+ background: #333;
+ color: #eee;
+ font-family: monospace;
+ z-index: 10;
+ margin: 0;
+ padding: 0;
+ box-shadow: 0 0 5px #333;
+}
+
+#progress_bar {
+ position: absolute;
+ width: 20%;
+ top: 3px;
+ right: 3px;
+ bottom: 3px;
+ height: auto;
+}
+
+#progress_bar-label {
+ position: absolute;
+ left: 0;
+ right: 0;
+ width: auto;
+ font-weight: bold;
+ color: #444;
+ text-align: center;
+ line-height: 1.5em;
+}
+
+#status_messages {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 80%;
+ background: rgba(50,50,50,0.8);
+ color: #0e0;
+ font-family: monospace;
+ white-space: pre;
+ padding: 0 0.5em;
+}
+
+#status_bar.expanded { overflow: visible; }
+
+#status_bar.expanded #status_messages {
+ position: fixed;
+ z-index: 21;
+ left: 0;
+ right: 0;
+ width: auto;
+ height: auto;
+ max-height: 30%;
+ bottom: 0;
+ overflow: auto;
+ line-height: 1.5em;
+}
+
+#os-mem_chart,
+#os-cpu_chart {
+ width: 49%;
+ max-width: 49%;
+ margin: 0;
+ display: inline-block;
+ height: 20em;
+}
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_server.py b/tools/memory_inspector/memory_inspector/frontends/www_server.py
new file mode 100644
index 0000000..3ba68e9
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_server.py
@@ -0,0 +1,311 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module implements a simple WSGI server for the memory_inspector Web UI.
+
+The WSGI server essentially handles two kinds of requests:
+ - /ajax/foo/bar: The AJAX endpoints which exchange JSON data with the JS.
+ Requests routing is achieved using a simple @uri decorator which simply
+ performs regex matching on the request path.
+ - /static/content: Anything not matching the /ajax/ prefix is treated as a
+ static content request (for serving the index.html and JS/CSS resources).
+
+The following HTTP status code are returned by the server:
+ - 200 - OK: The request was handled correctly.
+ - 404 - Not found: None of the defined handlers did match the /request/path.
+ - 410 - Gone: The path was matched but the handler returned an empty response.
+ This typically happens when the target device is disconnected.
+"""
+
+import collections
+import datetime
+import os
+import mimetypes
+import json
+import re
+import urlparse
+import wsgiref.simple_server
+
+from memory_inspector.core import backends
+from memory_inspector.data import serialization
+from memory_inspector.data import file_storage
+
+
+_HTTP_OK = '200 - OK'
+_HTTP_GONE = '410 - Gone'
+_HTTP_NOT_FOUND = '404 - Not Found'
+_PERSISTENT_STORAGE_PATH = os.path.join(
+ os.path.expanduser('~'), '.config', 'memory_inspector')
+_CONTENT_DIR = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), 'www_content'))
+_APP_PROCESS_RE = r'^[\w.:]+$' # Regex for matching app processes.
+_STATS_HIST_SIZE = 120 # Keep at most 120 samples of stats per process.
+
+_persistent_storage = file_storage.Storage(_PERSISTENT_STORAGE_PATH)
+_proc_stats_history = {} # /Android/device/PID -> deque([stats@T=0, stats@T=1])
+
+
+class UriHandler(object):
+ """Base decorator used to automatically route /requests/by/path.
+
+ Each handler is called with the following args:
+ args: a tuple of the matching regex groups.
+ req_vars: a dictionary of request args (querystring for GET, body for POST).
+ Each handler must return a tuple with the following elements:
+ http_code: a string with the HTTP status code (e.g., '200 - OK')
+ headers: a list of HTTP headers (e.g., [('Content-Type': 'foo/bar')])
+ body: the HTTP response body.
+ """
+ _handlers = []
+
+ def __init__(self, path_regex, verb='GET', output_filter=None):
+ self._path_regex = path_regex
+ self._verb = verb
+ default_output_filter = lambda *x: x # Just return the same args unchanged.
+ self._output_filter = output_filter or default_output_filter
+
+ def __call__(self, handler):
+ UriHandler._handlers += [(
+ self._verb, self._path_regex, self._output_filter, handler)]
+
+ @staticmethod
+ def Handle(method, path, req_vars):
+ """Finds a matching handler and calls it (or returns a 404 - Not Found)."""
+ for (match_method, path_regex, output_filter, fn) in UriHandler._handlers:
+ if method != match_method:
+ continue
+ m = re.match(path_regex, path)
+ if not m:
+ continue
+ (http_code, headers, body) = fn(m.groups(), req_vars)
+ return output_filter(http_code, headers, body)
+ return (_HTTP_NOT_FOUND, [], 'No AJAX handlers found')
+
+
+class AjaxHandler(UriHandler):
+ """Decorator for routing AJAX requests.
+
+ This decorator essentially groups the JSON serialization and the cache headers
+ which is shared by most of the handlers defined below.
+ """
+ def __init__(self, path_regex, verb='GET'):
+ super(AjaxHandler, self).__init__(
+ path_regex, verb, AjaxHandler.AjaxOutputFilter)
+
+ @staticmethod
+ def AjaxOutputFilter(http_code, headers, body):
+ serialized_content = json.dumps(body, cls=serialization.Encoder)
+ extra_headers = [('Cache-Control', 'no-cache'),
+ ('Expires', 'Fri, 19 Sep 1986 05:00:00 GMT')]
+ return http_code, headers + extra_headers, serialized_content
+
+@AjaxHandler('/ajax/backends')
+def _ListBackends(args, req_vars): # pylint: disable=W0613
+ return _HTTP_OK, [], [backend.name for backend in backends.ListBackends()]
+
+
+@AjaxHandler('/ajax/devices')
+def _ListDevices(args, req_vars): # pylint: disable=W0613
+ resp = []
+ for device in backends.ListDevices():
+ resp += [{'backend': device.backend.name,
+ 'id': device.id,
+ 'name': device.name}]
+ return _HTTP_OK, [], resp
+
+
+@AjaxHandler('/ajax/initialize/(\w+)/(\w+)$') # /ajax/initialize/Android/a0b1c2
+def _InitializeDevice(args, req_vars): # pylint: disable=W0613
+ device = _GetDevice(args)
+ if not device:
+ return _HTTP_GONE, [], 'Device not found'
+ device.Initialize()
+ return _HTTP_OK, [], {
+ 'is_mmap_tracing_enabled': device.IsMmapTracingEnabled(),
+ 'is_native_alloc_tracing_enabled': device.IsNativeAllocTracingEnabled()}
+
+
+@AjaxHandler(r'/ajax/ps/(\w+)/(\w+)$') # /ajax/ps/Android/a0b1c2[?all=1]
+def _ListProcesses(args, req_vars): # pylint: disable=W0613
+ """Lists processes and their CPU / mem stats.
+
+ The response is formatted according to the Google Charts DataTable format.
+ """
+ device = _GetDevice(args)
+ if not device:
+ return _HTTP_GONE, [], 'Device not found'
+ resp = {
+ 'cols': [
+ {'label': 'Pid', 'type':'number'},
+ {'label': 'Name', 'type':'string'},
+ {'label': 'Cpu %', 'type':'number'},
+ {'label': 'Mem RSS Kb', 'type':'number'},
+ {'label': '# Threads', 'type':'number'},
+ ],
+ 'rows': []}
+ for process in device.ListProcesses():
+ # Exclude system apps if the request didn't contain the ?all=1 arg.
+ if not req_vars.get('all') and not re.match(_APP_PROCESS_RE, process.name):
+ continue
+ stats = process.GetStats()
+ resp['rows'] += [{'c': [
+ {'v': process.pid, 'f': None},
+ {'v': process.name, 'f': None},
+ {'v': stats.cpu_usage, 'f': None},
+ {'v': stats.vm_rss, 'f': None},
+ {'v': stats.threads, 'f': None},
+ ]}]
+ return _HTTP_OK, [], resp
+
+
+@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)$') # /ajax/stats/Android/a0b1c2
+def _GetDeviceStats(args, req_vars): # pylint: disable=W0613
+ """Lists device CPU / mem stats.
+
+ The response is formatted according to the Google Charts DataTable format.
+ """
+ device = _GetDevice(args)
+ if not device:
+ return _HTTP_GONE, [], 'Device not found'
+ device_stats = device.GetStats()
+
+ cpu_stats = {
+ 'cols': [
+ {'label': 'CPU', 'type':'string'},
+ {'label': 'Usr %', 'type':'number'},
+ {'label': 'Sys %', 'type':'number'},
+ {'label': 'Idle %', 'type':'number'},
+ ],
+ 'rows': []}
+
+ for cpu_idx in xrange(len(device_stats.cpu_times)):
+ cpu = device_stats.cpu_times[cpu_idx]
+ cpu_stats['rows'] += [{'c': [
+ {'v': '# %d' % cpu_idx, 'f': None},
+ {'v': cpu['usr'], 'f': None},
+ {'v': cpu['sys'], 'f': None},
+ {'v': cpu['idle'], 'f': None},
+ ]}]
+
+ mem_stats = {
+ 'cols': [
+ {'label': 'Section', 'type':'string'},
+ {'label': 'MB', 'type':'number', 'pattern': ''},
+ ],
+ 'rows': []}
+
+ for key, value in device_stats.memory_stats.iteritems():
+ mem_stats['rows'] += [{'c': [
+ {'v': key, 'f': None},
+ {'v': value / 1024, 'f': None}
+ ]}]
+
+ return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats}
+
+
+@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)/(\d+)$') # /ajax/stats/Android/a0b1c2/42
+def _GetProcessStats(args, req_vars): # pylint: disable=W0613
+ """Lists CPU / mem stats for a given process (and keeps history).
+
+ The response is formatted according to the Google Charts DataTable format.
+ """
+ process = _GetProcess(args)
+ if not process:
+ return _HTTP_GONE, [], 'Device not found'
+
+ proc_uri = '/'.join(args)
+ cur_stats = process.GetStats()
+ if proc_uri not in _proc_stats_history:
+ _proc_stats_history[proc_uri] = collections.deque(maxlen=_STATS_HIST_SIZE)
+ history = _proc_stats_history[proc_uri]
+ history.append(cur_stats)
+
+ cpu_stats = {
+ 'cols': [
+ {'label': 'T', 'type':'string'},
+ {'label': 'CPU %', 'type':'number'},
+ {'label': '# Threads', 'type':'number'},
+ ],
+ 'rows': []
+ }
+
+ mem_stats = {
+ 'cols': [
+ {'label': 'T', 'type':'string'},
+ {'label': 'Mem RSS Kb', 'type':'number'},
+ {'label': 'Page faults', 'type':'number'},
+ ],
+ 'rows': []
+ }
+
+ for stats in history:
+ cpu_stats['rows'] += [{'c': [
+ {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None},
+ {'v': stats.cpu_usage, 'f': None},
+ {'v': stats.threads, 'f': None},
+ ]}]
+ mem_stats['rows'] += [{'c': [
+ {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None},
+ {'v': stats.vm_rss, 'f': None},
+ {'v': stats.page_faults, 'f': None},
+ ]}]
+
+ return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats}
+
+
+@UriHandler(r'^(?!/ajax)/(.*)$')
+def _StaticContent(args, req_vars): # pylint: disable=W0613
+ # Give the browser a 1-day TTL cache to minimize the start-up time.
+ cache_headers = [('Cache-Control', 'max-age=86400, public')]
+ req_path = args[0] if args[0] else 'index.html'
+ file_path = os.path.abspath(os.path.join(_CONTENT_DIR, req_path))
+ if (os.path.isfile(file_path) and
+ os.path.commonprefix([file_path, _CONTENT_DIR]) == _CONTENT_DIR):
+ mtype = 'text/plain'
+ guessed_mime = mimetypes.guess_type(file_path)
+ if guessed_mime and guessed_mime[0]:
+ mtype = guessed_mime[0]
+ with open(file_path, 'rb') as f:
+ body = f.read()
+ return _HTTP_OK, cache_headers + [('Content-Type', mtype)], body
+ return _HTTP_NOT_FOUND, cache_headers, file_path + ' not found'
+
+
+def _GetDevice(args):
+ """Returns a |backends.Device| instance from a /backend/device URI."""
+ assert(len(args) >= 2), 'Malformed request. Expecting /backend/device'
+ return backends.GetDevice(backend_name=args[0], device_id=args[1])
+
+
+def _GetProcess(args):
+ """Returns a |backends.Process| instance from a /backend/device/pid URI."""
+ assert(len(args) >= 3 and args[2].isdigit()), (
+ 'Malformed request. Expecting /backend/device/pid')
+ device = _GetDevice(args)
+ if not device:
+ return None
+ return device.GetProcess(int(args[2]))
+
+
+def _HttpRequestHandler(environ, start_response):
+ """Parses a single HTTP request and delegates the handling through UriHandler.
+
+ This essentially wires up wsgiref.simple_server with our @UriHandler(s).
+ """
+ path = environ['PATH_INFO']
+ method = environ['REQUEST_METHOD']
+ if method == 'POST':
+ req_body_size = int(environ.get('CONTENT_LENGTH', 0))
+ req_body = environ['wsgi.input'].read(req_body_size)
+ req_vars = json.loads(req_body)
+ else:
+ req_vars = urlparse.parse_qs(environ['QUERY_STRING'])
+ (http_code, headers, body) = UriHandler.Handle(method, path, req_vars)
+ start_response(http_code, headers)
+ return [body]
+
+
+def Start(http_port):
+ httpd = wsgiref.simple_server.make_server('', http_port, _HttpRequestHandler)
+ httpd.serve_forever() \ No newline at end of file
diff --git a/tools/memory_inspector/start_web_ui b/tools/memory_inspector/start_web_ui
new file mode 100755
index 0000000..f15e73f
--- /dev/null
+++ b/tools/memory_inspector/start_web_ui
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import memory_inspector
+import webbrowser
+
+from memory_inspector.frontends import www_server
+
+
+if __name__ == '__main__':
+ HTTP_PORT=8089
+ memory_inspector.RegisterAllBackends()
+ print 'Serving on port %d' % HTTP_PORT
+ webbrowser.open('http://localhost:%d' % HTTP_PORT)
+ www_server.Start(HTTP_PORT) \ No newline at end of file