Bug 1522138 - [release 120] [performance] Memoize breakpoints and URL parsing (#7774). r=dwalsh
authorJason Laster <jason.laster.11@gmail.com>
Thu, 24 Jan 2019 15:24:23 -0500
changeset 515390 c4c105118de1ad525241df78704b8c5e04444599
parent 515389 38d9045b04e6ac6debe87dcfa42322a24359eebd
child 515391 c2913cb9cb20bf4852b2cb5e1e3d5bf29421ba29
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdwalsh
bugs1522138
milestone66.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
Bug 1522138 - [release 120] [performance] Memoize breakpoints and URL parsing (#7774). r=dwalsh
devtools/client/debugger/new/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
devtools/client/debugger/new/src/utils/sources-tree/getURL.js
devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap
devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap
devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/sortTree.spec.js.snap
devtools/client/debugger/new/src/utils/sources-tree/tests/addToTree.spec.js
devtools/client/debugger/new/src/utils/sources-tree/tests/collapseTree.spec.js
devtools/client/debugger/new/src/utils/sources-tree/tests/getUrl.spec.js
devtools/client/debugger/new/src/utils/sources-tree/tests/sortTree.spec.js
devtools/client/debugger/new/src/utils/sources-tree/tests/utils.spec.js
devtools/client/debugger/new/src/utils/url.js
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
@@ -4,16 +4,17 @@
 
 // @flow
 
 import React, { PureComponent } from "react";
 import { connect } from "../../../utils/connect";
 import { createSelector } from "reselect";
 import classnames from "classnames";
 import actions from "../../../actions";
+import { memoize } from "lodash";
 
 import showContextMenu from "./BreakpointsContextMenu";
 import { CloseButton } from "../../shared/Button";
 
 import {
   getLocationWithoutColumn,
   getSelectedText
 } from "../../../utils/breakpoint";
@@ -119,32 +120,35 @@ class Breakpoint extends PureComponent<P
     return bpLocation;
   }
 
   getBreakpointText() {
     const { breakpoint, selectedSource } = this.props;
     return breakpoint.condition || getSelectedText(breakpoint, selectedSource);
   }
 
-  highlightText() {
-    const text = this.getBreakpointText() || "";
-    const editor = getEditor();
+  highlightText = memoize(
+    (text = "", editor) => {
+      if (!editor.CodeMirror) {
+        return { __html: text };
+      }
 
-    if (!editor.CodeMirror) {
-      return { __html: text };
-    }
-
-    const node = document.createElement("div");
-    editor.CodeMirror.runMode(text, "application/javascript", node);
-    return { __html: node.innerHTML };
-  }
+      const node = document.createElement("div");
+      editor.CodeMirror.runMode(text, "application/javascript", node);
+      return { __html: node.innerHTML };
+    },
+    (text, editor) => `${text} - ${editor ? "editor" : ""}`
+  );
 
   /* eslint-disable react/no-danger */
   render() {
     const { breakpoint } = this.props;
+    const text = this.getBreakpointText();
+    const editor = getEditor();
+
     return (
       <div
         className={classnames({
           breakpoint,
           paused: this.isCurrentlyPausedAtBreakpoint(),
           disabled: breakpoint.disabled,
           "is-conditional": !!breakpoint.condition,
           log: breakpoint.log
@@ -160,19 +164,19 @@ class Breakpoint extends PureComponent<P
           checked={!breakpoint.disabled}
           onChange={this.handleBreakpointCheckbox}
           onClick={ev => ev.stopPropagation()}
         />
         <label
           htmlFor={breakpoint.id}
           className="breakpoint-label cm-s-mozilla"
           onClick={this.selectBreakpoint}
-          title={this.getBreakpointText()}
+          title={text}
         >
-          <span dangerouslySetInnerHTML={this.highlightText()} />
+          <span dangerouslySetInnerHTML={this.highlightText(text, editor)} />
         </label>
         <div className="breakpoint-line-close">
           <div className="breakpoint-line">{this.getBreakpointLocation()}</div>
           <CloseButton
             handleClick={e => this.removeBreakpoint(e)}
             tooltip={L10N.getStr("breakpoints.removeBreakpointTooltip")}
           />
         </div>
--- a/devtools/client/debugger/new/src/utils/sources-tree/getURL.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/getURL.js
@@ -1,42 +1,40 @@
 /* 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/>. */
 
 // @flow
 
-import { parse } from "../../utils/url";
+import { parse } from "../url";
 import { getUnicodeHostname, getUnicodeUrlPath } from "devtools-modules";
 
 import type { Source } from "../../types";
 export type ParsedURL = {
   path: string,
   group: string,
   filename: string
 };
 
-const urlMap: WeakMap<Source, ParsedURL> = new WeakMap();
-
 export function getFilenameFromPath(pathname?: string) {
   let filename = "";
   if (pathname) {
     filename = pathname.substring(pathname.lastIndexOf("/") + 1);
     // This file does not have a name. Default should be (index).
     if (filename == "" || !filename.includes(".")) {
       filename = "(index)";
     }
   }
   return filename;
 }
 
 const NoDomain = "(no domain)";
 const def = { path: "", group: "", filename: "" };
 
-function _getURL(source: Source, defaultDomain: string): ParsedURL {
+export function getURL(source: Source, defaultDomain: ?string = ""): ParsedURL {
   const { url } = source;
   if (!url) {
     return def;
   }
 
   const { pathname, protocol, host } = parse(url);
   const filename = getUnicodeUrlPath(getFilenameFromPath(pathname));
 
@@ -111,18 +109,8 @@ function _getURL(source: Source, default
 
   return {
     ...def,
     path: pathname,
     group: protocol ? `${protocol}//` : "",
     filename
   };
 }
-
-export function getURL(source: Source, debuggeeUrl: ?string) {
-  if (urlMap.has(source)) {
-    return urlMap.get(source) || def;
-  }
-
-  const url = _getURL(source, debuggeeUrl || "");
-  urlMap.set(source, url);
-  return url;
-}
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap
@@ -10,17 +10,17 @@ exports[`sources-tree addToTree can add 
     - codemirror@5.1 path=unpkg.com/codemirror@5.1 source_id=server1.conn13.child1/37 
 "
 `;
 
 exports[`sources-tree addToTree correctly parses file sources 1`] = `
 " - root path= 
   - file:// path=file:// 
     - a path=file:///a 
-      - b.js path=file:///a/b.js source_id=undefined 
+      - b.js path=file:///a/b.js source_id=actor1 
 "
 `;
 
 exports[`sources-tree addToTree does not attempt to add two of the same directory 1`] = `
 " - root path= 
   - davidwalsh.name path=davidwalsh.name 
     - (index) path=https://davidwalsh.name/ source_id=server1.conn13.child1/37 
     - wp-content path=davidwalsh.name/wp-content 
@@ -34,32 +34,32 @@ exports[`sources-tree addToTree does not
     - (index) path=https://davidwalsh.name/ source_id=server1.conn13.child1/37 
 "
 `;
 
 exports[`sources-tree addToTree does not mangle encoded URLs 1`] = `
 " - root path= 
   - example.com path=example.com 
     - foo path=example.com/foo 
-      - B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 path=example.com/foo/B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 source_id=undefined 
+      - B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 path=example.com/foo/B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 source_id=actor1 
 "
 `;
 
 exports[`sources-tree addToTree excludes javascript: URLs from the tree 1`] = `
 " - root path= 
   - example.com path=example.com 
-    - source1.js path=example.com/source1.js source_id=undefined 
+    - source1.js path=example.com/source1.js source_id=actor2 
 "
 `;
 
 exports[`sources-tree addToTree name does not include query params 1`] = `
 " - root path= 
   - example.com path=example.com 
     - foo path=example.com/foo 
-      - name.js path=example.com/foo/name.js source_id=undefined 
+      - name.js path=example.com/foo/name.js source_id=actor1 
 "
 `;
 
 exports[`sources-tree addToTree replaces a file with a directory 1`] = `
 " - root path= 
   - unpkg.com path=unpkg.com 
     - codemirror@5.1 path=unpkg.com/codemirror@5.1 
       - mode path=unpkg.com/codemirror@5.1/mode 
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap
@@ -1,38 +1,38 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`sources tree collapseTree can collapse a single source 1`] = `
 " - root path= 
   - example.com path=example.com 
     - a/b path=example.com/a/b 
-      - c.js path=example.com/a/b/c.js source_id=undefined 
+      - c.js path=example.com/a/b/c.js source_id=actor1 
 "
 `;
 
 exports[`sources tree collapseTree correctly merges in a collapsed source with a deeper level 1`] = `
 " - root path= 
   - example.com path=example.com 
     - a/b path=example.com/a/b 
       - c/d path=example.com/a/b/c/d 
-        - e.js path=example.com/a/b/c/d/e.js source_id=undefined 
-      - c.js path=example.com/a/b/c.js source_id=undefined 
+        - e.js path=example.com/a/b/c/d/e.js source_id=actor2 
+      - c.js path=example.com/a/b/c.js source_id=actor1 
 "
 `;
 
 exports[`sources tree collapseTree correctly merges in a collapsed source with a shallower level 1`] = `
 " - root path= 
   - example.com path=example.com 
     - a/b path=example.com/a/b 
-      - c.js path=example.com/a/b/c.js source_id=undefined 
-      - x.js path=example.com/a/b/x.js source_id=undefined 
+      - c.js path=example.com/a/b/c.js source_id=actor1 
+      - x.js path=example.com/a/b/x.js source_id=actor3 
 "
 `;
 
 exports[`sources tree collapseTree correctly merges in a collapsed source with the same level 1`] = `
 " - root path= 
   - example.com path=example.com 
     - a/b path=example.com/a/b 
       - c/d path=example.com/a/b/c/d 
-        - e.js path=example.com/a/b/c/d/e.js source_id=undefined 
-      - c.js path=example.com/a/b/c.js source_id=undefined 
+        - e.js path=example.com/a/b/c/d/e.js source_id=actor2 
+      - c.js path=example.com/a/b/c.js source_id=actor1 
 "
 `;
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/sortTree.spec.js.snap
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/__snapshots__/sortTree.spec.js.snap
@@ -1,73 +1,73 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`sources-tree sortEntireTree alphabetically sorts children 1`] = `
 " - root path= 
   - example.com path=example.com 
     - foo path=example.com/foo 
-      - a_source3.js path=example.com/foo/a_source3.js source_id=undefined 
-      - b_source2.js path=example.com/foo/b_source2.js source_id=undefined 
-    - source1.js path=example.com/source1.js source_id=undefined 
+      - a_source3.js path=example.com/foo/a_source3.js source_id=actor3 
+      - b_source2.js path=example.com/foo/b_source2.js source_id=actor2 
+    - source1.js path=example.com/source1.js source_id=actor1 
 "
 `;
 
 exports[`sources-tree sortEntireTree puts folder at the top of the sort 1`] = `
 " - root path= 
   - example.com path=example.com 
     - folder path=example.com/folder 
       - b path=example.com/folder/b 
-        - b.js path=example.com/folder/b/b.js source_id=undefined 
+        - b.js path=example.com/folder/b/b.js source_id=actor2 
       - c path=example.com/folder/c 
-        - (index) path=http://example.com/folder/c/ source_id=undefined 
-      - a.js path=example.com/folder/a.js source_id=undefined 
+        - (index) path=http://example.com/folder/c/ source_id=actor3 
+      - a.js path=example.com/folder/a.js source_id=actor1 
 "
 `;
 
 exports[`sources-tree sortEntireTree puts root debugee url at the top of the sort 1`] = `
 " - root path= 
   - example.com path=example.com 
-    - b.js path=example.com/b.js source_id=undefined 
+    - b.js path=example.com/b.js source_id=actor2 
   - api.example.com path=api.example.com 
-    - a.js path=api.example.com/a.js source_id=undefined 
+    - a.js path=api.example.com/a.js source_id=actor1 
   - demo.com path=demo.com 
-    - c.js path=demo.com/c.js source_id=undefined 
+    - c.js path=demo.com/c.js source_id=actor3 
 "
 `;
 
 exports[`sources-tree sortEntireTree puts root debugee url at the top of the sort 2`] = `
 " - root path= 
   - demo.com path=demo.com 
-    - c.js path=demo.com/c.js source_id=undefined 
+    - c.js path=demo.com/c.js source_id=actor3 
   - api.example.com path=api.example.com 
-    - a.js path=api.example.com/a.js source_id=undefined 
+    - a.js path=api.example.com/a.js source_id=actor1 
   - example.com path=example.com 
-    - b.js path=example.com/b.js source_id=undefined 
+    - b.js path=example.com/b.js source_id=actor2 
 "
 `;
 
 exports[`sources-tree sortEntireTree sorts folders first 1`] = `
 " - root path= 
   - example.com path=example.com 
-    - (index) path=http://example.com source_id=undefined 
+    - (index) path=http://example.com source_id=actor4 
     - b.js path=example.com/b.js 
-      - b_source.js path=example.com/b.js/b_source.js source_id=undefined 
+      - b_source.js path=example.com/b.js/b_source.js source_id=actor2 
     - d path=example.com/d 
-      - d_source.js path=example.com/d/d_source.js source_id=undefined 
-    - a.js path=example.com/a.js source_id=undefined 
-    - b2 path=example.com/b2 source_id=undefined 
-    - c.js path=example.com/c.js source_id=undefined 
+      - d_source.js path=example.com/d/d_source.js source_id=actor5 
+    - a.js path=example.com/a.js source_id=actor1 
+    - b2 path=example.com/b2 source_id=actor6 
+    - c.js path=example.com/c.js source_id=actor3 
 "
 `;
 
 exports[`sources-tree sortEntireTree sorts folders first 2`] = `
 " - root path= 
   - example.com path=example.com 
-    - (index) path=http://example.com source_id=undefined 
+    - (index) path=http://example.com source_id=actor4 
     - b.js path=example.com/b.js 
-      - b_source.js path=example.com/b.js/b_source.js source_id=undefined 
+      - b_source.js path=example.com/b.js/b_source.js source_id=actor2 
     - d path=example.com/d 
-      - d_source.js path=example.com/d/d_source.js source_id=undefined 
-    - a.js path=example.com/a.js source_id=undefined 
-    - b2 path=example.com/b2 source_id=undefined 
-    - c.js path=example.com/c.js source_id=undefined 
+      - d_source.js path=example.com/d/d_source.js source_id=actor5 
+    - a.js path=example.com/a.js source_id=actor1 
+    - b2 path=example.com/b2 source_id=actor6 
+    - c.js path=example.com/c.js source_id=actor3 
 "
 `;
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/addToTree.spec.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/addToTree.spec.js
@@ -34,17 +34,17 @@ function getChildNode(tree, ...path) {
   return path.reduce((child, index) => child.contents[index], tree);
 }
 
 describe("sources-tree", () => {
   describe("addToTree", () => {
     it("should provide node API", () => {
       const source = createSource({
         url: "http://example.com/a/b/c.js",
-        actor: "actor1"
+        id: "actor1"
       });
 
       const root = createDirectoryNode("root", "", [
         createSourceNode("foo", "/foo", source)
       ]);
 
       expect(root.name).toBe("root");
       expect(nodeHasChildren(root)).toBe(true);
@@ -55,17 +55,17 @@ describe("sources-tree", () => {
       expect(child.path).toBe("/foo");
       expect(child.contents).toBe(source);
       expect(nodeHasChildren(child)).toBe(false);
     });
 
     it("builds a path-based tree", () => {
       const source1 = createSource({
         url: "http://example.com/foo/source1.js",
-        actor: "actor1"
+        id: "actor1"
       });
       const tree = createDirectoryNode("root", "", []);
 
       addToTree(tree, source1, "http://example.com/");
       expect(tree.contents).toHaveLength(1);
 
       const base = tree.contents[0];
       expect(base.name).toBe("example.com");
@@ -80,33 +80,33 @@ describe("sources-tree", () => {
     });
 
     it("does not mangle encoded URLs", () => {
       const sourceName = // eslint-disable-next-line max-len
         "B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1";
 
       const source1 = createSource({
         url: `https://example.com/foo/${sourceName}`,
-        actor: "actor1"
+        id: "actor1"
       });
 
       const tree = createDirectoryNode("root", "", []);
 
       addToTree(tree, source1, "http://example.com/");
       const childNode = getChildNode(tree, 0, 0, 0);
       expect(childNode.name).toEqual(sourceName);
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("name does not include query params", () => {
       const sourceName = "name.js?bar=3";
 
       const source1 = createSource({
         url: `https://example.com/foo/${sourceName}`,
-        actor: "actor1"
+        id: "actor1"
       });
 
       const tree = createDirectoryNode("root", "", []);
 
       addToTree(tree, source1, "http://example.com/");
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
@@ -170,25 +170,25 @@ describe("sources-tree", () => {
       const subtree = tree.contents[0];
       expect(subtree.contents).toHaveLength(1);
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("excludes javascript: URLs from the tree", () => {
       const source1 = createSource({
         url: "javascript:alert('Hello World')",
-        actor: "actor1"
+        id: "actor1"
       });
       const source2 = createSource({
         url: "http://example.com/source1.js",
-        actor: "actor2"
+        id: "actor2"
       });
       const source3 = createSource({
         url: "javascript:let i = 10; while (i > 0) i--; console.log(i);",
-        actor: "actor3"
+        id: "actor3"
       });
       const tree = createDirectoryNode("root", "", []);
 
       addToTree(tree, source1, "http://example.com/");
       addToTree(tree, source2, "http://example.com/");
       addToTree(tree, source3, "http://example.com/");
 
       const base = tree.contents[0];
@@ -197,17 +197,17 @@ describe("sources-tree", () => {
       const source1Node = base.contents[0];
       expect(source1Node.name).toBe("source1.js");
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("correctly parses file sources", () => {
       const source = createSource({
         url: "file:///a/b.js",
-        actor: "actor1"
+        id: "actor1"
       });
       const tree = createDirectoryNode("root", "", []);
 
       addToTree(tree, source, "file:///a/index.html");
       expect(tree.contents).toHaveLength(1);
 
       const base = tree.contents[0];
       expect(base.name).toBe("file://");
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/collapseTree.spec.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/collapseTree.spec.js
@@ -9,25 +9,25 @@ import {
   sortEntireTree,
   formatTree,
   addToTree,
   createDirectoryNode
 } from "../index";
 
 const abcSource = createSource({
   url: "http://example.com/a/b/c.js",
-  actor: "actor1"
+  id: "actor1"
 });
 const abcdeSource = createSource({
   url: "http://example.com/a/b/c/d/e.js",
-  actor: "actor2"
+  id: "actor2"
 });
 const abxSource = createSource({
   url: "http://example.com/a/b/x.js",
-  actor: "actor3"
+  id: "actor3"
 });
 
 describe("sources tree", () => {
   describe("collapseTree", () => {
     it("can collapse a single source", () => {
       const fullTree = createDirectoryNode("root", "", []);
       addToTree(fullTree, abcSource, "http://example.com/");
       expect(fullTree.contents).toHaveLength(1);
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/getUrl.spec.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/getUrl.spec.js
@@ -1,19 +1,16 @@
 /* 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/>. */
 
 // @flow
 
 import { getURL } from "../getURL";
 import { createSource } from "../../../reducers/sources";
-import * as Url from "../../url";
-
-let spy;
 
 function createMockSource(props) {
   return createSource(
     Object.assign(
       {
         id: "server1.conn13.child1/39",
         url: "",
         sourceMapURL: "",
@@ -57,79 +54,35 @@ describe("getUrl", () => {
       })
     );
     expect(urlObject.filename).toBe("b.js");
   });
 
   it("handles url with no filename for filename", function() {
     const urlObject = getURL(
       createMockSource({
-        url: "https://a/c"
+        url: "https://a/c",
+        id: "c"
       })
     );
     expect(urlObject.filename).toBe("(index)");
   });
 
   it("separates resources by protocol and host", () => {
     const urlObject = getURL(
       createMockSource({
-        url: "moz-extension://xyz/123"
+        url: "moz-extension://xyz/123",
+        id: "c2"
       })
     );
     expect(urlObject.group).toBe("moz-extension://xyz");
   });
 
   it("creates a group name for webpack", () => {
     const urlObject = getURL(
       createMockSource({
-        url: "webpack://src/component.jsx"
+        url: "webpack://src/component.jsx",
+        id: "c3"
       })
     );
     expect(urlObject.group).toBe("webpack://");
   });
-
-  describe("memoized", () => {
-    beforeEach(() => {
-      spy = jest.spyOn(Url, "parse");
-    });
-
-    afterEach(() => {
-      spy.mockReset();
-      spy.mockRestore();
-    });
-
-    it("parses a url once", () => {
-      const source = createMockSource({
-        url: "http://example.com/foo/bar/baz.js"
-      });
-
-      getURL(source);
-      const url = getURL(source);
-      expect(spy).toHaveBeenCalledTimes(1);
-
-      expect(url).toEqual({
-        filename: "baz.js",
-        group: "example.com",
-        path: "/foo/bar/baz.js"
-      });
-    });
-
-    it("parses a url once per source", () => {
-      const source = createMockSource({
-        url: "http://example.com/foo/bar/baz.js"
-      });
-      const source2 = createMockSource({
-        id: "server1.conn13.child1/40",
-        url: "http://example.com/foo/bar/baz.js"
-      });
-
-      getURL(source);
-      const url = getURL(source2);
-      expect(spy).toHaveBeenCalledTimes(2);
-
-      expect(url).toEqual({
-        filename: "baz.js",
-        group: "example.com",
-        path: "/foo/bar/baz.js"
-      });
-    });
-  });
 });
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/sortTree.spec.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/sortTree.spec.js
@@ -12,25 +12,25 @@ import {
   formatTree
 } from "../index";
 
 describe("sources-tree", () => {
   describe("sortEntireTree", () => {
     it("alphabetically sorts children", () => {
       const source1 = createSource({
         url: "http://example.com/source1.js",
-        actor: "actor1"
+        id: "actor1"
       });
       const source2 = createSource({
         url: "http://example.com/foo/b_source2.js",
-        actor: "actor2"
+        id: "actor2"
       });
       const source3 = createSource({
         url: "http://example.com/foo/a_source3.js",
-        actor: "actor3"
+        id: "actor3"
       });
       const _tree = createDirectoryNode("root", "", []);
 
       addToTree(_tree, source1, "http://example.com/");
       addToTree(_tree, source2, "http://example.com/");
       addToTree(_tree, source3, "http://example.com/");
       const tree = sortEntireTree(_tree);
 
@@ -49,37 +49,37 @@ describe("sources-tree", () => {
       expect(source3Node.name).toBe("a_source3.js");
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("sorts folders first", () => {
       const sources = [
         createSource({
           url: "http://example.com/a.js",
-          actor: "actor1"
+          id: "actor1"
         }),
         createSource({
           url: "http://example.com/b.js/b_source.js",
-          actor: "actor2"
+          id: "actor2"
         }),
         createSource({
           url: "http://example.com/c.js",
-          actor: "actor1"
+          id: "actor3"
         }),
         createSource({
           url: "http://example.com",
-          actor: "actor1"
+          id: "actor4"
         }),
         createSource({
           url: "http://example.com/d/d_source.js",
-          actor: "actor3"
+          id: "actor5"
         }),
         createSource({
           url: "http://example.com/b2",
-          actor: "actor2"
+          id: "actor6"
         })
       ];
 
       const _tree = createDirectoryNode("root", "", []);
       sources.forEach(source =>
         addToTree(_tree, source, "http://example.com/")
       );
       const tree = sortEntireTree(_tree);
@@ -111,25 +111,25 @@ describe("sources-tree", () => {
       expect(cFileNode.name).toBe("c.js");
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("puts folder at the top of the sort", () => {
       const sources = [
         createSource({
           url: "http://example.com/folder/a.js",
-          actor: "actor1"
+          id: "actor1"
         }),
         createSource({
           url: "http://example.com/folder/b/b.js",
-          actor: "actor2"
+          id: "actor2"
         }),
         createSource({
           url: "http://example.com/folder/c/",
-          actor: "actor1"
+          id: "actor3"
         })
       ];
 
       const _tree = createDirectoryNode("root", "", []);
       sources.forEach(source =>
         addToTree(_tree, source, "http://example.com/")
       );
       const tree = sortEntireTree(_tree);
@@ -150,25 +150,25 @@ describe("sources-tree", () => {
       expect(aFileNode.name).toBe("a.js");
       expect(formatTree(tree)).toMatchSnapshot();
     });
 
     it("puts root debugee url at the top of the sort", () => {
       const sources = [
         createSource({
           url: "http://api.example.com/a.js",
-          actor: "actor1"
+          id: "actor1"
         }),
         createSource({
           url: "http://example.com/b.js",
-          actor: "actor2"
+          id: "actor2"
         }),
         createSource({
           url: "http://demo.com/c.js",
-          actor: "actor3"
+          id: "actor3"
         })
       ];
 
       const rootA = "http://example.com/path/to/file.html";
       const rootB = "https://www.demo.com/index.html";
       const _treeA = createDirectoryNode("root", "", []);
       const _treeB = createDirectoryNode("root", "", []);
       sources.forEach(source => {
--- a/devtools/client/debugger/new/src/utils/sources-tree/tests/utils.spec.js
+++ b/devtools/client/debugger/new/src/utils/sources-tree/tests/utils.spec.js
@@ -37,21 +37,21 @@ describe("sources tree", () => {
     });
   });
 
   describe("isDirectory", () => {
     it("identifies directories correctly", () => {
       const sources = [
         createSource({
           url: "http://example.com/a.js",
-          actor: "actor1"
+          id: "actor1"
         }),
         createSource({
           url: "http://example.com/b/c/d.js",
-          actor: "actor2"
+          id: "actor2"
         })
       ];
 
       const tree = createDirectoryNode("root", "", []);
       sources.forEach(source => addToTree(tree, source, "http://example.com/"));
       sortEntireTree(tree);
       const [bFolderNode, aFileNode] = tree.contents[0].contents;
       const [cFolderNode] = bFolderNode.contents;
--- a/devtools/client/debugger/new/src/utils/url.js
+++ b/devtools/client/debugger/new/src/utils/url.js
@@ -1,12 +1,14 @@
 /* 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/>. */
 
+import { memoize } from "lodash";
+
 const defaultUrl = {
   hash: "",
   host: "",
   hostname: "",
   href: "",
   origin: "null",
   password: "",
   path: "",
@@ -14,22 +16,22 @@ const defaultUrl = {
   port: "",
   protocol: "",
   search: "",
   // This should be a "URLSearchParams" object
   searchParams: {},
   username: ""
 };
 
-export function parse(url: string): URL | object {
+export const parse = memoize(function parse(url: string) {
   try {
     const urlObj = new URL(url);
     urlObj.path = urlObj.pathname + urlObj.search;
     return urlObj;
   } catch (err) {
     // If we're given simply a filename...
     if (url) {
       return { ...defaultUrl, path: url, pathname: url };
     }
 
     return defaultUrl;
   }
-}
+});