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 @@ Memory Inspector + @@ -20,6 +21,7 @@ { packages: ['corechart', 'table', 'orgchart', 'treemap'] }); + @@ -92,19 +94,33 @@

Hierarchical view of selected snapshot

Filters: - Prot. flags: - File name: + + Prot. flags: + File name: + (Just press enter to apply filters)
- Totals: - Priv dirty (Kb): 0 - Priv clean (Kb): 0 - Shared dirty (Kb): 0 - Shared clean (Kb): 0 + Lookup addr: + + Offset in mapping: + 0 + +
+
+
+ Totals [Kb]: + Priv dirty: 0 + Priv clean: 0 + Shared dirty: 0 + Shared clean: 0
- Note: totals from this filtered table might not match the totals in the treemap, as table filtering is not hierarchical. + Selected [Kb]: + Priv dirty: 0 + Priv clean: 0 + Shared dirty: 0 + Shared clean: 0
@@ -130,17 +146,17 @@

Hierarchical view of selected snapshot

- - - + + +
- - + +
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)