diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/index.html b/tools/memory_inspector/memory_inspector/frontends/www_content/index.html
index 22d130c8064fb2..18baadbdbf68d4 100644
--- a/tools/memory_inspector/memory_inspector/frontends/www_content/index.html
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/index.html
@@ -10,6 +10,7 @@
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/js/mmap.js b/tools/memory_inspector/memory_inspector/frontends/www_content/js/mmap.js
new file mode 100644
index 00000000000000..400d24f906f1ac
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/mmap.js
@@ -0,0 +1,192 @@
+// 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.
+
+mmap = new (function() {
+
+this.AJAX_BASE_URL_ = '/ajax';
+this.COL_START = 0;
+this.COL_END = 1;
+this.COL_LEN = 2;
+this.COL_PROT = 3;
+this.COL_PRIV_DIRTY = 4;
+this.COL_PRIV_CLEAN = 5;
+this.COL_SHARED_DIRTY = 6;
+this.COL_SHARED_CLEAN = 7;
+this.COL_FILE = 8;
+this.COL_RESIDENT = 10;
+this.SHOW_COLUMNS = [0, 1, 2, 3, 4, 5, 6, 7, 8];
+this.PAGE_SIZE = 4096;
+
+this.mapsData_ = null;
+this.mapsTable_ = null;
+this.mapsFilter_ = null;
+
+this.onDomReady_ = function() {
+ $('#mm-lookup-addr').on('change', this.lookupAddress.bind(this));
+ $('#mm-filter-file').on('change', this.applyMapsTableFilters_.bind(this));
+ $('#mm-filter-prot').on('change', this.applyMapsTableFilters_.bind(this));
+ $('#mm-filter-clear').on('click', this.resetMapsTableFilters_.bind(this));
+};
+
+this.dumpMmaps = function(targetProcUri) {
+ if (!targetProcUri)
+ return;
+ webservice.ajaxRequest('/dump/mmap/' + targetProcUri,
+ this.onDumpAjaxResponse_.bind(this));
+ rootUi.showDialog('Dumping memory maps for ' + targetProcUri + '...');
+};
+
+this.onDumpAjaxResponse_ = function(data) {
+ $('#mm-filter-file').val('');
+ $('#mm-filter-prot').val('');
+ this.mapsData_ = new google.visualization.DataTable(data);
+ this.mapsFilter_ = new google.visualization.DataView(this.mapsData_);
+ this.mapsFilter_.setColumns(this.SHOW_COLUMNS);
+ this.mapsTable_ = new google.visualization.Table($('#mm-table')[0]);
+ google.visualization.events.addListener(
+ this.mapsTable_, 'select', this.onMmapTableRowSelect_.bind(this));
+ $('#mm-table').on('dblclick', this.onMmapTableDblClick_.bind(this));
+ rootUi.showTab('mm-table');
+ this.applyMapsTableFilters_();
+ rootUi.hideDialog();
+};
+
+this.applyMapsTableFilters_ = function() {
+ // Filters the rows according to the user-provided file and prot regexps.
+ if (!this.mapsFilter_)
+ return;
+
+ var fileRx = $('#mm-filter-file').val();
+ var protRx = $('#mm-filter-prot').val();
+ var rows = [];
+ var totPrivDirty = 0;
+ var totPrivClean = 0;
+ var totSharedDirty = 0;
+ var totSharedClean = 0;
+
+ for (var row = 0; row < this.mapsData_.getNumberOfRows(); ++row) {
+ mappedFile = this.mapsData_.getValue(row, this.COL_FILE);
+ protFlags = this.mapsData_.getValue(row, this.COL_PROT);
+ if (!mappedFile.match(fileRx) || !protFlags.match(protRx))
+ continue;
+ rows.push(row);
+ totPrivDirty += this.mapsData_.getValue(row, this.COL_PRIV_DIRTY);
+ totPrivClean += this.mapsData_.getValue(row, this.COL_PRIV_CLEAN);
+ totSharedDirty += this.mapsData_.getValue(row,this.COL_SHARED_DIRTY);
+ totSharedClean += this.mapsData_.getValue(row, this.COL_SHARED_CLEAN);
+ }
+ this.mapsFilter_.setRows(rows);
+ this.mapsTable_.draw(this.mapsFilter_);
+ $('#mm-totals-priv-dirty').text(totPrivDirty);
+ $('#mm-totals-priv-clean').text(totPrivClean);
+ $('#mm-totals-shared-dirty').text(totSharedDirty);
+ $('#mm-totals-shared-clean').text(totSharedClean);
+};
+
+this.resetMapsTableFilters_ = function() {
+ $('#mm-filter-file').val('');
+ $('#mm-filter-prot').val('');
+ this.applyMapsTableFilters_();
+};
+
+this.onMmapTableRowSelect_ = function() {
+ // Update the memory totals for the selected rows.
+ if (!this.mapsFilter_)
+ return;
+
+ var totPrivDirty = 0;
+ var totPrivClean = 0;
+ var totSharedDirty = 0;
+ var totSharedClean = 0;
+
+ this.mapsTable_.getSelection().forEach(function(sel) {
+ var row = sel.row;
+ totPrivDirty += this.mapsFilter_.getValue(row, this.COL_PRIV_DIRTY);
+ totPrivClean += this.mapsFilter_.getValue(row, this.COL_PRIV_CLEAN);
+ totSharedDirty += this.mapsFilter_.getValue(row,this.COL_SHARED_DIRTY);
+ totSharedClean += this.mapsFilter_.getValue(row, this.COL_SHARED_CLEAN);
+ }, this);
+ $('#mm-selected-priv-dirty').text(totPrivDirty);
+ $('#mm-selected-priv-clean').text(totPrivClean);
+ $('#mm-selected-shared-dirty').text(totSharedDirty);
+ $('#mm-selected-shared-clean').text(totSharedClean);
+};
+
+this.onMmapTableDblClick_ = function() {
+ // Show resident pages for the selected mapping.
+ var PAGES_PER_ROW = 16;
+
+ if (!this.mapsData_)
+ return;
+
+ var sel = this.mapsTable_.getSelection();
+ if (sel.length == 0)
+ return;
+
+ // |sel| returns the row index in the current view, which might be filtered.
+ // Need to walk back in the mapsFilter_.getViewRows to get the actual row
+ // index in the original table.
+ var row = this.mapsFilter_.getViewRows()[sel[0].row];
+ var arr = JSON.parse(this.mapsData_.getValue(row, this.COL_RESIDENT));
+ var table = $('
');
+ var curRow = $('
|
');
+ table.append(curRow);
+
+ for (var i = 0; i < arr.length; ++i) {
+ for (var j = 0; j < 8; ++j) {
+ var pageIdx = i * 8 + j;
+ var resident = !!(arr[i] & (1 << j));
+ if (pageIdx % PAGES_PER_ROW == 0) {
+ curRow = $('
|
');
+ table.append(curRow);
+ }
+ var hexAddr = (pageIdx * this.PAGE_SIZE).toString(16);
+ var cell = $('
| ').text(hexAddr);
+ if (resident)
+ cell.addClass('resident')
+ curRow.append(cell);
+ }
+ }
+ rootUi.showDialog(table, 'Resident page list');
+};
+
+this.lookupAddress = function() {
+ // Looks up the user-provided address in the mmap table and highlights the
+ // row containing the map (if found).
+ if (!this.mapsData_)
+ return;
+
+ addr = parseInt($('#mm-lookup-addr').val(), 16);
+ $('#mm-lookup-offset').text('');
+ if (!addr)
+ return;
+
+ this.resetMapsTableFilters_();
+
+ var lbound = 0;
+ var ubound = this.mapsData_.getNumberOfRows() - 1;
+ while (lbound <= ubound) {
+ var row = ((lbound + ubound) / 2) >> 0;
+ var start = parseInt(this.mapsData_.getValue(row, this.COL_START), 16);
+ var end = parseInt(this.mapsData_.getValue(row, this.COL_END), 16);
+ if (addr < start){
+ ubound = row - 1;
+ }
+ else if (addr > end) {
+ lbound = row + 1;
+ }
+ else {
+ $('#mm-lookup-offset').text((addr - start).toString(16));
+ this.mapsTable_.setSelection([{row: row, column: null}]);
+ // Scroll to row.
+ $('#wrapper').scrollTop(
+ $('#mm-table .google-visualization-table-tr-sel').offset().top);
+ break;
+ }
+ }
+};
+
+$(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
index 08cc48a5fd65d8..fc7b98e7913d6f 100644
--- a/tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/processes.js
@@ -39,6 +39,7 @@ this.refreshPsTable = function() {
this.psTable_ = new google.visualization.Table($('#ps-table')[0]);
google.visualization.events.addListener(
this.psTable_, 'select', this.onPsTableRowSelect_.bind(this));
+ $('#ps-table').on('dblclick', this.onPsTableDblClick_.bind(this));
};
var showAllParam = $('#ps-show_all').prop('checked') ? '?all=1' : '';
@@ -71,6 +72,10 @@ this.onPsTableRowSelect_ = function() {
this.startSelectedProcessStats();
};
+this.onPsTableDblClick_ = function() {
+ mmap.dumpMmaps(this.getSelectedProcessURI());
+};
+
this.onPsAjaxResponse_ = function(data) {
// Redraw table preserving sorting info.
var sort = this.psTable_.getSortInfo() || {column: -1, ascending: false};
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
index a9e85defe0fe94..a2620eb857a4e6 100644
--- a/tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/js/rootUi.js
@@ -36,13 +36,23 @@ this.showTab = function(tabId) {
$('#tabs').tabs('option', 'active', index - 1);
};
-this.showDialog = function(message) {
+this.showDialog = function(content, title) {
var dialog = $("#message_dialog");
+ title = title || '';
if (dialog.length == 0) {
dialog = $('
');
$('body').append(dialog);
}
- $("#message_dialog").text(message).dialog({ modal: true });
+ if (typeof(content) == 'string')
+ dialog.empty().text(content);
+ else
+ dialog.empty().append(content); // Assume is a jQuery DOM object.
+
+ dialog.dialog({modal: true, title: title, height:'auto', width:'auto'});
+};
+
+this.hideDialog = function() {
+ $("#message_dialog").dialog('close');
};
$(document).ready(this.onDomReady_.bind(this));
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/mmap.css b/tools/memory_inspector/memory_inspector/frontends/www_content/mmap.css
new file mode 100644
index 00000000000000..b47adb4ab94568
--- /dev/null
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/mmap.css
@@ -0,0 +1,16 @@
+/* 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. */
+
+.mm-resident-table {
+ border: 1px solid #999;
+}
+
+.mm-resident-table td {
+ padding: 0.2em;
+}
+
+.mm-resident-table td.resident {
+ background: lightgreen;
+}
+
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css b/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css
index 80b805d1ccdedd..7654f572b41e0d 100644
--- a/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css
+++ b/tools/memory_inspector/memory_inspector/frontends/www_content/rootUi.css
@@ -141,3 +141,7 @@ input[type="text"] {
display: inline-block;
height: 20em;
}
+
+table {
+ -webkit-user-select: none;
+}
\ No newline at end of file
diff --git a/tools/memory_inspector/memory_inspector/frontends/www_server.py b/tools/memory_inspector/memory_inspector/frontends/www_server.py
index fdf605dbbf563a..784881f5262cdf 100644
--- a/tools/memory_inspector/memory_inspector/frontends/www_server.py
+++ b/tools/memory_inspector/memory_inspector/frontends/www_server.py
@@ -100,6 +100,7 @@ def AjaxOutputFilter(http_code, headers, body):
('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()]
@@ -120,6 +121,48 @@ def _ListDevices(args, req_vars): # pylint: disable=W0613
return _HTTP_OK, [], resp
+@AjaxHandler(r'/ajax/dump/mmap/(\w+)/(\w+)/(\d+)')
+def _DumpMmapsForProcess(args, req_vars): # pylint: disable=W0613
+ """Dumps memory maps for a process.
+
+ The response is formatted according to the Google Charts DataTable format.
+ """
+ process = _GetProcess(args)
+ if not process:
+ return _HTTP_GONE, [], 'Device not found or process died'
+ mmap = process.DumpMemoryMaps()
+ resp = {
+ 'cols': [
+ {'label': 'Start', 'type':'string'},
+ {'label': 'End', 'type':'string'},
+ {'label': 'Length Kb', 'type':'number'},
+ {'label': 'Prot', 'type':'string'},
+ {'label': 'Priv. Dirty Kb', 'type':'number'},
+ {'label': 'Priv. Clean Kb', 'type':'number'},
+ {'label': 'Shared Dirty Kb', 'type':'number'},
+ {'label': 'Shared Clean Kb', 'type':'number'},
+ {'label': 'File', 'type':'string'},
+ {'label': 'Offset', 'type':'number'},
+ {'label': 'Resident Pages', 'type':'string'},
+ ],
+ 'rows': []}
+ for entry in mmap.entries:
+ resp['rows'] += [{'c': [
+ {'v': '%08x' % entry.start, 'f': None},
+ {'v': '%08x' % entry.end, 'f': None},
+ {'v': entry.len / 1024, 'f': None},
+ {'v': entry.prot_flags, 'f': None},
+ {'v': entry.priv_dirty_bytes / 1024, 'f': None},
+ {'v': entry.priv_clean_bytes / 1024, 'f': None},
+ {'v': entry.shared_dirty_bytes / 1024, 'f': None},
+ {'v': entry.shared_clean_bytes / 1024, 'f': None},
+ {'v': entry.mapped_file, 'f': None},
+ {'v': entry.mapped_offset, 'f': None},
+ {'v': '[%s]' % (','.join(map(str, entry.resident_pages))), 'f': None},
+ ]}]
+ return _HTTP_OK, [], resp
+
+
@AjaxHandler('/ajax/initialize/(\w+)/(\w+)$') # /ajax/initialize/Android/a0b1c2
def _InitializeDevice(args, req_vars): # pylint: disable=W0613
device = _GetDevice(args)