main.js revision 731df977c0511bca2206b5f333555b1205ff1f43
1// Copyright (c) 2010 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Dictionary of constants (initialized by browser).
7 */
8var LogEventType = null;
9var LogEventPhase = null;
10var ClientInfo = null;
11var LogSourceType = null;
12var LogLevelType = null;
13var NetError = null;
14var LoadFlag = null;
15var AddressFamily = null;
16
17/**
18 * Object to communicate between the renderer and the browser.
19 * @type {!BrowserBridge}
20 */
21var g_browser = null;
22
23/**
24 * Main entry point. called once the page has loaded.
25 */
26function onLoaded() {
27  g_browser = new BrowserBridge();
28
29  // Create the view which displays events lists, and lets you select, filter
30  // and delete them.
31  var eventsView = new EventsView('eventsListTableBody',
32                                  'filterInput',
33                                  'filterCount',
34                                  'deleteSelected',
35                                  'deleteAll',
36                                  'selectAll',
37                                  'sortById',
38                                  'sortBySource',
39                                  'sortByDescription',
40
41                                  // IDs for the details view.
42                                  'detailsTabHandles',
43                                  'detailsLogTab',
44                                  'detailsTimelineTab',
45                                  'detailsLogBox',
46                                  'detailsTimelineBox',
47
48                                  // IDs for the layout boxes.
49                                  'filterBox',
50                                  'eventsBox',
51                                  'actionBox',
52                                  'splitterBox');
53
54  // Create a view which will display info on the proxy setup.
55  var proxyView = new ProxyView('proxyTabContent',
56                                'proxyOriginalSettings',
57                                'proxyEffectiveSettings',
58                                'proxyReloadSettings',
59                                'badProxiesTableBody',
60                                'clearBadProxies');
61
62  // Create a view which will display information on the host resolver.
63  var dnsView = new DnsView('dnsTabContent',
64                            'hostResolverCacheTbody',
65                            'clearHostResolverCache',
66                            'hostResolverDefaultFamily',
67                            'hostResolverIPv6Disabled',
68                            'hostResolverEnableIPv6',
69                            'hostResolverCacheCapacity',
70                            'hostResolverCacheTTLSuccess',
71                            'hostResolverCacheTTLFailure');
72
73  // Create a view which will display import/export options to control the
74  // captured data.
75  var dataView = new DataView('dataTabContent', 'exportedDataText',
76                              'exportToText', 'securityStrippingCheckbox',
77                              'byteLoggingCheckbox',
78                              'passivelyCapturedCount',
79                              'activelyCapturedCount',
80                              'dataViewDeleteAll');
81
82  // Create a view which will display the results and controls for connection
83  // tests.
84  var testView = new TestView('testTabContent', 'testUrlInput',
85                              'connectionTestsForm', 'testSummary');
86
87  var httpCacheView = new HttpCacheView('httpCacheTabContent',
88                                        'httpCacheStats');
89
90  var socketsView = new SocketsView('socketsTabContent',
91                                    'socketPoolDiv',
92                                    'socketPoolGroupsDiv');
93
94  var spdyView = new SpdyView('spdyTabContent',
95                              'spdySessionNoneSpan',
96                              'spdySessionLinkSpan',
97                              'spdySessionDiv');
98
99
100  var serviceView;
101  if (g_browser.isPlatformWindows()) {
102    serviceView = new ServiceProvidersView('serviceProvidersTab',
103                                           'serviceProvidersTabContent',
104                                           'serviceProvidersTbody',
105                                           'namespaceProvidersTbody');
106  }
107
108  // Create a view which lets you tab between the different sub-views.
109  var categoryTabSwitcher =
110      new TabSwitcherView(new DivView('categoryTabHandles'));
111
112  // Populate the main tabs.
113  categoryTabSwitcher.addTab('eventsTab', eventsView, false);
114  categoryTabSwitcher.addTab('proxyTab', proxyView, false);
115  categoryTabSwitcher.addTab('dnsTab', dnsView, false);
116  categoryTabSwitcher.addTab('socketsTab', socketsView, false);
117  categoryTabSwitcher.addTab('spdyTab', spdyView, false);
118  categoryTabSwitcher.addTab('httpCacheTab', httpCacheView, false);
119  categoryTabSwitcher.addTab('dataTab', dataView, false);
120  if (g_browser.isPlatformWindows())
121    categoryTabSwitcher.addTab('serviceProvidersTab', serviceView, false);
122  categoryTabSwitcher.addTab('testTab', testView, false);
123
124  // Build a map from the anchor name of each tab handle to its "tab ID".
125  // We will consider navigations to the #hash as a switch tab request.
126  var anchorMap = {};
127  var tabIds = categoryTabSwitcher.getAllTabIds();
128  for (var i = 0; i < tabIds.length; ++i) {
129    var aNode = document.getElementById(tabIds[i]);
130    anchorMap[aNode.hash] = tabIds[i];
131  }
132  // Default the empty hash to the data tab.
133  anchorMap['#'] = anchorMap[''] = 'dataTab';
134
135  window.onhashchange = onUrlHashChange.bind(null, anchorMap,
136                                             categoryTabSwitcher);
137
138  // Make this category tab widget the primary view, that fills the whole page.
139  var windowView = new WindowView(categoryTabSwitcher);
140
141  // Trigger initial layout.
142  windowView.resetGeometry();
143
144  // Select the initial view based on the current URL.
145  window.onhashchange();
146
147  // Tell the browser that we are ready to start receiving log events.
148  g_browser.sendReady();
149}
150
151/**
152 * This class provides a "bridge" for communicating between the javascript and
153 * the browser.
154 *
155 * @constructor
156 */
157function BrowserBridge() {
158  // List of observers for various bits of browser state.
159  this.logObservers_ = [];
160  this.connectionTestsObservers_ = [];
161
162  this.pollableDataHelpers_ = {};
163  this.pollableDataHelpers_.proxySettings =
164      new PollableDataHelper('onProxySettingsChanged',
165                             this.sendGetProxySettings.bind(this));
166  this.pollableDataHelpers_.badProxies =
167      new PollableDataHelper('onBadProxiesChanged',
168                             this.sendGetBadProxies.bind(this));
169  this.pollableDataHelpers_.httpCacheInfo =
170      new PollableDataHelper('onHttpCacheInfoChanged',
171                             this.sendGetHttpCacheInfo.bind(this));
172  this.pollableDataHelpers_.hostResolverInfo =
173      new PollableDataHelper('onHostResolverInfoChanged',
174                             this.sendGetHostResolverInfo.bind(this));
175  this.pollableDataHelpers_.socketPoolInfo =
176      new PollableDataHelper('onSocketPoolInfoChanged',
177                             this.sendGetSocketPoolInfo.bind(this));
178  this.pollableDataHelpers_.spdySessionInfo =
179      new PollableDataHelper('onSpdySessionInfoChanged',
180                             this.sendGetSpdySessionInfo.bind(this));
181  if (this.isPlatformWindows()) {
182    this.pollableDataHelpers_.serviceProviders =
183        new PollableDataHelper('onServiceProvidersChanged',
184                               this.sendGetServiceProviders.bind(this));
185  }
186
187  // Cache of the data received.
188  this.numPassivelyCapturedEvents_ = 0;
189  this.capturedEvents_ = [];
190
191  // Next unique id to be assigned to a log entry without a source.
192  // Needed to simplify deletion, identify associated GUI elements, etc.
193  this.nextSourcelessEventId_ = -1;
194}
195
196/*
197 * Takes the current hash in form of "#tab&param1=value1&param2=value2&...".
198 * Puts the parameters in an object, and passes the resulting object to
199 * |categoryTabSwitcher|.  Uses tab and |anchorMap| to find a tab ID,
200 * which it also passes to the tab switcher.
201 *
202 * Parameters and values are decoded with decodeURIComponent().
203 */
204function onUrlHashChange(anchorMap, categoryTabSwitcher) {
205  var parameters = window.location.hash.split('&');
206
207  var tabId = anchorMap[parameters[0]];
208  if (!tabId)
209    return;
210
211  // Split each string except the first around the '='.
212  var paramDict = null;
213  for (var i = 1; i < parameters.length; i++) {
214    var paramStrings = parameters[i].split('=');
215    if (paramStrings.length != 2)
216      continue;
217    if (paramDict == null)
218      paramDict = {};
219    var key = decodeURIComponent(paramStrings[0]);
220    var value = decodeURIComponent(paramStrings[1]);
221    paramDict[key] = value;
222  }
223
224  categoryTabSwitcher.switchToTab(tabId, paramDict);
225}
226
227/**
228 * Delay in milliseconds between updates of certain browser information.
229 */
230BrowserBridge.POLL_INTERVAL_MS = 5000;
231
232//------------------------------------------------------------------------------
233// Messages sent to the browser
234//------------------------------------------------------------------------------
235
236BrowserBridge.prototype.sendReady = function() {
237  chrome.send('notifyReady');
238
239  // Some of the data we are interested is not currently exposed as a stream,
240  // so we will poll the browser to find out when it changes and then notify
241  // the observers.
242  window.setInterval(this.checkForUpdatedInfo.bind(this, false),
243                     BrowserBridge.POLL_INTERVAL_MS);
244};
245
246BrowserBridge.prototype.isPlatformWindows = function() {
247  return /Win/.test(navigator.platform);
248};
249
250BrowserBridge.prototype.sendGetProxySettings = function() {
251  // The browser will call receivedProxySettings on completion.
252  chrome.send('getProxySettings');
253};
254
255BrowserBridge.prototype.sendReloadProxySettings = function() {
256  chrome.send('reloadProxySettings');
257};
258
259BrowserBridge.prototype.sendGetBadProxies = function() {
260  // The browser will call receivedBadProxies on completion.
261  chrome.send('getBadProxies');
262};
263
264BrowserBridge.prototype.sendGetHostResolverInfo = function() {
265  // The browser will call receivedHostResolverInfo on completion.
266  chrome.send('getHostResolverInfo');
267};
268
269BrowserBridge.prototype.sendClearBadProxies = function() {
270  chrome.send('clearBadProxies');
271};
272
273BrowserBridge.prototype.sendClearHostResolverCache = function() {
274  chrome.send('clearHostResolverCache');
275};
276
277BrowserBridge.prototype.sendStartConnectionTests = function(url) {
278  chrome.send('startConnectionTests', [url]);
279};
280
281BrowserBridge.prototype.sendGetHttpCacheInfo = function() {
282  chrome.send('getHttpCacheInfo');
283};
284
285BrowserBridge.prototype.sendGetSocketPoolInfo = function() {
286  chrome.send('getSocketPoolInfo');
287};
288
289BrowserBridge.prototype.sendGetSpdySessionInfo = function() {
290  chrome.send('getSpdySessionInfo');
291};
292
293BrowserBridge.prototype.sendGetServiceProviders = function() {
294  chrome.send('getServiceProviders');
295};
296
297BrowserBridge.prototype.enableIPv6 = function() {
298  chrome.send('enableIPv6');
299};
300
301BrowserBridge.prototype.setLogLevel = function(logLevel) {
302  chrome.send('setLogLevel', ['' + logLevel]);
303}
304
305//------------------------------------------------------------------------------
306// Messages received from the browser
307//------------------------------------------------------------------------------
308
309BrowserBridge.prototype.receivedLogEntry = function(logEntry) {
310  // Silently drop entries received before ready to receive them.
311  if (!this.areLogTypesReady_())
312    return;
313  // Assign unique ID, if needed.
314  if (logEntry.source.id == 0) {
315    logEntry.source.id = this.nextSourcelessEventId_;
316    --this.nextSourcelessEventId_;
317  }
318  this.capturedEvents_.push(logEntry);
319  for (var i = 0; i < this.logObservers_.length; ++i)
320    this.logObservers_[i].onLogEntryAdded(logEntry);
321};
322
323BrowserBridge.prototype.receivedLogEventTypeConstants = function(constantsMap) {
324  LogEventType = constantsMap;
325};
326
327BrowserBridge.prototype.receivedClientInfo =
328function(info) {
329  ClientInfo = info;
330};
331
332BrowserBridge.prototype.receivedLogEventPhaseConstants =
333function(constantsMap) {
334  LogEventPhase = constantsMap;
335};
336
337BrowserBridge.prototype.receivedLogSourceTypeConstants =
338function(constantsMap) {
339  LogSourceType = constantsMap;
340};
341
342BrowserBridge.prototype.receivedLogLevelConstants =
343function(constantsMap) {
344  LogLevelType = constantsMap;
345};
346
347BrowserBridge.prototype.receivedLoadFlagConstants = function(constantsMap) {
348  LoadFlag = constantsMap;
349};
350
351BrowserBridge.prototype.receivedNetErrorConstants = function(constantsMap) {
352  NetError = constantsMap;
353};
354
355BrowserBridge.prototype.receivedAddressFamilyConstants =
356function(constantsMap) {
357  AddressFamily = constantsMap;
358};
359
360BrowserBridge.prototype.receivedTimeTickOffset = function(timeTickOffset) {
361  this.timeTickOffset_ = timeTickOffset;
362};
363
364BrowserBridge.prototype.receivedProxySettings = function(proxySettings) {
365  this.pollableDataHelpers_.proxySettings.update(proxySettings);
366};
367
368BrowserBridge.prototype.receivedBadProxies = function(badProxies) {
369  this.pollableDataHelpers_.badProxies.update(badProxies);
370};
371
372BrowserBridge.prototype.receivedHostResolverInfo =
373function(hostResolverInfo) {
374  this.pollableDataHelpers_.hostResolverInfo.update(hostResolverInfo);
375};
376
377BrowserBridge.prototype.receivedSocketPoolInfo = function(socketPoolInfo) {
378  this.pollableDataHelpers_.socketPoolInfo.update(socketPoolInfo);
379};
380
381BrowserBridge.prototype.receivedSpdySessionInfo = function(spdySessionInfo) {
382  this.pollableDataHelpers_.spdySessionInfo.update(spdySessionInfo);
383};
384
385BrowserBridge.prototype.receivedServiceProviders = function(serviceProviders) {
386  this.pollableDataHelpers_.serviceProviders.update(serviceProviders);
387};
388
389BrowserBridge.prototype.receivedPassiveLogEntries = function(entries) {
390  this.numPassivelyCapturedEvents_ += entries.length;
391  for (var i = 0; i < entries.length; ++i) {
392    var entry = entries[i];
393    entry.wasPassivelyCaptured = true;
394    this.receivedLogEntry(entry);
395  }
396};
397
398
399BrowserBridge.prototype.receivedStartConnectionTestSuite = function() {
400  for (var i = 0; i < this.connectionTestsObservers_.length; ++i)
401    this.connectionTestsObservers_[i].onStartedConnectionTestSuite();
402};
403
404BrowserBridge.prototype.receivedStartConnectionTestExperiment = function(
405    experiment) {
406  for (var i = 0; i < this.connectionTestsObservers_.length; ++i) {
407    this.connectionTestsObservers_[i].onStartedConnectionTestExperiment(
408        experiment);
409  }
410};
411
412BrowserBridge.prototype.receivedCompletedConnectionTestExperiment =
413function(info) {
414  for (var i = 0; i < this.connectionTestsObservers_.length; ++i) {
415    this.connectionTestsObservers_[i].onCompletedConnectionTestExperiment(
416        info.experiment, info.result);
417  }
418};
419
420BrowserBridge.prototype.receivedCompletedConnectionTestSuite = function() {
421  for (var i = 0; i < this.connectionTestsObservers_.length; ++i)
422    this.connectionTestsObservers_[i].onCompletedConnectionTestSuite();
423};
424
425BrowserBridge.prototype.receivedHttpCacheInfo = function(info) {
426  this.pollableDataHelpers_.httpCacheInfo.update(info);
427};
428
429BrowserBridge.prototype.areLogTypesReady_ = function() {
430  return (LogEventType  != null &&
431          LogEventPhase != null &&
432          LogSourceType != null);
433};
434
435//------------------------------------------------------------------------------
436
437/**
438 * Adds a listener of log entries. |observer| will be called back when new log
439 * data arrives, through:
440 *
441 *   observer.onLogEntryAdded(logEntry)
442 */
443BrowserBridge.prototype.addLogObserver = function(observer) {
444  this.logObservers_.push(observer);
445};
446
447/**
448 * Adds a listener of the proxy settings. |observer| will be called back when
449 * data is received, through:
450 *
451 *   observer.onProxySettingsChanged(proxySettings)
452 *
453 * |proxySettings| is a dictionary with (up to) two properties:
454 *
455 *   "original"  -- The settings that chrome was configured to use
456 *                  (i.e. system settings.)
457 *   "effective" -- The "effective" proxy settings that chrome is using.
458 *                  (decides between the manual/automatic modes of the
459 *                  fetched settings).
460 *
461 * Each of these two configurations is formatted as a string, and may be
462 * omitted if not yet initialized.
463 *
464 * TODO(eroman): send a dictionary instead.
465 */
466BrowserBridge.prototype.addProxySettingsObserver = function(observer) {
467  this.pollableDataHelpers_.proxySettings.addObserver(observer);
468};
469
470/**
471 * Adds a listener of the proxy settings. |observer| will be called back when
472 * data is received, through:
473 *
474 *   observer.onBadProxiesChanged(badProxies)
475 *
476 * |badProxies| is an array, where each entry has the property:
477 *   badProxies[i].proxy_uri: String identify the proxy.
478 *   badProxies[i].bad_until: The time when the proxy stops being considered
479 *                            bad. Note the time is in time ticks.
480 */
481BrowserBridge.prototype.addBadProxiesObserver = function(observer) {
482  this.pollableDataHelpers_.badProxies.addObserver(observer);
483};
484
485/**
486 * Adds a listener of the host resolver info. |observer| will be called back
487 * when data is received, through:
488 *
489 *   observer.onHostResolverInfoChanged(hostResolverInfo)
490 */
491BrowserBridge.prototype.addHostResolverInfoObserver = function(observer) {
492  this.pollableDataHelpers_.hostResolverInfo.addObserver(observer);
493};
494
495/**
496 * Adds a listener of the socket pool. |observer| will be called back
497 * when data is received, through:
498 *
499 *   observer.onSocketPoolInfoChanged(socketPoolInfo)
500 */
501BrowserBridge.prototype.addSocketPoolInfoObserver = function(observer) {
502  this.pollableDataHelpers_.socketPoolInfo.addObserver(observer);
503};
504
505/**
506 * Adds a listener of the SPDY info. |observer| will be called back
507 * when data is received, through:
508 *
509 *   observer.onSpdySessionInfoChanged(spdySessionInfo)
510 */
511BrowserBridge.prototype.addSpdySessionInfoObserver = function(observer) {
512  this.pollableDataHelpers_.spdySessionInfo.addObserver(observer);
513};
514
515/**
516 * Adds a listener of the service providers info. |observer| will be called
517 * back when data is received, through:
518 *
519 *   observer.onServiceProvidersChanged(serviceProviders)
520 */
521BrowserBridge.prototype.addServiceProvidersObserver = function(observer) {
522  this.pollableDataHelpers_.serviceProviders.addObserver(observer);
523};
524
525/**
526 * Adds a listener for the progress of the connection tests.
527 * The observer will be called back with:
528 *
529 *   observer.onStartedConnectionTestSuite();
530 *   observer.onStartedConnectionTestExperiment(experiment);
531 *   observer.onCompletedConnectionTestExperiment(experiment, result);
532 *   observer.onCompletedConnectionTestSuite();
533 */
534BrowserBridge.prototype.addConnectionTestsObserver = function(observer) {
535  this.connectionTestsObservers_.push(observer);
536};
537
538/**
539 * Adds a listener for the http cache info results.
540 * The observer will be called back with:
541 *
542 *   observer.onHttpCacheInfoChanged(info);
543 */
544BrowserBridge.prototype.addHttpCacheInfoObserver = function(observer) {
545  this.pollableDataHelpers_.httpCacheInfo.addObserver(observer);
546};
547
548/**
549 * The browser gives us times in terms of "time ticks" in milliseconds.
550 * This function converts the tick count to a Date() object.
551 *
552 * @param {String} timeTicks.
553 * @returns {Date} The time that |timeTicks| represents.
554 */
555BrowserBridge.prototype.convertTimeTicksToDate = function(timeTicks) {
556  // Note that the subtraction by 0 is to cast to a number (probably a float
557  // since the numbers are big).
558  var timeStampMs = (this.timeTickOffset_ - 0) + (timeTicks - 0);
559  var d = new Date();
560  d.setTime(timeStampMs);
561  return d;
562};
563
564/**
565 * Returns a list of all captured events.
566 */
567BrowserBridge.prototype.getAllCapturedEvents = function() {
568  return this.capturedEvents_;
569};
570
571/**
572 * Returns the number of events that were captured while we were
573 * listening for events.
574 */
575BrowserBridge.prototype.getNumActivelyCapturedEvents = function() {
576  return this.capturedEvents_.length - this.numPassivelyCapturedEvents_;
577};
578
579/**
580 * Returns the number of events that were captured passively by the
581 * browser prior to when the net-internals page was started.
582 */
583BrowserBridge.prototype.getNumPassivelyCapturedEvents = function() {
584  return this.numPassivelyCapturedEvents_;
585};
586
587/**
588 * Deletes captured events with source IDs in |sourceIds|.
589 */
590BrowserBridge.prototype.deleteEventsBySourceId = function(sourceIds) {
591  var sourceIdDict = {};
592  for (var i = 0; i < sourceIds.length; i++)
593    sourceIdDict[sourceIds[i]] = true;
594
595  var newEventList = [];
596  for (var i = 0; i < this.capturedEvents_.length; ++i) {
597    var id = this.capturedEvents_[i].source.id;
598    if (id in sourceIdDict) {
599      if (this.capturedEvents_[i].wasPassivelyCaptured)
600        --this.numPassivelyCapturedEvents_;
601      continue;
602    }
603    newEventList.push(this.capturedEvents_[i]);
604  }
605  this.capturedEvents_ = newEventList;
606
607  for (var i = 0; i < this.logObservers_.length; ++i)
608    this.logObservers_[i].onLogEntriesDeleted(sourceIds);
609};
610
611/**
612 * Deletes all captured events.
613 */
614BrowserBridge.prototype.deleteAllEvents = function() {
615  this.capturedEvents_ = [];
616  this.numPassivelyCapturedEvents_ = 0;
617  for (var i = 0; i < this.logObservers_.length; ++i)
618    this.logObservers_[i].onAllLogEntriesDeleted();
619};
620
621/**
622 * If |force| is true, calls all startUpdate functions.  Otherwise, just
623 * runs updates with active observers.
624 */
625BrowserBridge.prototype.checkForUpdatedInfo = function(force) {
626  for (name in this.pollableDataHelpers_) {
627    var helper = this.pollableDataHelpers_[name];
628    if (force || helper.hasActiveObserver())
629      helper.startUpdate();
630  }
631};
632
633/**
634 * Calls all startUpdate functions and, if |callback| is non-null,
635 * calls it with the results of all updates.
636 */
637BrowserBridge.prototype.updateAllInfo = function(callback) {
638  if (callback)
639    new UpdateAllObserver(callback, this.pollableDataHelpers_);
640  this.checkForUpdatedInfo(true);
641};
642
643/**
644 * This is a helper class used by BrowserBridge, to keep track of:
645 *   - the list of observers interested in some piece of data.
646 *   - the last known value of that piece of data.
647 *   - the name of the callback method to invoke on observers.
648 *   - the update function.
649 * @constructor
650 */
651function PollableDataHelper(observerMethodName, startUpdateFunction) {
652  this.observerMethodName_ = observerMethodName;
653  this.startUpdate = startUpdateFunction;
654  this.observerInfos_ = [];
655}
656
657PollableDataHelper.prototype.getObserverMethodName = function() {
658  return this.observerMethodName_;
659};
660
661/**
662 * This is a helper class used by PollableDataHelper, to keep track of
663 * each observer and whether or not it has received any data.  The
664 * latter is used to make sure that new observers get sent data on the
665 * update following their creation.
666 * @constructor
667 */
668function ObserverInfo(observer) {
669  this.observer = observer;
670  this.hasReceivedData = false;
671}
672
673PollableDataHelper.prototype.addObserver = function(observer) {
674  this.observerInfos_.push(new ObserverInfo(observer));
675};
676
677PollableDataHelper.prototype.removeObserver = function(observer) {
678  for (var i = 0; i < this.observerInfos_.length; ++i) {
679    if (this.observerInfos_[i].observer == observer) {
680      this.observerInfos_.splice(i, 1);
681      return;
682    }
683  }
684};
685
686/**
687 * Helper function to handle calling all the observers, but ONLY if the data has
688 * actually changed since last time or the observer has yet to receive any data.
689 * This is used for data we received from browser on an update loop.
690 */
691PollableDataHelper.prototype.update = function(data) {
692  var prevData = this.currentData_;
693  var changed = false;
694
695  // If the data hasn't changed since last time, will only need to notify
696  // observers that have not yet received any data.
697  if (!prevData || JSON.stringify(prevData) != JSON.stringify(data)) {
698    changed = true;
699    this.currentData_ = data;
700  }
701
702  // Notify the observers of the change, as needed.
703  for (var i = 0; i < this.observerInfos_.length; ++i) {
704    var observerInfo = this.observerInfos_[i];
705    if (changed || !observerInfo.hasReceivedData) {
706      observerInfo.observer[this.observerMethodName_](this.currentData_);
707      observerInfo.hasReceivedData = true;
708    }
709  }
710};
711
712/**
713 * Returns true if one of the observers actively wants the data
714 * (i.e. is visible).
715 */
716PollableDataHelper.prototype.hasActiveObserver = function() {
717  for (var i = 0; i < this.observerInfos_.length; ++i) {
718    if (this.observerInfos_[i].observer.isActive())
719      return true;
720  }
721  return false;
722};
723
724/**
725 * This is a helper class used by BrowserBridge to send data to
726 * a callback once data from all polls has been received.
727 *
728 * It works by keeping track of how many polling functions have
729 * yet to receive data, and recording the data as it it received.
730 *
731 * @constructor
732 */
733function UpdateAllObserver(callback, pollableDataHelpers) {
734  this.callback_ = callback;
735  this.observingCount_ = 0;
736  this.updatedData_ = {};
737
738  for (name in pollableDataHelpers) {
739    ++this.observingCount_;
740    var helper = pollableDataHelpers[name];
741    helper.addObserver(this);
742    this[helper.getObserverMethodName()] =
743        this.onDataReceived_.bind(this, helper, name);
744  }
745}
746
747UpdateAllObserver.prototype.isActive = function() {
748  return true;
749};
750
751UpdateAllObserver.prototype.onDataReceived_ = function(helper, name, data) {
752  helper.removeObserver(this);
753  --this.observingCount_;
754  this.updatedData_[name] = data;
755  if (this.observingCount_ == 0)
756    this.callback_(this.updatedData_);
757};
758