Merge mozilla-central to autoland. CLOSED TREE
authorNarcis Beleuzu <nbeleuzu@mozilla.com>
Wed, 28 Nov 2018 12:00:56 +0200
changeset 507700 1af50e5a47da5a1c95c7bb915d23704c12846dfc
parent 507699 dd2f3ac3a0baa2993f64a7fdcde82592e09a375c (current diff)
parent 507690 5c66354bff282452a6f1a3c911fa8756b6e752af (diff)
child 507701 ff378a3336a5704867e20ea5a27893e729aa5f9e
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. CLOSED TREE
--- a/devtools/client/debugger/new/images/moz.build
+++ b/devtools/client/debugger/new/images/moz.build
@@ -16,16 +16,17 @@ DevToolsModules(
     'command-chevron.svg',
     'disable-pausing.svg',
     'domain.svg',
     'extension.svg',
     'file.svg',
     'folder.svg',
     'help.svg',
     'javascript.svg',
+    'next.svg',
     'pause.svg',
     'prettyPrint.svg',
     'react.svg',
     'resume.svg',
     'stepIn.svg',
     'stepOut.svg',
     'stepOver.svg',
     'tab.svg',
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/images/next.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg"
+     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
+<path d="M12.4,2.1c-0.3,0-0.5,0.2-0.5,0.5v4.8c0,0-0.1-0.1-0.1-0.1l-7.4-5C3.8,1.8,3,2.2,3,3v10c0,0.8,0.8,1.3,1.4,0.8l7.4-5
+    c0.1,0,0.1-0.1,0.1-0.1v4.8c0,0.3,0.2,0.5,0.5,0.5s0.5-0.2,0.5-0.5v-11C12.9,2.3,12.7,2.1,12.4,2.1z M3.9,13V3l7.4,5L3.9,13z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/debugger/new/images/pause.svg
+++ b/devtools/client/debugger/new/images/pause.svg
@@ -1,8 +1,8 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
-  <g fill-rule="evenodd">
-    <path d="M5 12.503l.052-9a.5.5 0 0 0-1-.006l-.052 9a.5.5 0 0 0 1 .006zM12 12.497l-.05-9A.488.488 0 0 0 11.474 3a.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/>
-  </g>
+<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg"
+     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
+	<path d="M5,13.5C5,13.8,4.7,14,4.5,14C4.2,14,4,13.8,4,13.5V2.6c0-0.3,0.2-0.5,0.5-0.5C4.7,2.1,5,2.3,5,2.6V13.5z"/>
+	<path d="M11.9,13.5c0,0.3-0.2,0.5-0.5,0.5s-0.5-0.2-0.5-0.5V2.6c0-0.3,0.2-0.5,0.5-0.5s0.5,0.2,0.5,0.5V13.5z"/>
 </svg>
--- a/devtools/client/debugger/new/images/resume.svg
+++ b/devtools/client/debugger/new/images/resume.svg
@@ -1,6 +1,6 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+<svg  xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
   <path fill="black" id="svg_1" fill-rule="evenodd" d="m4.55195,12.97461l7.4,-5l-7.4,-5l0,10zm-0.925,0l0,-10c0,-0.785 0.8,-1.264 1.415,-0.848l7.4,5c0.58,0.392 0.58,1.304 0,1.696l-7.4,5c-0.615,0.416 -1.415,-0.063 -1.415,-0.848z"/>
 </svg>
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -49,23 +49,16 @@ function attachThread(toolbox) {
       if (res.error) {
         reject(new Error("Couldn't attach to thread: " + res.error));
         return;
       }
 
       threadClient.addListener("paused", handleThreadState.bind(null, toolbox));
       threadClient.addListener("resumed", handleThreadState.bind(null, toolbox));
 
-      threadClient.addListener("progress", (_, {recording}) => {
-        const replayButton = toolbox.doc.getElementById("command-button-stop-replay");
-        if (replayButton) {
-          replayButton.classList.toggle("recording", recording);
-        }
-      });
-
       if (!threadClient.paused) {
         reject(new Error("Thread in wrong state when starting up, should be paused"));
       }
 
       // These flags need to be set here because the client sends them
       // with the `resume` request. We make sure to do this before
       // resuming to avoid another interrupt. We can't pass it in with
       // `threadOptions` because the resume request will override them.
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -222,8 +222,12 @@ toolbox.debugTargetInfo.targetLabel=%1$S
 # inspecting tabs in about:debugging.
 # Currently, we support only this type.
 toolbox.debugTargetInfo.type.tab=tab
 
 # LOCALIZATION NOTE (browserToolbox.statusMessage): This is the label
 # shown next to status details when the Browser Toolbox fails to connect or
 # appears to be taking a while to do so.
 browserToolbox.statusMessage=Browser Toolbox connection status:
+
+# LOCALIZATION NOTE (toolbox.replay.jumpMessage): This is the label
+# shown in the web replay timeline marker
+toolbox.replay.jumpMessage=Jump to message %1$S
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -333,42 +333,40 @@
   stroke: none;
 }
 
 #command-button-responsive.checked::before {
   fill: var(--theme-toolbar-checked-color);
   stroke: var(--theme-toolbar-checked-color);
 }
 
-#command-button-stop-replay::before, #command-button-replay::before {
+#command-button-stop-replay::before,
+#command-button-replay::before {
   background-image: var(--command-replay-image);
   fill: var(--theme-toolbar-photon-icon-color);
   -moz-context-properties: fill;
   background-repeat: no-repeat;
   height: 16px;
   background-size: contain;
 }
 
-
-#command-button-replay, #command-button-stop-replay {
+#command-button-replay,
+#command-button-stop-replay {
   background-color: transparent;
 }
 
-#command-button-replay:hover, #command-button-stop-replay:hover {
+#command-button-replay:hover,
+#command-button-stop-replay:hover {
   background: var(--toolbarbutton-background);
 }
 
 #command-button-stop-replay::before {
   fill: currentColor;
 }
 
-#command-button-stop-replay.recording::before{
-  fill: var(--red-60);
-}
-
 #command-button-scratchpad::before {
   background-image: var(--command-scratchpad-image);
 }
 
 #command-button-eyedropper::before {
   background-image: var(--command-eyedropper-image);
 }
 
@@ -435,106 +433,172 @@
   -moz-user-focus: normal;
 }
 
 /* Toolbox tabs reordering */
 #toolbox-container.tabs-reordering > .theme-body {
   pointer-events: none;
 }
 
-#toolbox-container.tabs-reordering .devtools-tab:not(.selected):hover .devtools-tab-line {
+#toolbox-container.tabs-reordering
+  .devtools-tab:not(.selected):hover
+  .devtools-tab-line {
   background: transparent;
   opacity: 0;
   transition: none;
 }
 
 #toolbox-container.tabs-reordering .devtools-tab.selected {
   background-color: var(--theme-toolbar-hover);
   z-index: 1;
 }
 
