jsconf2013



jsconf2013

0 0


jsconf2013


On Github dangoor / jsconf2013

Building Live HTML and Omnisicent Debuggers

JSConf.eu 2013

Kevin Dangoor, Adobe / @dangoor, +KevinDangoor

Peter Flynn, Adobe / @knownissues

Code Editor?

Theseus
Theseus
Instabug
Tern-driven code hints
CSS Live Development
HTML Live Development
HTML Live Development
Live Highlight
Everyscrub
Markdown live preview
SVG live preview
Inline editors
Responsive design editing
PSD Lens
JavaScript hot replacement

HTML Live Development

  • DEMO?

Constraining the Problem

  • Update the page as the user types
  • Do it quickly
  • Try to replace a reasonably minimal part of the page

How hard can it be?

HTML (or XML) Diff Research

Daniel Ehrenberg wrote a paper surveying the research:

A warning: All of the algorithms are fairly difficult to understand. I don’tunderstand all of them; it took me months to figure out the Zhang-Shasha algorithm.

"BULD" was a Starting Point

"Detecting Changes in XML Documents" by Cobéna and Marian
[O]ur algorithm has to be very efficient in terms of speed and memory space even at the cost of some loss of “quality”. Also, it considers, besides insertions, deletions and updates (standard in diffs), a move operation on subtrees that is essential in the context of XML.

The Biggest Difference

We can identify elements thanks to CodeMirror's marks.

The Basic Process

Try to find the affected part of the tree Tokenize/parse the document Generate a list of edits Send the edits to the browser Apply the edits

Incremental or Full?

if (changeList && !changeList.next) {
    if (!isDangerousEdit(changeList.text) && !isDangerousEdit(changeList.removed)) {
        var startMark = _getMarkerAtDocumentPos(editor, changeList.from, true);
        if (startMark) {
            var range = startMark.find();
            if (range) {
                text = editor._codeMirror.getRange(range.from, range.to);
                this.changedTagID = startMark.tagID;
                startOffsetPos = range.from;
                startOffset = editor._codeMirror.indexFromPos(startOffsetPos);
                this.isIncremental = true;
            }
        }
    }
}

Subtrees

Tokenizing/Parsing

Read the tokens, if the doc is invalid stop updating until it becomes valid As tokens are read, tags are matched up with the marks to assign IDs to them Build a "Simple DOM" Calculate hashes for text nodes, element attributes, child nodes, subtrees

Simple DOM

<p class="example"> ID: 1attrSig: 812370188, childSig: 1654266372, subtreeSig: 1543130807 "Some text " sig: -487627308 <em> ID: 2attrSig: 100177033, childSig: 1177450403, subtreeSig: 1177450403 " more text" sig: 855807722   "emphasized text"sig: -52577378

Signatures

update: function () {
            if (this.isElement()) {
                var i,
                    subtreeHashes = "",
                    childHashes = "",
                    child;
                for (i = 0; i < this.children.length; i++) {
                    child = this.children[i];
                    if (child.isElement()) {
                        childHashes += String(child.tagID);
                        subtreeHashes += String(child.tagID) + child.attributeSignature + child.subtreeSignature;
                    } else {
                        childHashes += child.textSignature;
                        subtreeHashes += child.textSignature;
                    }
                }
                this.childSignature = MurmurHash3.hashString(childHashes, childHashes.length, seed);
                this.subtreeSignature = MurmurHash3.hashString(subtreeHashes, subtreeHashes.length, seed);
            } else {
                this.textSignature = MurmurHash3.hashString(this.content, this.content.length, seed);
            }
        },

Edit Example

Hi, JSConf.

We're merging paragraphs.

Example Simple DOM

<section> 1 "\n " <h2> 2 "\n " <p> 3 "\n " <p> 4 "\n "   "Edit Example"   "\n Hi, JSConf.\n "   "\n " <em> 5 "\n "   "We're merging paragraphs."

Simple DOM (Changing)

<section> 1 "\n " <h2> 2 "\n " <p> 3 "\n " <p> 4 "\n "   "Edit Example"   "\n Hi, JSConf.\n "   "\n " <em> 5 "\n "   "We're merging paragraphs."

Simple DOM (New)

<section> 1 "\n " <h2> 2 "\n " <p> 3 "\n "   "Edit Example"   "\n Hi, JSConf.\n " <em> 5 "\n "   "We're merging paragraphs."

Edit Example

Hi, JSConf.

We're merging paragraphs.

Diff Generation

