diff options
author | primiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-13 15:20:12 +0000 |
---|---|---|
committer | primiano@chromium.org <primiano@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-13 15:20:12 +0000 |
commit | 438c69e18ab05ea3701b4bdf51cffdd7d1982e5a (patch) | |
tree | 32c8fcd9629da098882b0f7d89119b22dfcf0263 /tools/memory_inspector | |
parent | c2d2ef92677c79d67d637e5bb613f059c5aca18b (diff) | |
download | chromium_src-438c69e18ab05ea3701b4bdf51cffdd7d1982e5a.zip chromium_src-438c69e18ab05ea3701b4bdf51cffdd7d1982e5a.tar.gz chromium_src-438c69e18ab05ea3701b4bdf51cffdd7d1982e5a.tar.bz2 |
Add mmap table and resident page UI to memory_inspector.
This wires-up the raw mmap dump (through memdump), introducing:
- A new /ajax/dump/mmap endpoint to the server.
- The HTML/JS ui to show the mmap table and the resident page list.
BUG=340294
NOTRY=true
Review URL: https://codereview.chromium.org/196973008
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@256836 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/memory_inspector')
7 files changed, 301 insertions, 15 deletions
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 22d130c..18baadb 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 @@ <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="/mmap.css" rel="stylesheet" type="text/css"> <link href="/rootUi.css" rel="stylesheet" type="text/css"> <link href="/settings.css" rel="stylesheet" type="text/css"> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> @@ -20,6 +21,7 @@ { packages: ['corechart', 'table', 'orgchart', 'treemap'] }); </script> <script src="/js/devices.js"></script> + <script src="/js/mmap.js"></script> <script src="/js/rootUi.js"></script> <script src="/js/processes.js"></script> <script src="/js/settings.js"></script> @@ -92,19 +94,33 @@ <header> <b>Filters: </b> <span> - Prot. flags: <input type="text" id="mm-filter-prot" /> - File name: <input type="text" id="mm-filter-file" /> + <input type="button" id="mm-filter-clear" value="Reset"> + Prot. flags: <input type="text" id="mm-filter-prot"> + File name: <input type="text" id="mm-filter-file"> + <i>(Just press enter to apply filters)</i> </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> + <b>Lookup addr: </b> + <input type="text" id="mm-lookup-addr"> + <span><b>Offset in mapping:</b></span> + <span id="mm-lookup-offset">0</span> + </span> + </header> + <hr> + <header> + <b>Totals [Kb]: </b> + <b>Priv dirty: </b><span id="mm-totals-priv-dirty">0</span> + <b>Priv clean: </b><span id="mm-totals-priv-clean">0</span> + <b>Shared dirty: </b><span id="mm-totals-shared-dirty">0</span> + <b>Shared clean: </b><span id="mm-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. + <b>Selected [Kb]: </b> + <b>Priv dirty: </b><span id="mm-selected-priv-dirty">0</span> + <b>Priv clean: </b><span id="mm-selected-priv-clean">0</span> + <b>Shared dirty: </b><span id="mm-selected-shared-dirty">0</span> + <b>Shared clean: </b><span id="mm-selected-shared-clean">0</span> </header> <div id="mm-table"></div> </div> @@ -130,17 +146,17 @@ </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" /> + <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" /> + <input type="button" value="Reload" id="settings-load"> + <input type="button" value="Store" id="settings-store"> </header> <div id="settings-container"> </div> 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 0000000..400d24f --- /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 = $('<table class="mm-resident-table"/>'); + var curRow = $('<tr/>'); + 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 = $('<tr/>'); + table.append(curRow); + } + var hexAddr = (pageIdx * this.PAGE_SIZE).toString(16); + var cell = $('<td/>').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 08cc48a..fc7b98e 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 a9e85de..a2620eb 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 = $('<div id="message_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 0000000..b47adb4 --- /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 80b805d..7654f57 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 fdf605d..784881f 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 @@ class AjaxHandler(UriHandler): ('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) |