-
 /*. webreplay */
 
 .webreplay-player {
   -moz-appearance: none;
   background: var(--theme-tab-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);
   box-sizing: border-box;
   min-height: 29px;
 
-  --pause-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M5 12.503l.052-9a.5.5 0 0 0-1-.006l-.052 9a.5.5 0 0 0 1 .006zM12 12.497l-.05-9A.488.488 0 0 0 11.474 3a.488.488 0 0 0-.473.503l.05 9a.488.488 0 0 0 .477.497.488.488 0 0 0 .473-.503z"/></g></svg>');
-  --play-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M4 12.5l8-5-8-5v10zm-1 0v-10a1 1 0 0 1 1.53-.848l8 5a1 1 0 0 1 0 1.696l-8 5A1 1 0 0 1 3 12.5z" /></g></svg>');
+  --progress-recording-background: #ffebeb;
+  --progress-playing-background: #ebf6ff;
+
+  --recording-marker-background: hsl(14.9, 100%, 47.3%);
+  --recording-marker-background-hover: hsl(14.9, 100%, 37.3%);
+  --command-button-size: 14px;
+  --command-button-primary-size: 20px;
 }
 
 .webreplay-player .overlay-container {
   display: flex;
 }
 
 .webreplay-player .progressBar {
   position: relative;
   width: 100%;
-  height: 6px;
-  background: #DCDCDC;
-  margin: 11px 10px 11px 0;
+  height: 20px;
+  background: #fff;
+  margin: 4px 10px 4px 0;
+  border: 1px solid #bfc9d2;
 }
 
 .webreplay-player .progress {
   position: absolute;
-  width: 0;
+  width: 100%;
   height: 100%;
-  background: #B7B6B6;
+  background: var(--progress-playing-background);
+}
+
+.webreplay-player .recording .progress {
+  background: var(--progress-recording-background);
 }
 
 .webreplay-player .message {
   position: absolute;
   height: 100%;
-  width: 2px;
+  width: 7px;
+  height: 7px;
+  border-radius: 4.5px;
+  top: calc(50% - 3.5px);
   background: var(--blue-50);
 }
 
+.webreplay-player .message.overlayed {
+  border: 1px solid var(--progress-playing-background);
+  top: 5.5px;
+}
+
+.webreplay-player .message.overlayed.future {
+  border-color: #fff;
+}
+
+.webreplay-player .recording .message.overlayed {
+  border-color: var(--progress-recording-background);
+}
+
+.webreplay-player .recording .message {
+  background: var(--recording-marker-background);
+}
+
+.webreplay-player .recording .message:hover {
+  background: var(--recording-marker-background-hover);
+}
+
 .webreplay-player .message:hover {
-  background: var(--blue-40);
+  background: var(--blue-60);
   cursor: pointer;
 }
 
+.webreplay-player .message:hover::before {
+  transform: scale(0.1);
+}
+
 .webreplay-player .commands {
   display: flex;
 }
 
 .webreplay-player .command-button {
   display: flex;
+  min-width: 17px;
+  opacity: 0.7;
 }
 