Generate edits to go from old to new.
Start at the top and work down the new tree Compare attribute hashes Compare child signatures Compare subtree signatures (Or add a new element if the element didn't exist in the old tree)

Generating Child Edits

Gets complicated by:
  • Text nodes
  • Moves
  • Large structure changes

Old

<section> 1 "\n " <h2> 2 "\n " <p> 3 "\n " <p> 4 "\n "   "Edit Example"   "\n Hi, JSConf.\n "   "\n " <em> 5 "\n "   "We're merging paragraphs."

New

<section> 1 "\n " <h2> 2 "\n " <p> 3 "\n "   "Edit Example"   "\n Hi, JSConf.\n " <em> 5 "\n "   "We're merging paragraphs."

When you see a difference...

var addElementInsert = function () {
    if (!oldNodeMap[newChild.tagID]) {
        newEdit = {
            type: "elementInsert",
            tag: newChild.tag,
            tagID: newChild.tagID,
            parentID: newChild.parent.tagID,
            attributes: newChild.attributes
        };
        
        newEdits.push(newEdit);        
        newElements.push(newChild);
        textAfterID = newChild.tagID;
        newIndex++;
        return true;
    }
    return false;
};

Performance

Tokenizing/parsing, hash computation, diff generation all seem expensive.

Nope.

The Bottleneck

Calculating mark positions overwhelmed everything else.

Read More

Theseus

Tom Lieber, MIT

Omniscient Debugging

  • Deep history of execution
  • Step backwards
  • Visualize entire history

Omniscient Debugging

  • Deep history of execution
  • Step backwards
  • Visualize entire history
  • Queryable dataset

Demo

How Theseus Works

Injected instrumentation

  • Esprima + Falafel + modified JSHint
  • Fondue instrumentation
  • Serve up instrumented code
  • Retrieve collected data in Brackets
  • Visualize in Brackets UI

Fondue Instrumentation

Function entry/exit

function foo(x) {
    theseus.traceEnter({id: "foo", arguments: [x], this: this});
    return x + 1;
    theseus.traceExit({id: "foo"});
}

Fondue Instrumentation

Tracing invocations to callers

function foo(x) {
    // Call #1
    bar(x);
    // Call #2
    bar(x / 2);
}
function foo(x) {
    // Call #1
    theseus.traceFunCall("foo-bar1", {func: bar}, [x]);
    // Call #2
    theseus.traceFunCall("foo-bar2", {func: bar}, [x / 2]);
}
Records state so next traceEnter() knows where it came from

Fondue Instrumentation

Tracing *async* invocations

function foo(url) {
    $.get(url, function (data) {
        // ...
    });
}
function foo(url) {
    $.get(url, theseus.wrapCallback("foo_1", function (data) {
        // ...
    }) );
}
Saves current foo() invocation in the wrapper so callback knows where it (asynchronously) came from

Read More

A Sandbox

Example: SVG live preview

define(function (require, exports, module) {
    "use strict";
    
    var DocumentManager = brackets.getModule("document/DocumentManager"),
        PanelManager    = brackets.getModule("view/PanelManager"),
        ExtensionUtils  = brackets.getModule("utils/ExtensionUtils");
    
    var previewPanel, currentDoc;
    
    function updatePanel() {
        // Update SVG display
        var $svgParent = $(".svg-preview", previewPanel.$panel);
        $svgParent.html(currentDoc.getText());
        
        var $svgRoot = $svgParent.children();
        $svgParent.width($svgRoot.width());
        $svgParent.height($svgRoot.height());
        
        // Update panel height
        var panelHeight = $svgRoot.height() + 30;
        if (panelHeight !== previewPanel.$panel.height()) {
            previewPanel.$panel.height(panelHeight);
            previewPanel.$panel.trigger("panelResizeUpdate");  // trigger editor resize
        }
    }
    
    function setPanel(newDoc) {
        // Detach from last doc & attach to new one
        if (currentDoc) {
            $(currentDoc).off("change", updatePanel);
        }
        currentDoc = newDoc;
        
        if (currentDoc) {
            $(currentDoc).on("change", updatePanel);
            previewPanel.show();
            updatePanel();
        } else {
            previewPanel.hide();
        }
    }
    
    ExtensionUtils.loadStyleSheet(module, "svg-preview.css");
    
    // Create panel
    var $panel = $("<div id='svg-preview-panel' class='inline-widget'><div class='svg-preview'></div></div>");
    previewPanel = PanelManager.createBottomPanel("svg-preview", $panel, 0);
    
    // Listen for editor switch
    $(DocumentManager).on("currentDocumentChange", function () {
        var newDoc = DocumentManager.getCurrentDocument();
        if (newDoc && newDoc.file.fullPath.match(/\.svg$/i)) {
            setPanel(newDoc);
        } else {
            setPanel(null);
        }
    });
});