diff options
author | primiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-12 21:13:34 +0000 |
---|---|---|
committer | primiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-12 21:13:34 +0000 |
commit | c2028b5519fd3bf95c864e6e0c12b1c946b7220f (patch) | |
tree | a5e6fb2d073e5c2a04388f0a5708e80c00c01af4 /tools | |
parent | fba1eb214a613eefad7dde40eefbb50a12320a45 (diff) | |
download | chromium_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')
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="⟲"> + </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 |