-.webreplay-player .command-button:hover {
-  background: #efefef;
+.webreplay-player .command-button.primary {
+  min-width: 24px;
 }
 
 .webreplay-player .btn {
-  width: 15px;
-  height: 19px;
-  background: #6A6A6A;
-  mask-size: 15px 19px;
+  width: var(--command-button-size);
+  height: var(--command-button-size);
+  mask-size: var(--command-button-size);
+  background: #6a6a6a;
   align-self: center;
+  margin: 0 auto;
+}
+
+.webreplay-player .primary .btn {
+  width: var(--command-button-primary-size);
+  height: var(--command-button-primary-size);
+  mask-size: var(--command-button-primary-size);
 }
 
 .webreplay-player .command-button.active:hover {
-  background: none;
+  background: #efefef;
+  cursor: pointer;
+}
+
+.webreplay-player .command-button.active {
+  opacity: 1;
 }
 
-.webreplay-player .play-button {
-  mask-image: var(--play-image);
-  margin-right: 5px;
-  margin-left: 2px;
+.webreplay-player div.command-button .rewind {
+  transform: scaleX(-1);
+}
+
+.webreplay-player div.command-button .previous {
+  transform: scaleX(-1);
+  margin-left: 8px;
 }
 
-.webreplay-player .rewind-button {
-  mask-image: var(--play-image);
-  transform: scaleX(-1);
-  margin-left: 5px;
-  margin-right: 2px;
+.webreplay-player div.command-button .next {
+  margin-right: 8px;
 }
 
-.webreplay-player .pause-button {
-  mask-image: var(--pause-image);
-  margin-left: 5px;
-  margin-right: 10px;
+.webreplay-player .progress-line {
+  width: 0%;
+  height: 1px;
+  background: #0074e8;
+  position: absolute;
+  left: 0;
+  right: 10px;
+  top: 50%;
 }
+
+.webreplay-player .progress-line.end {
+  opacity: 0.3;
+}
+
+.webreplay-player .recording .progress-line {
+  background: #d0021b;
+  opacity: 0.3;
+}
--- a/devtools/client/webreplay/components/WebReplayPlayer.js
+++ b/devtools/client/webreplay/components/WebReplayPlayer.js
@@ -1,214 +1,387 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Component } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const getFormatStr = (key, a) => L10N.getFormatStr(`toolbox.replay.${key}`, a);
+
 const { div } = dom;
 
+const markerWidth = 7;
+const imgResource = "resource://devtools/client/debugger/new/images";
+const shouldLog = false;
+
+function classname(name, bools) {
+  for (const key in bools) {
+    if (bools[key]) {
+      name += ` ${key}`;
+    }
+  }
+
+  return name;
+}
+
+function log(message) {
+  if (shouldLog) {
+    console.log(message);
+  }
+}
+
+function CommandButton({ img, className, onClick }) {
+  const images = {
+    rewind: "resume",
+    resume: "resume",
+    next: "next",
+    previous: "next",
+    pause: "pause",
+    play: "resume",
+  };
+
+  return dom.div(
+    {
+      className: `command-button ${className}`,
+      onClick,
+    },
+    dom.img({
+      className: `btn ${img}`,
+      style: {
+        maskImage: `url("${imgResource}/${images[img]}.svg")`,
+      },
+    })
+  );
+}
+
+/*
+ *
+ * The player has 4 valid states
+ * - Paused:       (paused, !recording, !seeking)
+ * - Playing:      (!paused, !recording, !seeking)
+ * - Seeking:      (!paused, !recording, seeking)
+ * - Recording:    (!paused, recording, !seeking)
+ *
+ */
 class WebReplayPlayer extends Component {
   static get propTypes() {
     return {
       toolbox: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
     this.state = {
       executionPoint: null,
       recordingEndpoint: null,
       seeking: false,
       recording: true,
+      paused: false,
       messages: [],
     };
+    this.overlayWidth = 0;
   }
 
   componentDidMount() {
+    this.overlayWidth = this.updateOverlayWidth();
     this.threadClient.addListener("paused", this.onPaused.bind(this));
     this.threadClient.addListener("resumed", this.onResumed.bind(this));
     this.threadClient.addListener("progress", this.onProgress.bind(this));
     this.activeConsole._client.addListener(
       "consoleAPICall",
       this.onMessage.bind(this)
     );
   }
 
+  componentDidUpdate() {
+    this.overlayWidth = this.updateOverlayWidth();
+  }
+
   get threadClient() {
     return this.props.toolbox.threadClient;
   }
 
   get activeConsole() {
     return this.props.toolbox.target.activeConsole;
   }
 
   isRecording() {
-    return this.state.recording;
+    return !this.isPaused() && this.state.recording;
   }
 
   isReplaying() {
-    const {recording} = this.state;
-    return !this.isPaused() && !recording;
+    return !this.isPaused() && !this.state.recording;
   }
 
   isPaused() {
-    const { paused } = this.state;
-    return paused;
+    return this.state.paused;
+  }
+
+  isSeeking() {
+    return this.state.seeking;
   }
 
   onPaused(_, packet) {
     if (packet && packet.recordingEndpoint) {
       const { executionPoint, recordingEndpoint } = packet;
-      this.setState({ executionPoint, recordingEndpoint, paused: true });
+      this.setState({
+        executionPoint,
+        recordingEndpoint,
+        paused: true,
+        seeking: false,
+        recording: false,
+      });
     }
   }
 
   onResumed(_, packet) {
     this.setState({ paused: false });
   }
 
   onProgress(_, packet) {
     const { recording, executionPoint } = packet;
-    this.setState({ recording, executionPoint });
+    log(`progress: ${recording ? "rec" : "play"} ${executionPoint.progress}`);
+
+    if (this.state.seeking) {
+      return;
+    }
+
+    // We want to prevent responding to interrupts
+    if (this.isRecording() && !recording) {
+      return;
+    }
+
+    const newState = { recording, executionPoint };
+    if (recording) {
+      newState.recordingEndpoint = executionPoint;
+    }
+
+    this.setState(newState);
   }
 
   onMessage(_, packet) {
     this.setState({ messages: this.state.messages.concat(packet.message) });
   }
 
   seek(executionPoint) {
     if (!executionPoint) {
       return null;
     }
 
     // set seeking to the current execution point to avoid a progress bar jump
+    this.setState({ seeking: true });
     return this.threadClient.timeWarp(executionPoint);
   }
 
-  next(ev) {
+  next() {
     if (!this.isPaused()) {
       return null;
     }
 
-    if (!ev.metaKey) {
-      return this.threadClient.resume();
-    }
-
     const { messages, executionPoint, recordingEndpoint } = this.state;
     let seekPoint = messages
       .map(m => m.executionPoint)
       .filter(point => point.progress > executionPoint.progress)
       .slice(0)[0];
 
     if (!seekPoint) {
       seekPoint = recordingEndpoint;
     }
 
     return this.seek(seekPoint);
   }
 
-  previous(ev) {
+  async previous() {
+    if (this.isRecording()) {
+      await this.threadClient.interrupt();
+    }
+
     if (!this.isPaused()) {
       return null;
     }
 
-    if (!ev.metaKey) {
-      return this.threadClient.rewind();
-    }
-
     const { messages, executionPoint } = this.state;
 
     const seekPoint = messages
       .map(m => m.executionPoint)
       .filter(point => point.progress < executionPoint.progress)
       .slice(-1)[0];
 
     return this.seek(seekPoint);
   }
 
-  renderCommands() {
+  resume() {
+    if (!this.isPaused()) {
+      return null;
+    }
+
+    return this.threadClient.resume();
+  }
+
+  async rewind() {
     if (this.isRecording()) {
-      return [
-        div(
-          { className: "command-button" },
-          div({
-            className: "pause-button btn",
-            onClick: () => this.threadClient.interrupt(),
-          })
-        ),
-      ];
+      await this.threadClient.interrupt();
     }
 
-    const isActiveClass = !this.isPaused() ? "active" : "";
+    if (!this.isPaused()) {
+      return null;
+    }
+
+    return this.threadClient.rewind();
+  }
+
+  pause() {
+    if (this.isPaused()) {
+      return null;
+    }
+
+    return this.threadClient.interrupt();
+  }
+
+  renderCommands() {
+    const paused = this.isPaused();
+    const recording = this.isRecording();
+    const seeking = this.isSeeking();
 
     return [
-      div(
-        { className: `command-button ${isActiveClass}` },
-        div({
-          className: "rewind-button btn",
-          onClick: ev => this.previous(ev),
-        })
-      ),
-      div(
-        { className: `command-button ${isActiveClass}` },
-        div({
-          className: "play-button btn",
-          onClick: ev => this.next(ev),
-        })
-      ),
+      CommandButton({
+        className: classname("", { active: paused || recording }),
+        img: "previous",
+        onClick: () => this.previous(),
+      }),
+
+      CommandButton({
+        className: classname("", { active: paused || recording }),
+        img: "rewind",
+        onClick: () => this.rewind(),
+      }),
+
+      CommandButton({
+        className: classname(" primary", { active: !paused || seeking }),
+        img: "pause",
+        onClick: () => this.pause(),
+      }),
+
+      CommandButton({
+        className: classname("", { active: paused }),
+        img: "resume",
+        onClick: () => this.resume(),
+      }),
+
+      CommandButton({
+        className: classname("", { active: paused }),
+        img: "next",
+        onClick: () => this.next(),
+      }),
     ];
   }
 
+  updateOverlayWidth() {
+    const el = ReactDOM.findDOMNode(this).querySelector(".progressBar");
+    return el.clientWidth;
+  }
+
+  // calculate pixel distance from two points
+  getDistanceFrom(to, from) {
+    const toPercent = this.getPercent(to);
+    const fromPercent = this.getPercent(from);
+
+    return ((toPercent - fromPercent) * this.overlayWidth) / 100;
+  }
+
+  getOffset(point) {
+    const percent = this.getPercent(point);
+    return (percent * this.overlayWidth) / 100;
+  }
+
+  renderMessage(message, index) {
+    const { messages, executionPoint } = this.state;
+
+    const offset = this.getOffset(message.executionPoint);
+    const previousMessage = messages[index - 1];
+
+    // Check to see if two messages overlay each other on the timeline
+    const isOverlayed =
+      previousMessage &&
+      this.getDistanceFrom(
+        message.executionPoint,
+        previousMessage.executionPoint
+      ) < markerWidth;
+
+    // Check to see if a message appears after the current execution point
+    const isFuture =
+      this.getDistanceFrom(message.executionPoint, executionPoint) >
+      markerWidth / 2;
+
+    return dom.a({
+      className: classname("message", {
+        overlayed: isOverlayed,
+        future: isFuture,
+      }),
+      style: {
+        left: `${offset - markerWidth / 2}px`,
+        zIndex: `${index + 100}`,
+      },
+      title: getFormatStr("jumpMessage", index + 1),
+      onClick: () => this.seek(message.executionPoint),
+    });
+  }
+
   renderMessages() {
     const messages = this.state.messages;
-
-    if (this.isRecording()) {
-      return [];
-    }
-
-    return messages.map((message, index) =>
-      dom.div({
-        className: "message",
-        style: {
-          left: `${this.getPercent(message.executionPoint)}%`,
-        },
-        onClick: () => this.seek(message.executionPoint),
-      })
-    );
+    return messages.map((message, index) => this.renderMessage(message, index));
   }
 
   getPercent(executionPoint) {
-    if (!executionPoint || !this.state.recordingEndpoint) {
+    if (!this.state.recordingEndpoint) {
+      return 100;
+    }
+
+    if (!executionPoint) {
       return 0;
     }
 
     const ratio =
       executionPoint.progress / this.state.recordingEndpoint.progress;
-    return Math.round(ratio * 100);
+    return ratio * 100;
   }
 
   render() {
+    const percent = this.getPercent(this.state.executionPoint);
+    const recording = this.isRecording();
     return div(
       { className: "webreplay-player" },
       div(
-        { id: "overlay", className: "paused" },
+        {
+          id: "overlay",
+          className: classname("", { recording: recording, paused: !recording }),
+        },
         div(
           { className: "overlay-container " },
           div({ className: "commands" }, ...this.renderCommands()),
           div(
             { className: "progressBar" },
             div({
               className: "progress",
-              style: {
-                width: `${this.getPercent(this.state.executionPoint)}%`,
-              },
+              style: { width: `${percent}%` },
+            }),
+            div({
+              className: "progress-line",
+              style: { width: `${percent}%` },
+            }),
+            div({
+              className: "progress-line end",
+              style: { left: `${percent}%`, width: `${100 - percent}%` },
             }),
             ...this.renderMessages()
           )
         )
       )
     );
   }
 }
--- a/gfx/webrender_bindings/WebRenderTypes.h
+++ b/gfx/webrender_bindings/WebRenderTypes.h
@@ -64,20 +64,21 @@ inline DebugFlags NewDebugFlags(uint32_t
   flags.mBits = aFlags;
   return flags;
 }
 
 inline Maybe<wr::ImageFormat>
 SurfaceFormatToImageFormat(gfx::SurfaceFormat aFormat) {
   switch (aFormat) {
     case gfx::SurfaceFormat::R8G8B8X8:
-    case gfx::SurfaceFormat::R8G8B8A8:
-      // WebRender not support RGBA8 and RGBX8. Assert here.
+      // WebRender not support RGBX8. Assert here.
       MOZ_ASSERT(false);
       return Nothing();
+    case gfx::SurfaceFormat::R8G8B8A8:
+      return Some(wr::ImageFormat::RGBA8);
     case gfx::SurfaceFormat::B8G8R8X8:
       // TODO: WebRender will have a BGRA + opaque flag for this but does not
       // have it yet (cf. issue #732).
     case gfx::SurfaceFormat::B8G8R8A8:
       return Some(wr::ImageFormat::BGRA8);
     case gfx::SurfaceFormat::A8:
       return Some(wr::ImageFormat::R8);
     case gfx::SurfaceFormat::A16:
--- a/js/public/CharacterEncoding.h
+++ b/js/public/CharacterEncoding.h
@@ -86,16 +86,36 @@ class UTF8Chars : public mozilla::Range<
       : UTF8Chars(reinterpret_cast<char*>(aUnits), aLength)
     {}
     UTF8Chars(const mozilla::Utf8Unit* aUnits, size_t aLength)
       : UTF8Chars(reinterpret_cast<const char*>(aUnits), aLength)
     {}
 };
 
 /*
+ * Similar to UTF8Chars, but contains WTF-8.
+ * https://simonsapin.github.io/wtf-8/
+ */
+class WTF8Chars : public mozilla::Range<unsigned char>
+{
+    typedef mozilla::Range<unsigned char> Base;
+
+  public:
+    using CharT = unsigned char;
+
+    WTF8Chars() : Base() {}
+    WTF8Chars(char* aBytes, size_t aLength)
+      : Base(reinterpret_cast<unsigned char*>(aBytes), aLength)
+    {}
+    WTF8Chars(const char* aBytes, size_t aLength)
+      : Base(reinterpret_cast<unsigned char*>(const_cast<char*>(aBytes)), aLength)
+    {}
+};
+
+/*
  * SpiderMonkey also deals directly with UTF-8 encoded text in some places.
  */
 class UTF8CharsZ : public mozilla::RangedPtr<unsigned char>
 {
     typedef mozilla::RangedPtr<unsigned char> Base;
 
   public:
     using CharT = unsigned char;
@@ -250,16 +270,22 @@ Utf8ToOneUcs4Char(const uint8_t* utf8Buf
  * - On error, returns an empty TwoByteCharsZ.
  * - On success, returns a malloc'd TwoByteCharsZ, and updates |outlen| to hold
  *   its length;  the length value excludes the trailing null.
  */
 extern JS_PUBLIC_API TwoByteCharsZ
 UTF8CharsToNewTwoByteCharsZ(JSContext* cx, const UTF8Chars utf8, size_t* outlen);
 
 /*
+ * Like UTF8CharsToNewTwoByteCharsZ, but for WTF8Chars.
+ */
+extern JS_PUBLIC_API TwoByteCharsZ
+WTF8CharsToNewTwoByteCharsZ(JSContext* cx, const WTF8Chars wtf8, size_t* outlen);
+
+/*
  * Like UTF8CharsToNewTwoByteCharsZ, but for ConstUTF8CharsZ.
  */
 extern JS_PUBLIC_API TwoByteCharsZ
 UTF8CharsToNewTwoByteCharsZ(JSContext* cx, const ConstUTF8CharsZ& utf8, size_t* outlen);
 
 /*
  * The same as UTF8CharsToNewTwoByteCharsZ(), except that any malformed UTF-8 characters
  * will be replaced by \uFFFD. No exception will be thrown for malformed UTF-8
--- a/js/src/NamespaceImports.h
+++ b/js/src/NamespaceImports.h
@@ -23,16 +23,17 @@
 namespace JS {
 
 class Latin1Chars;
 class Latin1CharsZ;
 class ConstTwoByteChars;
 class TwoByteChars;
 class TwoByteCharsZ;
 class UTF8Chars;
+class WTF8Chars;
 class UTF8CharsZ;
 
 using AutoValueVector = AutoVector<Value>;
 using AutoIdVector = AutoVector<jsid>;
 using AutoObjectVector = AutoVector<JSObject*>;
 
 using ValueVector = JS::GCVector<JS::Value>;
 using IdVector = JS::GCVector<jsid>;
@@ -70,16 +71,17 @@ using JS::UndefinedValue;
 
 using JS::Latin1Char;
 using JS::Latin1Chars;
 using JS::Latin1CharsZ;
 using JS::ConstTwoByteChars;
 using JS::TwoByteChars;
 using JS::TwoByteCharsZ;
 using JS::UTF8Chars;
+using JS::WTF8Chars;
 using JS::UTF8CharsZ;
 using JS::UniqueChars;
 using JS::UniqueTwoByteChars;
 
 using JS::Result;
 using JS::Ok;
 using JS::OOM;
 
--- a/js/src/frontend/BinTokenReaderMultipart.cpp
+++ b/js/src/frontend/BinTokenReaderMultipart.cpp
@@ -169,17 +169,17 @@ BinTokenReaderMultipart::readHeader()
         if (current_ + byteLen > stop_ || current_ + byteLen < current_) {
             return raiseError("Invalid byte length in individual string");
         }
 
         // Check null string.
         if (byteLen == 2 && *current_ == 255 && *(current_ + 1) == 0) {
             atom = nullptr;
         } else {
-            BINJS_TRY_VAR(atom, AtomizeUTF8Chars(cx_, (const char*)current_, byteLen));
+            BINJS_TRY_VAR(atom, AtomizeWTF8Chars(cx_, (const char*)current_, byteLen));
         }
 
         metadata->getAtom(i) = atom;
 
         // Populate `slicesTable_`: i => slice
         new (&metadata->getSlice(i)) Chars((const char*)current_, byteLen);
 
         current_ += byteLen;
--- a/js/src/vm/CharacterEncoding.cpp
+++ b/js/src/vm/CharacterEncoding.cpp
@@ -204,23 +204,28 @@ JS::CharsToNewUTF8CharsZ(JSContext* mayb
 
 template UTF8CharsZ
 JS::CharsToNewUTF8CharsZ(JSContext* maybeCx,
                          const mozilla::Range<const char16_t> chars);
 
 static const uint32_t INVALID_UTF8 = UINT32_MAX;
 
 /*
- * Convert a utf8 character sequence into a UCS-4 character and return that
- * character.  It is assumed that the caller already checked that the sequence
- * is valid.
+ * Convert a UTF-8 or WTF-8 (depending on InputCharsT, which is either
+ * UTF8Chars or WTF8Chars) character sequence into a UCS-4 character and return
+ * that character.  It is assumed that the caller already checked that the
+ * sequence is valid.
  */
-uint32_t
-JS::Utf8ToOneUcs4Char(const uint8_t* utf8Buffer, int utf8Length)
+template <class InputCharsT>
+static uint32_t
+Utf8ToOneUcs4CharImpl(const uint8_t* utf8Buffer, int utf8Length)
 {
+    static_assert(std::is_same<InputCharsT, UTF8Chars>::value ||
+                  std::is_same<InputCharsT, WTF8Chars>::value,
+                  "must be either UTF-8 or WTF-8");
     MOZ_ASSERT(1 <= utf8Length && utf8Length <= 4);
 
     if (utf8Length == 1) {
         MOZ_ASSERT(!(*utf8Buffer & 0x80));
         return *utf8Buffer;
     }
 
     /* from Unicode 3.1, non-shortest form is illegal */
@@ -230,23 +235,36 @@ JS::Utf8ToOneUcs4Char(const uint8_t* utf
                (0x100 - (1 << (8 - utf8Length))));
     uint32_t ucs4Char = *utf8Buffer++ & ((1 << (7 - utf8Length)) - 1);
     uint32_t minucs4Char = minucs4Table[utf8Length - 2];
     while (--utf8Length) {
         MOZ_ASSERT((*utf8Buffer & 0xC0) == 0x80);
         ucs4Char = (ucs4Char << 6) | (*utf8Buffer++ & 0x3F);
     }
 
-    if (MOZ_UNLIKELY(ucs4Char < minucs4Char || (ucs4Char >= 0xD800 && ucs4Char <= 0xDFFF))) {
+    if (MOZ_UNLIKELY(ucs4Char < minucs4Char)) {
+        return INVALID_UTF8;
+    }
+
+    // WTF-8 allows lone surrogate.
+    if (std::is_same<InputCharsT, UTF8Chars>::value &&
+        MOZ_UNLIKELY(ucs4Char >= 0xD800 && ucs4Char <= 0xDFFF))
+    {
         return INVALID_UTF8;
     }
 
     return ucs4Char;
 }
 
+uint32_t
+JS::Utf8ToOneUcs4Char(const uint8_t* utf8Buffer, int utf8Length)
+{
+    return Utf8ToOneUcs4CharImpl<UTF8Chars>(utf8Buffer, utf8Length);
+}
+
 static void
 ReportInvalidCharacter(JSContext* cx, uint32_t offset)
 {
     char buffer[10];
     SprintfLiteral(buffer, "%u", offset);
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_MALFORMED_UTF8_CHAR, buffer);
 }
 
@@ -271,23 +289,23 @@ enum class LoopDisposition {
 
 enum class OnUTF8Error {
     InsertReplacementCharacter,
     InsertQuestionMark,
     Throw,
     Crash,
 };
 
-// Scan UTF8 input and (internally, at least) convert it to a series of UTF-16
-// code units. But you can also do odd things like pass an empty lambda for
-// `dst`, in which case the output is discarded entirely--the only effect of
-// calling the template that way is error-checking.
-template <OnUTF8Error ErrorAction, typename OutputFn>
+// Scan UTF-8 or WTF-8 input and (internally, at least) convert it to a series
+// of UTF-16 code units. But you can also do odd things like pass an empty
+// lambda for `dst`, in which case the output is discarded entirely--the only
+// effect of calling the template that way is error-checking.
+template <OnUTF8Error ErrorAction, typename OutputFn, class InputCharsT>
 static bool
-InflateUTF8ToUTF16(JSContext* cx, const UTF8Chars src, OutputFn dst)
+InflateUTF8ToUTF16(JSContext* cx, const InputCharsT src, OutputFn dst)
 {
     size_t srclen = src.length();
     for (uint32_t i = 0; i < srclen; i++) {
         uint32_t v = uint32_t(src[i]);
         if (!(v & 0x80)) {
             // ASCII code unit.  Simple copy.
             if (dst(uint16_t(v)) == LoopDisposition::Break) {
                 break;
@@ -334,28 +352,36 @@ InflateUTF8ToUTF16(JSContext* cx, const 
 
             // Check the second byte.  From Unicode Standard v6.2, Table 3-7
             // Well-Formed UTF-8 Byte Sequences.
             if ((v == 0xE0 && ((uint8_t)src[i + 1] & 0xE0) != 0xA0) ||  // E0 A0~BF
                 (v == 0xED && ((uint8_t)src[i + 1] & 0xE0) != 0x80) ||  // ED 80~9F
                 (v == 0xF0 && ((uint8_t)src[i + 1] & 0xF0) == 0x80) ||  // F0 90~BF
                 (v == 0xF4 && ((uint8_t)src[i + 1] & 0xF0) != 0x80))    // F4 80~8F
             {
-                INVALID(ReportInvalidCharacter, i, 1);
+                if (std::is_same<InputCharsT, UTF8Chars>::value) {
+                    INVALID(ReportInvalidCharacter, i, 1);
+                } else {
+                    // WTF-8 allows lone surrogate as ED A0~BF 80~BF.
+                    MOZ_ASSERT((std::is_same<InputCharsT, WTF8Chars>::value));
+                    if (v == 0xED && ((uint8_t)src[i + 1] & 0xE0) != 0xA0) { // ED A0~BF
+                        INVALID(ReportInvalidCharacter, i, 1);
+                    }
+                }
             }
 
             // Check the continuation bytes.
             for (uint32_t m = 1; m < n; m++) {
                 if ((src[i + m] & 0xC0) != 0x80) {
                     INVALID(ReportInvalidCharacter, i, m);
                 }
             }
 
             // Determine the code unit's length in CharT and act accordingly.
-            v = JS::Utf8ToOneUcs4Char((uint8_t*)&src[i], n);
+            v = Utf8ToOneUcs4CharImpl<InputCharsT>((uint8_t*)&src[i], n);
             if (v < 0x10000) {
                 // The n-byte UTF8 code unit will fit in a single CharT.
                 if (dst(char16_t(v)) == LoopDisposition::Break) {
                     break;
                 }
             } else {
                 v -= 0x10000;
                 if (v <= 0xFFFFF) {
@@ -378,19 +404,20 @@ InflateUTF8ToUTF16(JSContext* cx, const 
             // code unit.
             i += n - 1;
         }
     }
 
     return true;
 }
 
-template <OnUTF8Error ErrorAction, typename CharT>
+template <OnUTF8Error ErrorAction, typename CharT, class InputCharsT>
 static void
-CopyAndInflateUTF8IntoBuffer(JSContext* cx, const UTF8Chars src, CharT *dst, size_t outlen, bool allASCII)
+CopyAndInflateUTF8IntoBuffer(JSContext* cx, const InputCharsT src, CharT* dst, size_t outlen,
+                             bool allASCII)
 {
     if (allASCII) {
         size_t srclen = src.length();
         MOZ_ASSERT(outlen == srclen);
         for (uint32_t i = 0; i < srclen; i++) {
             dst[i] = CharT(src[i]);
         }
     } else {
@@ -400,19 +427,19 @@ CopyAndInflateUTF8IntoBuffer(JSContext* 
             return LoopDisposition::Continue;
         };
         MOZ_ALWAYS_TRUE((InflateUTF8ToUTF16<ErrorAction>(cx, src, push)));
         MOZ_ASSERT(j == outlen);
     }
     dst[outlen] = CharT('\0');    // NUL char
 }
 
-template <OnUTF8Error ErrorAction, typename CharsT>
+template <OnUTF8Error ErrorAction, typename CharsT, class InputCharsT>
 static CharsT
-InflateUTF8StringHelper(JSContext* cx, const UTF8Chars src, size_t* outlen)
+InflateUTF8StringHelper(JSContext* cx, const InputCharsT src, size_t* outlen)
 {
     using CharT = typename CharsT::CharT;
     static_assert(std::is_same<CharT, char16_t>::value ||
                   std::is_same<CharT, Latin1Char>::value,
                   "bad CharT");
 
     *outlen = 0;
 
@@ -444,16 +471,22 @@ InflateUTF8StringHelper(JSContext* cx, c
 
 TwoByteCharsZ
 JS::UTF8CharsToNewTwoByteCharsZ(JSContext* cx, const UTF8Chars utf8, size_t* outlen)
 {
     return InflateUTF8StringHelper<OnUTF8Error::Throw, TwoByteCharsZ>(cx, utf8, outlen);
 }
 
 TwoByteCharsZ
+JS::WTF8CharsToNewTwoByteCharsZ(JSContext* cx, const WTF8Chars wtf8, size_t* outlen)
+{
+    return InflateUTF8StringHelper<OnUTF8Error::Throw, TwoByteCharsZ>(cx, wtf8, outlen);
+}
+
+TwoByteCharsZ
 JS::UTF8CharsToNewTwoByteCharsZ(JSContext* cx, const ConstUTF8CharsZ& utf8, size_t* outlen)
 {
     UTF8Chars chars(utf8.c_str(), strlen(utf8.c_str()));
     return InflateUTF8StringHelper<OnUTF8Error::Throw, TwoByteCharsZ>(cx, chars, outlen);
 }
 
 TwoByteCharsZ
 JS::LossyUTF8CharsToNewTwoByteCharsZ(JSContext* cx, const JS::UTF8Chars utf8, size_t* outlen)
@@ -513,18 +546,19 @@ JS::LossyUTF8CharsToNewLatin1CharsZ(JSCo
 
 /**
  * Atomization Helpers.
  *
  * These functions are extremely single-use, and are not intended for general
  * consumption.
  */
 
+template <class InputCharsT>
 bool
-GetUTF8AtomizationData(JSContext* cx, const JS::UTF8Chars utf8, size_t* outlen,
+GetUTF8AtomizationData(JSContext* cx, const InputCharsT utf8, size_t* outlen,
                        JS::SmallestEncoding* encoding, HashNumber* hashNum)
 {
     *outlen = 0;
     *encoding = JS::SmallestEncoding::ASCII;
     *hashNum = 0;
 
     auto getMetadata = [outlen, encoding, hashNum](char16_t c) -> LoopDisposition {
         (*outlen)++;
@@ -534,16 +568,25 @@ GetUTF8AtomizationData(JSContext* cx, co
     };
     if (!InflateUTF8ToUTF16<OnUTF8Error::Throw>(cx, utf8, getMetadata)) {
         return false;
     }
 
     return true;
 }
 
+template
+bool
+GetUTF8AtomizationData<JS::UTF8Chars>(JSContext* cx, const JS::UTF8Chars utf8, size_t* outlen,
+                                      JS::SmallestEncoding* encoding, HashNumber* hashNum);
+template
+bool
+GetUTF8AtomizationData<JS::WTF8Chars>(JSContext* cx, const JS::WTF8Chars utf8, size_t* outlen,
+                                      JS::SmallestEncoding* encoding, HashNumber* hashNum);
+
 template <typename CharT>
 bool
 UTF8EqualsChars(const JS::UTF8Chars utfChars, const CharT* chars)
 {
     size_t ind = 0;
     bool isEqual = true;
 
     auto checkEqual = [&isEqual, &ind, chars](char16_t c) -> LoopDisposition {
@@ -570,31 +613,37 @@ UTF8EqualsChars(const JS::UTF8Chars utfC
     InflateUTF8ToUTF16<OnUTF8Error::Crash>(/* cx = */ nullptr, utfChars, checkEqual);
 
     return isEqual;
 }
 
 template bool UTF8EqualsChars<char16_t>(const JS::UTF8Chars, const char16_t*);
 template bool UTF8EqualsChars<JS::Latin1Char>(const JS::UTF8Chars, const JS::Latin1Char*);
 
-template <typename CharT>
+template <typename CharT, class InputCharsT>
 void
-InflateUTF8CharsToBufferAndTerminate(const UTF8Chars src, CharT* dst, size_t dstLen,
+InflateUTF8CharsToBufferAndTerminate(const InputCharsT src, CharT* dst, size_t dstLen,
                                      JS::SmallestEncoding encoding)
 {
     CopyAndInflateUTF8IntoBuffer<OnUTF8Error::Crash>(/* cx = */ nullptr, src, dst, dstLen,
                                                      encoding == JS::SmallestEncoding::ASCII);
 }
 
 template void
 InflateUTF8CharsToBufferAndTerminate<char16_t>(const UTF8Chars src, char16_t* dst, size_t dstLen,
                                                JS::SmallestEncoding encoding);
 template void
 InflateUTF8CharsToBufferAndTerminate<JS::Latin1Char>(const UTF8Chars src, JS::Latin1Char* dst,
                                                      size_t dstLen, JS::SmallestEncoding encoding);
+template void
+InflateUTF8CharsToBufferAndTerminate<char16_t>(const WTF8Chars src, char16_t* dst, size_t dstLen,
+                                               JS::SmallestEncoding encoding);
+template void
+InflateUTF8CharsToBufferAndTerminate<JS::Latin1Char>(const WTF8Chars src, JS::Latin1Char* dst,
+                                                     size_t dstLen, JS::SmallestEncoding encoding);
 
 #ifdef DEBUG
 void
 JS::ConstUTF8CharsZ::validate(size_t aLength)
 {
     MOZ_ASSERT(data_);
     UTF8Chars chars(data_, aLength);
     auto nop = [](char16_t) -> LoopDisposition { return LoopDisposition::Continue; };
--- a/js/src/vm/JSAtom.cpp
+++ b/js/src/vm/JSAtom.cpp
@@ -37,26 +37,27 @@
 using namespace js;
 
 using mozilla::ArrayEnd;
 using mozilla::ArrayLength;
 using mozilla::Maybe;
 using mozilla::Nothing;
 using mozilla::RangedPtr;
 
-template <typename CharT>
-extern void InflateUTF8CharsToBufferAndTerminate(const UTF8Chars src, CharT* dst, size_t dstLen,
+template <typename CharT, typename InputCharsT>
+extern void InflateUTF8CharsToBufferAndTerminate(const InputCharsT src, CharT* dst, size_t dstLen,
                                                  JS::SmallestEncoding encoding);
 
 template <typename CharT>
 extern bool UTF8EqualsChars(const JS::UTF8Chars utf8, const CharT* chars);
 
+template <typename InputCharsT>
 extern bool
-GetUTF8AtomizationData(JSContext* cx, const JS::UTF8Chars utf8, size_t* outlen, JS::SmallestEncoding* encoding,
-                       HashNumber* hashNum);
+GetUTF8AtomizationData(JSContext* cx, const InputCharsT utf8, size_t* outlen,
+                       JS::SmallestEncoding* encoding, HashNumber* hashNum);
 
 struct js::AtomHasher::Lookup
 {
     union {
         const JS::Latin1Char* latin1Chars;
         const char16_t* twoByteChars;
         const char* utf8Bytes;
     };
@@ -861,38 +862,41 @@ PermanentlyAtomizeAndCopyChars(JSContext
     {
         ReportOutOfMemory(cx);
         return nullptr;
     }
 
     return atom;
 }
 
-struct AtomizeUTF8CharsWrapper
+template <typename CharsT>
+struct AtomizeUTF8OrWTF8CharsWrapper
 {
-    JS::UTF8Chars utf8;
+    CharsT utf8;
     JS::SmallestEncoding encoding;
 
-    AtomizeUTF8CharsWrapper(const JS::UTF8Chars& chars, JS::SmallestEncoding minEncode)
+    AtomizeUTF8OrWTF8CharsWrapper(const CharsT& chars, JS::SmallestEncoding minEncode)
       : utf8(chars), encoding(minEncode)
     { }
 };
 
+// MakeFlatStringForAtomization has 4 variants.
+// This is used by Latin1Char and char16_t.
 template <typename CharT>
 MOZ_ALWAYS_INLINE
 static JSFlatString*
 MakeFlatStringForAtomization(JSContext* cx, const CharT* tbchars, size_t length)
 {
     return NewStringCopyN<NoGC>(cx, tbchars, length);
 }
 
-template<typename CharT>
+template<typename CharT, typename WrapperT>
 MOZ_ALWAYS_INLINE
 static JSFlatString*
-MakeUTF8AtomHelper(JSContext* cx, const AtomizeUTF8CharsWrapper* chars, size_t length)
+MakeUTF8AtomHelper(JSContext* cx, const WrapperT* chars, size_t length)
 {
     if (JSInlineString::lengthFits<CharT>(length)) {
         CharT* storage;
         JSInlineString* str = AllocateInlineString<NoGC>(cx, length, &storage);
         if (!str) {
             return nullptr;
         }
 
@@ -915,20 +919,24 @@ MakeUTF8AtomHelper(JSContext* cx, const 
     if (!str) {
         return nullptr;
     }
 
     mozilla::Unused << newStr.release();
     return str;
 }
 
-template<>
+// Another 2 variants of MakeFlatStringForAtomization.
+// This is used by AtomizeUTF8OrWTF8CharsWrapper with UTF8Chars or WTF8Chars.
+template<typename InputCharsT>
 MOZ_ALWAYS_INLINE
 /* static */ JSFlatString*
-MakeFlatStringForAtomization(JSContext* cx, const AtomizeUTF8CharsWrapper* chars, size_t length)
+MakeFlatStringForAtomization(JSContext* cx,
+                             const AtomizeUTF8OrWTF8CharsWrapper<InputCharsT>* chars,
+                             size_t length)
 {
     if (length == 0) {
         return cx->emptyString();
     }
 
     if (chars->encoding == JS::SmallestEncoding::UTF16) {
         return MakeUTF8AtomHelper<char16_t>(cx, chars, length);
     }
@@ -1036,37 +1044,50 @@ js::AtomizeChars(JSContext* cx, const Ch
 }
 
 template JSAtom*
 js::AtomizeChars(JSContext* cx, const Latin1Char* chars, size_t length, PinningBehavior pin);
 
 template JSAtom*
 js::AtomizeChars(JSContext* cx, const char16_t* chars, size_t length, PinningBehavior pin);
 
+template <typename CharsT>
 JSAtom*
-js::AtomizeUTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength)
+AtomizeUTF8OrWTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength)
 {
     // Since the static strings are all ascii, we can check them before trying anything else.
     if (JSAtom* s = cx->staticStrings().lookup(utf8Chars, utf8ByteLength)) {
         return s;
     }
 
     size_t length;
     HashNumber hash;
     JS::SmallestEncoding forCopy;
-    UTF8Chars utf8(utf8Chars, utf8ByteLength);
+    CharsT utf8(utf8Chars, utf8ByteLength);
     if (!GetUTF8AtomizationData(cx, utf8, &length, &forCopy, &hash)) {
         return nullptr;
     }
 
-    AtomizeUTF8CharsWrapper chars(utf8, forCopy);
+    AtomizeUTF8OrWTF8CharsWrapper<CharsT> chars(utf8, forCopy);
     AtomHasher::Lookup lookup(utf8Chars, utf8ByteLength, length, hash);
     return AtomizeAndCopyCharsFromLookup(cx, &chars, length, lookup, DoNotPinAtom, Nothing());
 }
 
+JSAtom*
+js::AtomizeUTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength)
+{
+    return AtomizeUTF8OrWTF8Chars<UTF8Chars>(cx, utf8Chars, utf8ByteLength);
+}
+
+JSAtom*
+js::AtomizeWTF8Chars(JSContext* cx, const char* wtf8Chars, size_t wtf8ByteLength)
+{
+    return AtomizeUTF8OrWTF8Chars<WTF8Chars>(cx, wtf8Chars, wtf8ByteLength);
+}
+
 bool
 js::IndexToIdSlow(JSContext* cx, uint32_t index, MutableHandleId idp)
 {
     MOZ_ASSERT(index > JSID_INT_MAX);
 
     char16_t buf[UINT32_CHAR_BUFFER_LENGTH];
     RangedPtr<char16_t> end(ArrayEnd(buf), buf, ArrayEnd(buf));
     RangedPtr<char16_t> start = BackfillIndexInCharBuffer(index, end);
--- a/js/src/vm/JSAtom.h
+++ b/js/src/vm/JSAtom.h
@@ -65,16 +65,19 @@ template <typename CharT>
 extern JSAtom*
 AtomizeChars(JSContext* cx, const CharT* chars, size_t length,
              js::PinningBehavior pin = js::DoNotPinAtom);
 
 extern JSAtom*
 AtomizeUTF8Chars(JSContext* cx, const char* utf8Chars, size_t utf8ByteLength);
 
 extern JSAtom*
+AtomizeWTF8Chars(JSContext* cx, const char* wtf8Chars, size_t wtf8ByteLength);
+
+extern JSAtom*
 AtomizeString(JSContext* cx, JSString* str, js::PinningBehavior pin = js::DoNotPinAtom);
 
 template <AllowGC allowGC>
 extern JSAtom*
 ToAtom(JSContext* cx, typename MaybeRooted<JS::Value, allowGC>::HandleType v);
 
 // These functions are declared in vm/Xdr.h
 //
--- a/layout/generic/crashtests/crashtests.list
+++ b/layout/generic/crashtests/crashtests.list
@@ -110,17 +110,17 @@ load 380012-1.html
 load 381152-1.html
 load 381786-1.html
 load 382129-1.xhtml
 load 382131-1.html
 load 382199-1.html
 load 382208-1.xhtml
 load 382262-1.html
 load 382396-1.xhtml
-asserts(1) load 382745-1.xhtml # Bug 758695
+load 382745-1.xhtml # Bug 758695
 load 383089-1.html
 load 385265-1.xhtml
 load 385295-1.xhtml
 load 385344-1.html
 load 385344-2.html
 load 385414-1.html
 load 385414-2.html
 load 385426-1.html
--- a/toolkit/content/widgets/tree.js
+++ b/toolkit/content/widgets/tree.js
@@ -405,9 +405,35 @@
       // prevent click event from firing after column drag and drop
       aEvent.stopPropagation();
       aEvent.preventDefault();
     }
   }
 
   customElements.define("treecol", MozTreecol);
 
+  class MozTreecols extends MozElements.BaseControl {
+    connectedCallback() {
+      if (this.delayConnectedCallback()) {
+        return;
+      }
+
+      if (!this.querySelector("treecolpicker")) {
+        this.appendChild(MozXULElement.parseXULToFragment(`
+          <treecolpicker class="treecol-image" fixed="true"></treecolpicker>
+        `));
+      }
+
+      let treecolpicker = this.querySelector("treecolpicker");
+      this.inheritAttribute(treecolpicker, "tooltiptext=pickertooltiptext");
+
+      // Set resizeafter="farthest" on the splitters if nothing else has been
+      // specified.
+      Array.forEach(this.getElementsByTagName("splitter"), function(splitter) {
+        if (!splitter.hasAttribute("resizeafter"))
+          splitter.setAttribute("resizeafter", "farthest");
+      });
+    }
+  }
+
+  customElements.define("treecols", MozTreecols);
+
 }
--- a/toolkit/content/widgets/tree.xml
+++ b/toolkit/content/widgets/tree.xml
@@ -879,35 +879,16 @@
            }
            event.preventDefault();
          }
          ]]>
       </handler>
     </handlers>
   </binding>
 
-  <binding id="treecols">
-    <content orient="horizontal">
-      <xul:hbox class="tree-scrollable-columns" flex="1">
-        <children includes="treecol|splitter"/>
-      </xul:hbox>
-      <xul:treecolpicker class="treecol-image" fixed="true" xbl:inherits="tooltiptext=pickertooltiptext"/>
-    </content>
-    <implementation>
-      <constructor><![CDATA[
-        // Set resizeafter="farthest" on the splitters if nothing else has been
-        // specified.
-        Array.forEach(this.getElementsByTagName("splitter"), function(splitter) {
-          if (!splitter.hasAttribute("resizeafter"))
-            splitter.setAttribute("resizeafter", "farthest");
-        });
-      ]]></constructor>
-    </implementation>
-  </binding>
-
   <binding id="treerows" extends="chrome://global/content/bindings/general.xml#basecontrol">
     <content>
       <xul:hbox flex="1" class="tree-bodybox">
         <children/>
       </xul:hbox>
       <xul:scrollbar height="0" minwidth="0" minheight="0" orient="vertical" xbl:inherits="collapsed=hidevscroll" style="position:relative; z-index:2147483647;"
         oncontextmenu="event.stopPropagation(); event.preventDefault();"
         onclick="event.stopPropagation(); event.preventDefault();"
@@ -1003,16 +984,17 @@
             this.buildPopup(popup);
             popup.openPopup(this, "after_end");
           } else {
             var tree = this.parentNode.parentNode;
             tree.stopEditing(true);
             var menuitem = document.getAnonymousElementByAttribute(this, "anonid", "menuitem");
             if (event.originalTarget == menuitem) {
               tree.columns.restoreNaturalOrder();
+              this.removeAttribute("ordinal");
               tree._ensureColumnOrder();
             } else {
               var colindex = event.originalTarget.getAttribute("colindex");
               var column = tree.columns[colindex];
               if (column) {
                 var element = column.element;
                 if (element.getAttribute("hidden") == "true")
                   element.setAttribute("hidden", "false");
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -433,21 +433,17 @@ column {
 }
 
 /******** tree ******/
 
 tree {
   -moz-binding: url("chrome://global/content/bindings/tree.xml#tree");
 }
 
-treecols {
-  -moz-binding: url("chrome://global/content/bindings/tree.xml#treecols");
-}
-
-treecol {
+treecolpicker {
   -moz-box-ordinal-group: 2147483646;
 }
 
 tree > treechildren {
   display: -moz-box;
   -moz-user-select: none;
   -moz-box-flex: 1;
 }
@@ -476,21 +472,16 @@ treecol {
   min-width: 16px;
 }
 
 treecol[hidden="true"] {
   visibility: collapse;
   display: -moz-box;
 }
 
-.tree-scrollable-columns {
-  /* Yes, Virginia, this makes it scrollable */
-  overflow: hidden;
-}
-
 /* ::::: lines connecting cells ::::: */
 tree:not([treelines="true"]) > treechildren::-moz-tree-line {
   visibility: hidden;
 }
 
 treechildren::-moz-tree-cell(ltr) {
   direction: ltr !important;
 }