Bug 1233927 - Switching between large JS files in debugger is slow. r=jlong
authorJason Laster <jason.laster.11@gmail.com>
Mon, 28 Mar 2016 13:12:28 -0700
changeset 290716 2572bf0929df26a694f58cc8765da73abea0f655
parent 290715 f40c52c4b8005e98065133bf4baaa33b5ca62084
child 290717 34a3c2d3359fb3b233a2a46f4e176459ace6b354
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlong
bugs1233927
milestone48.0a1
Bug 1233927 - Switching between large JS files in debugger is slow. r=jlong Changes the way that source files are loaded in the editor. Previously, source text and mode were set on the existing editor document. Now, source files are their own CodeMirror Documents, so when it comes time to showing them in the editor, it's just a matter of swapping one document for another. Notes: + The DebuggerView now has a _setEditorDocument method for showing a source document + The Editor now has support for creating documents and replacing documents. MozReview-Commit-ID: HrkiHrsJPOB
devtools/client/debugger/debugger-view.js
devtools/client/sourceeditor/debugger.js
devtools/client/sourceeditor/editor.js
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -77,16 +77,17 @@ var DebuggerView = {
     this.Sources.initialize();
     this.VariableBubble.initialize();
     this.WatchExpressions.initialize();
     this.EventListeners.initialize();
     this.GlobalSearch.initialize();
     this._initializeVariablesView();
 
     this._editorSource = {};
+    this._editorDocuments = {};
 
     document.title = L10N.getStr("DebuggerWindowTitle");
 
     this.editor.on("cursorActivity", this.Sources._onEditorCursorActivity);
 
     this.controller = DebuggerController;
     const getState = this.controller.getState;
 
@@ -411,24 +412,38 @@ var DebuggerView = {
   showProgressBar: function() {
     this._editorDeck.selectedIndex = 2;
   },
 
   /**
    * Sets the currently displayed text contents in the source editor.
    * This resets the mode and undo stack.
    *
+   * @param string documentKey
+   *        Key to get the correct editor document
+   *
    * @param string aTextContent
    *        The source text content.
+   *
+   * @param boolean shouldUpdateText
+            Forces a text and mode reset
    */
-  _setEditorText: function(aTextContent = "") {
-    this.editor.setMode(Editor.modes.text);
-    this.editor.setText(aTextContent);
+  _setEditorText: function(documentKey, aTextContent = "", shouldUpdateText = false) {
+    const isNew = this._setEditorDocument(documentKey);
+
     this.editor.clearDebugLocation();
     this.editor.clearHistory();
+    this.editor.setCursor({ line: 0, ch: 0});
+    this.editor.removeBreakpoints();
+
+    // Only set editor's text and mode if it is a new document
+    if (isNew || shouldUpdateText) {
+      this.editor.setMode(Editor.modes.text);
+      this.editor.setText(aTextContent);
+    }
   },
 
   /**
    * Sets the proper editor mode (JS or HTML) according to the specified
    * content type, or by determining the type from the url or text content.
    *
    * @param string aUrl
    *        The source url.
@@ -448,16 +463,39 @@ var DebuggerView = {
     if (aTextContent.match(/^\s*</)) {
       return void this.editor.setMode(Editor.modes.html);
     }
 
     // Unknown language, use text.
     this.editor.setMode(Editor.modes.text);
   },
 
+  /**
+   * Sets the editor's displayed document.
+   * If there isn't a document for the source, create one
+   *
+   * @param string key - key used to access the editor document cache
+   *
+   * @return boolean isNew - was the document just created
+   */
+  _setEditorDocument: function(key) {
+    let isNew;
+
+    if (!this._editorDocuments[key]) {
+      isNew = true;
+      this._editorDocuments[key] = this.editor.createDocument();
+    } else {
+      isNew = false;
+    }
+
+    const doc = this._editorDocuments[key];
+    this.editor.replaceDocument(doc);
+    return isNew;
+  },
+
   renderBlackBoxed: function(source) {
     this._renderSourceText(
       source,
       queries.getSourceText(this.controller.getState(), source.actor)
     );
   },
 
   renderPrettyPrinted: function(source) {
@@ -473,16 +511,17 @@ var DebuggerView = {
       queries.getSourceText(this.controller.getState(), source.actor),
       queries.getSelectedSourceOpts(this.controller.getState())
     );
   },
 
   _renderSourceText: function(source, textInfo, opts = {}) {
     const selectedSource = queries.getSelectedSource(this.controller.getState());
 
+    // Exit early if we're attempting to render an unselected source
     if (!selectedSource || selectedSource.actor !== source.actor) {
       return;
     }
 
     if (source.isBlackBoxed) {
       this.showBlackBoxMessage();
       setTimeout(() => {
         window.emit(EVENTS.SOURCE_SHOWN, source);
@@ -492,22 +531,22 @@ var DebuggerView = {
     else {
       this.showEditor();
     }
 
     if (textInfo.loading) {
       // TODO: bug 1228866, we need to update `_editorSource` here but
       // still make the editor be updated when the full text comes
       // through somehow.
-      this._setEditorText(L10N.getStr("loadingText"));
+      this._setEditorText('loading', L10N.getStr("loadingText"));
       return;
     }
     else if (textInfo.error) {
       let msg = L10N.getFormatStr("errorLoadingText2", textInfo.error);
-      this._setEditorText(msg);
+      this._setEditorText('error', msg);
       Cu.reportError(msg);
       dumpn(msg);
 
       this.showEditor();
       window.emit(EVENTS.SOURCE_ERROR_SHOWN, source);
       return;
     }
 
@@ -524,24 +563,28 @@ var DebuggerView = {
 
     if (this._editorSource.actor === source.actor &&
         this._editorSource.prettyPrinted === source.isPrettyPrinted &&
         this._editorSource.blackboxed === source.isBlackBoxed) {
       this.updateEditorPosition(opts);
       return;
     }
 
+    let { text, contentType } = textInfo;
+    let shouldUpdateText = this._editorSource.prettyPrinted != source.isPrettyPrinted;
+    this._setEditorText(source.actor, text, shouldUpdateText);
+
     this._editorSource.actor = source.actor;
     this._editorSource.prettyPrinted = source.isPrettyPrinted;
     this._editorSource.blackboxed = source.isBlackBoxed;
+    this._editorSource.prettyPrinted = source.isPrettyPrinted;
 
-    let { text, contentType } = textInfo;
-    this._setEditorText(text);
     this._setEditorMode(source.url, contentType, text);
     this.updateEditorBreakpoints(source);
+
     setTimeout(() => {
       window.emit(EVENTS.SOURCE_SHOWN, source);
     }, 0);
 
     this.updateEditorPosition(opts);
   },
 
   updateEditorPosition: function(opts) {
@@ -783,29 +826,29 @@ var DebuggerView = {
     this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   handleTabNavigation: function() {
     dumpn("Handling tab navigation in the DebuggerView");
-
     this.Filtering.clearSearch();
     this.GlobalSearch.clearView();
     this.StackFrames.empty();
     this.Sources.empty();
     this.Variables.empty();
     this.EventListeners.empty();
 
     if (this.editor) {
       this.editor.setMode(Editor.modes.text);
       this.editor.setText("");
       this.editor.clearHistory();
       this._editorSource = {};
+      this._editorDocuments = {};
     }
   },
 
   Toolbar: null,
   Options: null,
   Filtering: null,
   GlobalSearch: null,
   StackFrames: null,
--- a/devtools/client/sourceeditor/debugger.js
+++ b/devtools/client/sourceeditor/debugger.js
@@ -169,18 +169,38 @@ function addBreakpoint(ctx, line, cond) 
     DevToolsUtils.executeSoon(() => _addBreakpoint(ctx, line, cond));
   } else {
     _addBreakpoint(ctx, line, cond);
   }
   return deferred.promise;
 }
 
 /**
+ * Helps reset the debugger's breakpoint state
+ * - removes the breakpoints in the editor
+ * - cleares the debugger's breakpoint state
+ *
+ * Note, does not *actually* remove a source's breakpoints.
+ * The canonical state is kept in the app state.
+ *
+ */
+function removeBreakpoints(ctx) {
+  let { ed, cm } = ctx;
+
+  let meta = dbginfo.get(ed);
+  if (meta.breakpoints != null) {
+    meta.breakpoints = {};
+  }
+
+  cm.doc.iter((line) => { removeBreakpoint(ctx, line) });
+}
+
+/**
  * Removes a visual breakpoint from a specified line and
- * makes Editor to emit a breakpointRemoved event.
+ * makes Editor emit a breakpointRemoved event.
  */
 function removeBreakpoint(ctx, line) {
   if (!hasBreakpoint(ctx, line)) {
     return;
   }
 
   let { ed, cm } = ctx;
   let meta = dbginfo.get(ed);
@@ -298,12 +318,12 @@ function findNext(ctx, query) {
 function findPrev(ctx, query) {
   doSearch(ctx, true, query);
 }
 
 // Export functions
 
 [
   initialize, hasBreakpoint, addBreakpoint, removeBreakpoint, moveBreakpoint,
-  setBreakpointCondition, removeBreakpointCondition, getBreakpoints,
+  setBreakpointCondition, removeBreakpointCondition, getBreakpoints, removeBreakpoints,
   setDebugLocation, getDebugLocation, clearDebugLocation, find, findNext,
   findPrev
 ].forEach(func => module.exports[func.name] = func);
--- a/devtools/client/sourceeditor/editor.js
+++ b/devtools/client/sourceeditor/editor.js
@@ -236,16 +236,17 @@ function Editor(config) {
 
   events.decorate(this);
 }
 
 Editor.prototype = {
   container: null,
   version: null,
   config: null,
+  Doc: null,
 
   /**
    * Appends the current Editor instance to the element specified by
    * 'el'. You can also provide your won iframe to host the editor as
    * an optional second parameter. This method actually creates and
    * loads CodeMirror and all its dependencies.
    *
    * This method is asynchronous and returns a promise.
@@ -297,16 +298,17 @@ Editor.prototype = {
 
       win.CodeMirror.commands.save = () => this.emit("saveRequested");
 
       // Create a CodeMirror instance add support for context menus,
       // overwrite the default controller (otherwise items in the top and
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
+      this.Doc = win.CodeMirror.Doc;
 
       // Disable APZ for source editors. It currently causes the line numbers to
       // "tear off" and swim around on top of the content. Bug 1160601 tracks
       // finding a solution that allows APZ to work with CodeMirror.
       cm.getScrollerElement().addEventListener("wheel", ev => {
         // By handling the wheel events ourselves, we force the platform to
         // scroll synchronously, like it did before APZ. However, we lose smooth
         // scrolling for users with mouse wheels. This seems acceptible vs.
@@ -484,16 +486,32 @@ Editor.prototype = {
     if (!this.container) {
       throw new Error("Can't load a script until the editor is loaded.");
     }
     let win = this.container.contentWindow.wrappedJSObject;
     Services.scriptloader.loadSubScript(url, win, "utf8");
   },
 
   /**
+   * Creates a CodeMirror Document
+   * @returns CodeMirror.Doc
+   */
+  createDocument: function() {
+     return new this.Doc("");
+  },
+
+  /**
+   * Replaces the current document with a new source document
+   */
+  replaceDocument: function(doc) {
+    let cm = editors.get(this);
+    cm.swapDoc(doc);
+  },
+
+  /**
    * Changes the value of a currently used highlighting mode.
    * See Editor.modes for the list of all supported modes.
    */
   setMode: function(value) {
     this.setOption("mode", value);
 
     // If autocomplete was set up and the mode is changing, then
     // turn it off and back on again so the proper mode can be used.