diff options
author | scottfr@chromium.org <scottfr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-20 23:01:23 +0000 |
---|---|---|
committer | scottfr@chromium.org <scottfr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-20 23:01:23 +0000 |
commit | ebaf5d0037fb655dce4cdeee00faa16800cfc956 (patch) | |
tree | 98f9d5cd42e11993fee658139ec8ccda0c5bb6fc | |
parent | 3b172116930bdad38aeb24c3e70d80d6b9df77a3 (diff) | |
download | chromium_src-ebaf5d0037fb655dce4cdeee00faa16800cfc956.zip chromium_src-ebaf5d0037fb655dce4cdeee00faa16800cfc956.tar.gz chromium_src-ebaf5d0037fb655dce4cdeee00faa16800cfc956.tar.bz2 |
Display media cache events on chrome://media-internals.
BUG=none
TEST=unittests, manually
Review URL: http://codereview.chromium.org/7395031
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@93272 0039d316-1c4b-4281-b951-d872f2087c98
7 files changed, 558 insertions, 4 deletions
diff --git a/chrome/browser/resources/media_internals.html b/chrome/browser/resources/media_internals.html index 51f01b8..482c30b 100644 --- a/chrome/browser/resources/media_internals.html +++ b/chrome/browser/resources/media_internals.html @@ -9,12 +9,16 @@ found in the LICENSE file. <link rel="stylesheet" href="webui.css" /> <link rel="stylesheet" href="media_internals/media_internals.css" /> <script src="shared/js/cr.js"></script> + <script src="media_internals/cache_entry.js"></script> <script src="media_internals/item_store.js"></script> <script src="media_internals/media_internals.js"></script> + <script src="media_internals/disjoint_range_set.js"></script> <title>Media Internals</title> </head> <body> <h2>Active audio streams:</h2> <div id="audio-streams"></div> + <h2>Cached resources:</h2> + <div id="cache-entries"></div> </body> </html> diff --git a/chrome/browser/resources/media_internals/cache_entry.js b/chrome/browser/resources/media_internals/cache_entry.js new file mode 100644 index 0000000..1136a3f --- /dev/null +++ b/chrome/browser/resources/media_internals/cache_entry.js @@ -0,0 +1,91 @@ +// Copyright (c) 2011 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. + +cr.define('media', function() { + + /** + * This class represents a file cached by net. + */ + function CacheEntry() { + this.read_ = new media.DisjointRangeSet; + this.written_ = new media.DisjointRangeSet; + this.available_ = new media.DisjointRangeSet; + + // Set to true when we know the entry is sparse. + this.sparse = false; + this.key = null; + this.size = null; + }; + + CacheEntry.prototype = { + /** + * Mark a range of bytes as read from the cache. + * @param {int} start The first byte read. + * @param {int} length The number of bytes read. + */ + readBytes: function(start, length) { + start = parseInt(start); + length = parseInt(length); + this.read_.add(start, start + length); + this.available_.add(start, start + length); + this.sparse = true; + }, + + /** + * Mark a range of bytes as written to the cache. + * @param {int} start The first byte written. + * @param {int} length The number of bytes written. + */ + writeBytes: function(start, length) { + start = parseInt(start); + length = parseInt(length); + this.written_.add(start, start + length); + this.available_.add(start, start + length); + this.sparse = true; + }, + + /** + * Merge this CacheEntry with another, merging recorded ranges and flags. + * @param {CacheEntry} other The CacheEntry to merge into this one. + */ + merge: function(other) { + this.read_.merge(other.read_); + this.written_.merge(other.written_); + this.available_.merge(other.available_); + this.sparse = this.sparse || other.sparse; + this.key = this.key || other.key; + this.size = this.size || other.size; + }, + + /** + * Render this CacheEntry as a <li>. + * @return {HTMLElement} A <li> representing this CacheEntry. + */ + toListItem: function() { + var result = document.createElement('li'); + result.id = this.key; + result.className = 'cache-entry'; + result.innerHTML = this.key + '<br />Read: '; + result.innerHTML += this.read_.map(function(start, end) { + return start + '-' + end; + }).join(', '); + + result.innerHTML += '. Written: '; + result.innerHTML += this.written_.map(function(start, end) { + return start + '-' + end; + }).join(', '); + + // Include the total size if we know it. + if (this.size) { + result.innerHTML += '. Out of '; + result.innerHTML += this.size; + } + return result; + } + }; + + return { + CacheEntry: CacheEntry + }; +}); diff --git a/chrome/browser/resources/media_internals/disjoint_range_set.js b/chrome/browser/resources/media_internals/disjoint_range_set.js new file mode 100644 index 0000000..cff8f69 --- /dev/null +++ b/chrome/browser/resources/media_internals/disjoint_range_set.js @@ -0,0 +1,134 @@ +// Copyright (c) 2011 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. + +cr.define('media', function() { + + /** + * This class represents a collection of non-intersecting ranges. Ranges + * specified by (start, end) can be added and removed at will. It is used to + * record which sections of a media file have been cached, e.g. the first and + * last few kB plus several MB in the middle. + * + * Example usage: + * someRange.add(0, 100); // Contains 0-100. + * someRange.add(150, 200); // Contains 0-100, 150-200. + * someRange.remove(25, 75); // Contains 0-24, 76-100, 150-200. + * someRange.add(25, 149); // Contains 0-200. + */ + function DisjointRangeSet() { + this.ranges_ = {}; + }; + + DisjointRangeSet.prototype = { + /** + * Deletes all ranges intersecting with (start ... end) and returns the + * extents of the cleared area. + * @param {int} start The start of the range to remove. + * @param {int} end The end of the range to remove. + * @param {int} sloppiness 0 removes only strictly overlapping ranges, and + * 1 removes adjacent ones. + * @return {Object} The start and end of the newly cleared range. + */ + clearRange: function(start, end, sloppiness) { + var ranges = this.ranges_; + var result = {start: start, end: end}; + + for (var rangeStart in this.ranges_) { + rangeEnd = this.ranges_[rangeStart]; + // A range intersects another if its start lies within the other range + // or vice versa. + if ((rangeStart >= start && rangeStart <= (end + sloppiness)) || + (start >= rangeStart && start <= (rangeEnd + sloppiness))) { + delete ranges[rangeStart]; + result.start = Math.min(result.start, rangeStart); + result.end = Math.max(result.end, rangeEnd); + } + } + + return result; + }, + + /** + * Adds a range to this DisjointRangeSet. + * Joins adjacent and overlapping ranges together. + * @param {int} start The beginning of the range to add, inclusive. + * @param {int} end The end of the range to add, inclusive. + */ + add: function(start, end) { + if (end < start) + return; + + // Remove all touching ranges. + result = this.clearRange(start, end, 1); + // Add back a single contiguous range. + this.ranges_[Math.min(start, result.start)] = Math.max(end, result.end); + }, + + /** + * Combines a DisjointRangeSet with this one. + * @param {DisjointRangeSet} ranges A DisjointRangeSet to be squished into + * this one. + */ + merge: function(other) { + var ranges = this; + other.forEach(function(start, end) { ranges.add(start, end); }); + }, + + /** + * Removes a range from this DisjointRangeSet. + * Will split existing ranges if necessary. + * @param {int} start The beginning of the range to remove, inclusive. + * @param {int} end The end of the range to remove, inclusive. + */ + remove: function(start, end) { + if (end < start) + return; + + // Remove instersecting ranges. + result = this.clearRange(start, end, 0); + + // Add back non-overlapping ranges. + if (result.start < start) + this.ranges_[result.start] = start - 1; + if (result.end > end) + this.ranges_[end + 1] = result.end; + }, + + /** + * Iterates over every contiguous range in this DisjointRangeSet, calling a + * function for each (start, end). + * @param {function(int, int)} iterator The function to call on each range. + */ + forEach: function(iterator) { + for (var start in this.ranges_) + iterator(start, this.ranges_[start]); + }, + + /** + * Maps this DisjointRangeSet to an array by calling a given function on the + * start and end of each contiguous range, sorted by start. + * @param {function(int, int)} mapper Maps a range to an array element. + * @return {Array} An array of each mapper(range). + */ + map: function(mapper) { + var starts = []; + for (var start in this.ranges_) + starts.push(parseInt(start)); + starts.sort(function(a, b) { + return a - b; + }); + + var ranges = this.ranges_; + var results = starts.map(function(s) { + return mapper(s, ranges[s]); + }); + + return results; + } + }; + + return { + DisjointRangeSet: DisjointRangeSet + }; +}); diff --git a/chrome/browser/resources/media_internals/disjoint_range_set_test.html b/chrome/browser/resources/media_internals/disjoint_range_set_test.html new file mode 100644 index 0000000..b7c61d9 --- /dev/null +++ b/chrome/browser/resources/media_internals/disjoint_range_set_test.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<!-- +Copyright (c) 2011 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. +--> + <head> + <title></title> + <script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> + <script src="../shared/js/cr.js"></script> + <script src="disjoint_range_set.js"></script> + <script> + goog.require('goog.testing.jsunit'); + </script> + </head> + <body> + <script> + + var range; + + function assertRangeEquals(ranges) { + assertArrayEquals( + ranges, range.map(function(start, end) { return [start, end]; })); + }; + + function setUp() { + range = new media.DisjointRangeSet; + }; + + function testAdd() { + range.add(1, 6); + assertRangeEquals([[1, 6]]); + range.add(-5, -3); + assertRangeEquals([[-5, -3], [1, 6]]); + }; + + function testAddAdjacent() { + range.add(3, 6); + assertRangeEquals([[3, 6]]); + range.add(1, 2); + assertRangeEquals([[1, 6]]); + range.add(7, 9); + assertRangeEquals([[1, 9]]); + }; + + function testAddNotQuiteAdjacent() { + range.add(3, 6); + assertRangeEquals([[3, 6]]); + range.add(0, 1); + assertRangeEquals([[0, 1], [3, 6]]); + range.add(8, 9); + assertRangeEquals([[0, 1], [3, 6], [8, 9]]); + }; + + function testAddOverlapping() { + range.add(1, 6); + assertRangeEquals([[1, 6]]); + range.add(5, 8); + assertRangeEquals([[1, 8]]); + range.add(0, 1); + assertRangeEquals([[0, 8]]); + }; + + function testRemove() { + range.add(1, 20); + assertRangeEquals([[1, 20]]); + range.remove(0, 3); + assertRangeEquals([[4, 20]]); + range.remove(18, 20); + assertRangeEquals([[4, 17]]); + range.remove(5, 16); + assertRangeEquals([[4, 4], [17, 17]]); + }; + + function testStartsEmpty() { + assertRangeEquals([]); + }; + + </script> + </body> +</html> diff --git a/chrome/browser/resources/media_internals/media_internals.js b/chrome/browser/resources/media_internals/media_internals.js index c046889..a01a06d 100644 --- a/chrome/browser/resources/media_internals/media_internals.js +++ b/chrome/browser/resources/media_internals/media_internals.js @@ -6,14 +6,28 @@ cr.define('media', function() { // Stores information on open audio streams, referenced by id. var audioStreams = new media.ItemStore; - // The <div> on the page in which to display audio stream data. + + // Cached files indexed by key and source id. + var cacheEntriesByKey = {}; + var cacheEntries = {}; + + // Map of event source -> url. + var requestURLs = {}; + + // Constants passed to us from Chrome. + var eventTypes = {}; + var eventPhases = {}; + + // The <div>s on the page in which to display information. var audioStreamDiv; + var cacheDiv; /** * Initialize variables and ask MediaInternals for all its data. */ initialize = function() { audioStreamDiv = document.getElementById('audio-streams'); + cacheDiv = document.getElementById('cache-entries'); // Get information about all currently active media. chrome.send('getEverything'); }; @@ -50,6 +64,20 @@ cr.define('media', function() { }; /** + * Write the set of sparse CacheEntries to the DOM. + */ + printSparseCacheEntries = function() { + var out = document.createElement('ul'); + for (var key in cacheEntriesByKey) { + if (cacheEntriesByKey[key].sparse) + out.appendChild(cacheEntriesByKey[key].toListItem()); + } + + cacheDiv.textContent = ''; + cacheDiv.appendChild(out); + }; + + /** * Receiving data for an audio stream. * Add it to audioStreams and update the page. * @param {Object} stream JSON representation of an audio stream. @@ -85,11 +113,90 @@ cr.define('media', function() { } }; + /** + * Receiving net events. + * Update cache information and update that section of the page. + * @param {Array} updates A list of net events that have occurred. + */ + onNetUpdate = function(updates) { + updates.forEach(function(update) { + var id = update.source.id; + if (!cacheEntries[id]) + cacheEntries[id] = new media.CacheEntry; + + switch (eventPhases[update.phase] + '.' + eventTypes[update.type]) { + case 'PHASE_BEGIN.DISK_CACHE_ENTRY_IMPL': + var key = update.params.key; + + // Merge this source with anything we already know about this key. + if (cacheEntriesByKey[key]) { + cacheEntriesByKey[key].merge(cacheEntries[id]); + cacheEntries[id] = cacheEntriesByKey[key]; + } else { + cacheEntriesByKey[key] = cacheEntries[id]; + } + cacheEntriesByKey[key].key = key; + break; + + case 'PHASE_BEGIN.SPARSE_READ': + cacheEntries[id].readBytes(update.params.offset, + update.params.buff_len); + cacheEntries[id].sparse = true; + break; + + case 'PHASE_BEGIN.SPARSE_WRITE': + cacheEntries[id].writeBytes(update.params.offset, + update.params.buff_len); + cacheEntries[id].sparse = true; + break; + + case 'PHASE_BEGIN.URL_REQUEST_START_JOB': + requestURLs[update.source.id] = update.params.url; + break; + + case 'PHASE_NONE.HTTP_TRANSACTION_READ_RESPONSE_HEADERS': + // Record the total size of the file if this was a range request. + var range = /content-range:\s*bytes\s*\d+-\d+\/(\d+)/i.exec( + update.params.headers); + var key = requestURLs[update.source.id]; + delete requestURLs[update.source.id]; + if (range && key) { + if (!cacheEntriesByKey[key]) { + cacheEntriesByKey[key] = new CacheEntry; + cacheEntriesByKey[key].key = key; + } + cacheEntriesByKey[key].size = range[1]; + } + break; + } + }); + + printSparseCacheEntries(); + }; + + /** + * Receiving values for constants. Store them for later use. + * @param {Object} constants A dictionary of constants. + * @param {Object} constants.eventTypes A dictionary of event name -> int. + * @param {Object} constants.eventPhases A dictionary of event phase -> int. + */ + onReceiveConstants = function(constants) { + var events = constants.eventTypes; + for (var e in events) + eventTypes[events[e]] = e; + + var phases = constants.eventPhases; + for (var p in phases) + eventPhases[phases[p]] = p; + }; + return { initialize: initialize, addAudioStream: addAudioStream, onReceiveEverything: onReceiveEverything, onItemDeleted: onItemDeleted, + onNetUpdate: onNetUpdate, + onReceiveConstants: onReceiveConstants, }; }); diff --git a/chrome/browser/ui/webui/media/media_internals_proxy.cc b/chrome/browser/ui/webui/media/media_internals_proxy.cc index 94b2ce1..2f2e46a 100644 --- a/chrome/browser/ui/webui/media/media_internals_proxy.cc +++ b/chrome/browser/ui/webui/media/media_internals_proxy.cc @@ -9,7 +9,21 @@ #include "chrome/browser/media/media_internals.h" #include "chrome/browser/ui/webui/media/media_internals_handler.h" -MediaInternalsProxy::MediaInternalsProxy() { +static const int kMediaInternalsProxyEventDelayMilliseconds = 100; + +static const net::NetLog::EventType kNetEventTypeFilter[] = { + net::NetLog::TYPE_DISK_CACHE_ENTRY_IMPL, + net::NetLog::TYPE_SPARSE_READ, + net::NetLog::TYPE_SPARSE_WRITE, + net::NetLog::TYPE_URL_REQUEST_START_JOB, + net::NetLog::TYPE_HTTP_TRANSACTION_READ_RESPONSE_HEADERS, +}; + +static const char kSendConstantsFunction[] = "media.onReceiveConstants"; +static const char kSendNetworkEventsFunction[] = "media.onNetUpdate"; + +MediaInternalsProxy::MediaInternalsProxy() + : ThreadSafeObserver(net::NetLog::LOG_ALL_BUT_BYTES) { io_thread_ = g_browser_process->io_thread(); } @@ -31,8 +45,13 @@ void MediaInternalsProxy::Detach() { void MediaInternalsProxy::GetEverything() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Ask MediaInternals for all its data. BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, NewRunnableMethod(this, &MediaInternalsProxy::GetEverythingOnIOThread)); + + // Send the page names for constants. + CallJavaScriptFunctionOnUIThread(kSendConstantsFunction, GetConstants()); } void MediaInternalsProxy::OnUpdate(const string16& update) { @@ -42,22 +61,111 @@ void MediaInternalsProxy::OnUpdate(const string16& update) { &MediaInternalsProxy::UpdateUIOnUIThread, update)); } +void MediaInternalsProxy::OnAddEntry(net::NetLog::EventType type, + const base::TimeTicks& time, + const net::NetLog::Source& source, + net::NetLog::EventPhase phase, + net::NetLog::EventParameters* params) { + bool is_event_interesting = false; + for (size_t i = 0; i < arraysize(kNetEventTypeFilter); i++) { + if (type == kNetEventTypeFilter[i]) { + is_event_interesting = true; + break; + } + } + + if (!is_event_interesting) + return; + + BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, + NewRunnableMethod( + this, &MediaInternalsProxy::AddNetEventOnUIThread, + net::NetLog::EntryToDictionaryValue(type, time, source, phase, + params, false))); +} + MediaInternalsProxy::~MediaInternalsProxy() {} +Value* MediaInternalsProxy::GetConstants() { + DictionaryValue* event_types = new DictionaryValue(); + std::vector<net::NetLog::EventType> types_vec = + net::NetLog::GetAllEventTypes(); + + for (size_t i = 0; i < types_vec.size(); ++i) { + const char* name = net::NetLog::EventTypeToString(types_vec[i]); + event_types->SetInteger(name, static_cast<int>(types_vec[i])); + } + + DictionaryValue* event_phases = new DictionaryValue(); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_NONE), + net::NetLog::PHASE_NONE); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_BEGIN), + net::NetLog::PHASE_BEGIN); + event_phases->SetInteger( + net::NetLog::EventPhaseToString(net::NetLog::PHASE_END), + net::NetLog::PHASE_END); + + DictionaryValue* constants = new DictionaryValue(); + constants->Set("eventTypes", event_types); + constants->Set("eventPhases", event_phases); + + return constants; +} + void MediaInternalsProxy::ObserveMediaInternalsOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); io_thread_->globals()->media.media_internals->AddObserver(this); + // TODO(scottfr): Get the passive log data as well. + io_thread_->net_log()->AddObserver(this); } void MediaInternalsProxy::StopObservingMediaInternalsOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); io_thread_->globals()->media.media_internals->RemoveObserver(this); + io_thread_->net_log()->RemoveObserver(this); } void MediaInternalsProxy::GetEverythingOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); io_thread_->globals()->media.media_internals->SendEverything(); } void MediaInternalsProxy::UpdateUIOnUIThread(const string16& update) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // Don't forward updates to a destructed UI. if (handler_) handler_->OnUpdate(update); } + +void MediaInternalsProxy::AddNetEventOnUIThread(Value* entry) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Send the updates to the page in kMediaInternalsProxyEventDelayMilliseconds + // if an update is not already pending. + if (!pending_net_updates_.get()) { + pending_net_updates_.reset(new ListValue()); + MessageLoop::current()->PostDelayedTask(FROM_HERE, + NewRunnableMethod( + this, &MediaInternalsProxy::SendNetEventsOnUIThread), + kMediaInternalsProxyEventDelayMilliseconds); + } + pending_net_updates_->Append(entry); +} + +void MediaInternalsProxy::SendNetEventsOnUIThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + CallJavaScriptFunctionOnUIThread(kSendNetworkEventsFunction, + pending_net_updates_.release()); +} + +void MediaInternalsProxy::CallJavaScriptFunctionOnUIThread( + const std::string& function, Value* args) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + scoped_ptr<Value> args_value(args); + std::vector<const Value*> args_vector; + args_vector.push_back(args_value.get()); + string16 update = WebUI::GetJavascriptCall(function, args_vector); + UpdateUIOnUIThread(update); +} diff --git a/chrome/browser/ui/webui/media/media_internals_proxy.h b/chrome/browser/ui/webui/media/media_internals_proxy.h index ccb0995..1915e94 100644 --- a/chrome/browser/ui/webui/media/media_internals_proxy.h +++ b/chrome/browser/ui/webui/media/media_internals_proxy.h @@ -6,20 +6,27 @@ #define CHROME_BROWSER_UI_WEBUI_MEDIA_MEDIA_INTERNALS_PROXY_H_ #pragma once -#include "base/string16.h" #include "base/memory/ref_counted.h" +#include "base/string16.h" #include "chrome/browser/media/media_internals_observer.h" +#include "chrome/browser/net/chrome_net_log.h" class IOThread; class MediaInternalsMessageHandler; +namespace base { +class ListValue; +class Value; +} + // This class is a proxy between MediaInternals (on the IO thread) and // MediaInternalsMessageHandler (on the UI thread). // It is ref_counted to ensure that it completes all pending Tasks on both // threads before destruction. class MediaInternalsProxy : public MediaInternalsObserver, - public base::RefCountedThreadSafe<MediaInternalsProxy> { + public base::RefCountedThreadSafe<MediaInternalsProxy>, + public ChromeNetLog::ThreadSafeObserver { public: MediaInternalsProxy(); @@ -35,17 +42,38 @@ class MediaInternalsProxy // MediaInternalsObserver implementation. Called on the IO thread. virtual void OnUpdate(const string16& update); + // ChromeNetLog::ThreadSafeObserver implementation. Callable from any thread: + virtual void OnAddEntry(net::NetLog::EventType type, + const base::TimeTicks& time, + const net::NetLog::Source& source, + net::NetLog::EventPhase phase, + net::NetLog::EventParameters* params); + private: friend class base::RefCountedThreadSafe<MediaInternalsProxy>; virtual ~MediaInternalsProxy(); + // Build a dictionary mapping constant names to values. + base::Value* GetConstants(); + void ObserveMediaInternalsOnIOThread(); void StopObservingMediaInternalsOnIOThread(); void GetEverythingOnIOThread(); void UpdateUIOnUIThread(const string16& update); + // Put |entry| on a list of events to be sent to the page. + void AddNetEventOnUIThread(base::Value* entry); + + // Send all pending events to the page. + void SendNetEventsOnUIThread(); + + // Call a JavaScript function on the page. Takes ownership of |args|. + void CallJavaScriptFunctionOnUIThread(const std::string& function, + base::Value* args); + MediaInternalsMessageHandler* handler_; IOThread* io_thread_; + scoped_ptr<base::ListValue> pending_net_updates_; DISALLOW_COPY_AND_ASSIGN(MediaInternalsProxy); }; |