1 // Copyright 2015 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.
8 * Singleton object representing the main window of the Memory Inspector Chrome
12 var MemoryInspectorWindow = function() {
13 // HTML elements in the window.
14 this.terminalElement_ = undefined;
15 this.terminalButtonElement_ = undefined;
16 this.contentsElement_ = undefined;
17 this.inspectorViewElement_ = undefined;
18 this.loadOverlayElement_ = undefined;
19 this.loadMessageElement_ = undefined;
20 this.loadDotsElement_ = undefined;
21 this.phantomLoadDots_ = undefined;
24 this.loadDotsAnimationIntervalId_ = undefined;
27 this.processManager_ = undefined;
30 this.terminalVisible_ = false;
33 /** The threshold for terminal auto-scroll. */
34 MemoryInspectorWindow.AUTOSCROLL_THRESHOLD = 10;
36 /** The interval between steps of the loading dots animation (milliseconds). */
37 MemoryInspectorWindow.LOAD_DOTS_ANIMATION_INTERVAL = 500;
39 /** The maximum number of dots in the loading dots animation. */
40 MemoryInspectorWindow.MAX_LOAD_DOTS_COUNT = 3;
42 /** The interval between polls to the backend server (milliseconds). */
43 MemoryInspectorWindow.POLL_INTERVALL = 500;
46 * Initialize the main window. It will be shown once settings are retrieved
47 * (asynchronous request).
50 MemoryInspectorWindow.prototype.initialize = function() {
52 this.startServerProcess_();
53 this.retrieveSettings_();
58 * Set up the main window. This method retrieves relevant HTML elements, adds
59 * corresponding event handlers, and starts animations.
62 MemoryInspectorWindow.prototype.setUpGui_ = function() {
63 // Retrieve HTML elements in the window.
64 this.terminalElement_ = document.getElementById('terminal');
65 this.terminalButtonElement_ = document.getElementById('terminal_button');
66 this.contentsElement_ = document.getElementById('contents');
67 this.inspectorViewElement_ = document.getElementById('inspector_view');
68 this.loadOverlayElement_ = document.getElementById('load_overlay');
69 this.loadMessageElement_ = document.getElementById('load_message');
70 this.loadDotsElement_ = document.getElementById('load_dots');
71 this.phantomLoadDotsElement_ = document.getElementById('phantom_load_dots');
73 // Hook up the terminal toggle button.
74 this.terminalButtonElement_.addEventListener('click',
75 this.toggleTerminal_.bind(this));
77 // Print app name and version in the terminal.
78 var manifest = chrome.runtime.getManifest();
79 this.printInfo_(manifest.name + ' (version ' + manifest.version + ')\n');
81 // Start the loading dots animation.
82 this.loadDotsAnimationIntervalId_ = window.setInterval(
83 this.animateLoadDots_.bind(this),
84 MemoryInspectorWindow.LOAD_DOTS_ANIMATION_INTERVAL);
88 * Toggle the terminal.
91 MemoryInspectorWindow.prototype.toggleTerminal_ = function() {
92 if (this.terminalVisible_) {
97 this.storeSettings_();
104 MemoryInspectorWindow.prototype.showTerminal_ = function() {
105 // Scroll to the bottom.
106 this.terminalElement_.scrollTop = terminal.scrollHeight;
107 document.body.classList.add('terminal_visible');
108 this.terminalVisible_ = true;
115 MemoryInspectorWindow.prototype.hideTerminal_ = function() {
116 document.body.classList.remove('terminal_visible');
117 this.terminalVisible_ = false;
121 * Stop the loading dots animation.
124 MemoryInspectorWindow.prototype.stopLoadDotsAnimation_ = function() {
125 window.clearInterval(this.loadDotsAnimationIntervalId_);
126 this.loadDotsAnimationIntervalId_ = undefined;
130 * Animate the loading dots.
133 MemoryInspectorWindow.prototype.animateLoadDots_ = function() {
134 if (this.loadDotsElement_.innerText.length >=
135 MemoryInspectorWindow.MAX_LOAD_DOTS_COUNT) {
136 this.loadDotsElement_.innerText = '.';
138 this.loadDotsElement_.innerText += '.';
143 * Start the server process inside PNaCl.
146 MemoryInspectorWindow.prototype.startServerProcess_ = function() {
147 // Create and hook up a NaCl process manager.
148 var mgr = this.processManager = new NaClProcessManager();
149 mgr.setStdoutListener(this.onServerStdout_.bind(this));
150 mgr.setErrorListener(this.onServerError_.bind(this));
151 mgr.setRootProgressListener(this.onServerProgress_.bind(this));
152 mgr.setRootLoadListener(this.onServerLoad_.bind(this));
154 // Set dummy terminal size.
155 // (see https://code.google.com/p/naclports/issues/detail?id=186)
156 mgr.onTerminalResize(200, 200);
158 // Spawn the server process.
159 this.printInfo_('Spawning ' + MemoryInspectorConfig.ARGV.join(' ') + '\n');
161 MemoryInspectorConfig.NMF,
162 MemoryInspectorConfig.ARGV,
163 MemoryInspectorConfig.ENV,
164 MemoryInspectorConfig.CWD,
167 this.onServerProcessSpawned_.bind(this));
171 * Listener called when the server process is spawned.
173 * @param {number} pid The PID of the spawned server process or error code if
176 MemoryInspectorWindow.prototype.onServerProcessSpawned_ = function(pid) {
177 this.processManager.waitpid(pid, 0, this.onServerProcessExit_.bind(this));
181 * Listener called when the server process exits.
183 * @param {number} pid The PID of the server process or an error code on error.
184 * @param {number} status The exit code of the server process.
186 MemoryInspectorWindow.prototype.onServerProcessExit_ = function(pid, status) {
187 this.printInfo_('NaCl module exited with status ' + status + '\n');
191 * Listener called when an stdout event is received from the server process.
193 * @param {string} msg The string sent to stdout.
195 MemoryInspectorWindow.prototype.onServerStdout_ = function(msg) {
196 this.printOutput_(msg);
200 * Listener called when an error event is received from the server process.
202 * @param {string} cmd The name of the process with the error.
203 * @param {string} err The error message.
205 MemoryInspectorWindow.prototype.onServerError_ = function(cmd, err) {
206 this.printError_(cmd + ': ' + err + '\n');
210 * Listener called when a part of the server NaCl module has been loaded.
212 * @param {string} url The URL that is being loaded.
213 * @param {boolean} lengthComputable Is our progress quantitatively measurable?
214 * @param {number} loaded The number of bytes that have been loaded.
215 * @param {number} total The total number of bytes to be loaded.
217 MemoryInspectorWindow.prototype.onServerProgress_ = function(url,
218 lengthComputable, loaded, total) {
219 if (url === undefined) {
223 var message = 'Loading ' + url.substring(url.lastIndexOf('/') + 1);
224 if (lengthComputable && total > 0) {
225 var percentLoaded = Math.round(loaded / total * 100);
226 var kbLoaded = Math.round(loaded / 1024);
227 var kbTotal = Math.round(total / 1024);
228 message += ' [' + kbLoaded + '/' + kbTotal + ' KiB ' + percentLoaded + '%]';
230 this.printInfo_(message + '\n');
234 * Listener called when the server NaCl module has been successfully loaded.
237 MemoryInspectorWindow.prototype.onServerLoad_ = function() {
238 this.printInfo_('NaCl module loaded\n');
242 * Print an output message in the terminal.
244 * @param {string} msg The text of the message.
246 MemoryInspectorWindow.prototype.printOutput_ = function(msg) {
247 this.printMessage_(msg, 'terminal_message_output');
251 * Print an info message in the terminal.
253 * @param {string} msg The text of the message.
255 MemoryInspectorWindow.prototype.printInfo_ = function(msg) {
256 this.printMessage_(msg, 'terminal_message_info');
260 * Print an error message in the terminal.
262 * @param {string} msg The text of the message.
264 MemoryInspectorWindow.prototype.printError_ = function(msg) {
265 this.printMessage_(msg, 'terminal_message_error');
269 * Print a message of a given type in the terminal.
271 * @param {string} msg The text of the message.
272 * @param {string} cls The CSS class of the message.
274 MemoryInspectorWindow.prototype.printMessage_ = function(msg, cls) {
275 // Determine whether we are at the bottom of the terminal.
276 var scrollBottom = this.terminalElement_.scrollTop +
277 this.terminalElement_.clientHeight;
278 var autoscrollBottomMin = this.terminalElement_.scrollHeight -
279 MemoryInspectorWindow.AUTOSCROLL_THRESHOLD;
280 var shouldScroll = scrollBottom >= autoscrollBottomMin;
282 // Append the message to the terminal.
283 var messageElement = document.createElement('span');
284 messageElement.innerText = msg;
285 messageElement.classList.add(cls);
286 this.terminalElement_.appendChild(messageElement);
288 // If we were at the bottom of the terminal, scroll to the bottom again.
290 this.terminalElement_.scrollTop = this.terminalElement_.scrollHeight;
295 * Asynchronously retrieve main window settings from local storage.
298 MemoryInspectorWindow.prototype.retrieveSettings_ = function() {
299 chrome.storage.local.get('terminal_visible',
300 this.onSettingsRetrieved_.bind(this));
304 * Asynchronously store main window settings in local storage.
307 MemoryInspectorWindow.prototype.storeSettings_ = function() {
309 settings['terminal_visible'] = this.terminalVisible_;
310 chrome.storage.local.set(settings);
314 * Listener called when main window settings were retrieved from local storage.
315 * It saves them and finally shows the app window.
317 * @param {Object<*>} settings The retrieved settings.
319 MemoryInspectorWindow.prototype.onSettingsRetrieved_ = function(settings) {
320 if (chrome.runtime.lastError === undefined && settings['terminal_visible']) {
321 // Skip CSS animations by temporarily hiding everything.
322 document.body.style.display = 'none';
323 this.showTerminal_();
324 document.body.style.display = 'block';
327 // We are finally ready to show the window.
328 chrome.app.window.current().show();
332 * Keep polling the backend server until it is reachable. The requests are
336 MemoryInspectorWindow.prototype.pollServer_ = function() {
337 var requestUrl = 'http://127.0.0.1:' + MemoryInspectorConfig.PORT +
338 '?timestamp=' + new Date().getTime();
339 var request = new XMLHttpRequest();
340 request.addEventListener('readystatechange',
341 this.onPollRequestStateChange_.bind(this, request));
342 request.open('GET', requestUrl);
347 * Listener called when the state of the server request changes. If the request
348 * is complete and successful, the inspector view is navigated to the server.
349 * If unsuccessful, the request is repeated. Incomplete requests are ignored.
351 * @param {XMLHttpRequest} request The request.
353 MemoryInspectorWindow.prototype.onPollRequestStateChange_ = function(request) {
354 // We wait until the request is complete.
355 if (request.readyState !== XMLHttpRequest.DONE) {
359 // If the request was not successful, try again.
360 if (request.status !== 200) {
361 console.log('Inspector server unreachable. Trying again...');
362 setTimeout(this.pollServer_.bind(this),
363 MemoryInspectorWindow.POLL_INTERVALL);
367 // Stop the animation, load the inspector, and display it.
368 console.log('Inspector server ready. Loading the inspector view...');
369 this.inspectorViewElement_.addEventListener('loadstop',
370 this.onInspectorLoaded_.bind(this));
371 this.inspectorViewElement_.addEventListener('loadcommit',
372 this.onInspectorNavigated_.bind(this));
373 this.inspectorViewElement_.src = 'http://127.0.0.1:' +
374 MemoryInspectorConfig.PORT;
378 * Listener called when the inspector is loaded. It stops the loading dots
379 * animation, shows the inspector view, and fades out the loading overlay.
382 MemoryInspectorWindow.prototype.onInspectorLoaded_ = function() {
383 this.stopLoadDotsAnimation_();
384 document.body.classList.add('inspector_view_visible');
388 * Listener called when the inspector view navigates to the inspector website.
391 MemoryInspectorWindow.prototype.onInspectorNavigated_ = function() {
392 this.inspectorViewElement_.executeScript({
394 'runAt': 'document_start'
398 window.addEventListener('load', function() {
399 // Create the singleton MemoryInspectorWindow instance and initialize it.
400 var mainWindow = new MemoryInspectorWindow();
401 mainWindow.initialize();
403 // Make the instance global for debugging purposes.
404 window.mainWindow = mainWindow;