Bug 1211723 - Share JS source text between JSRuntimes; r=jimb
☠☠ backed out by 06a8c115f8fa ☠ ☠
authorNick Fitzgerald <fitzgen@gmail.com>
Tue, 29 Mar 2016 15:15:47 -0700
changeset 290938 735da799e3bbb98c087339f21599571c48ce484f
parent 290937 63338edce3ba60f6668973b60bf832560e78d7c4
child 290939 cdb91f4483be1538ca4f1f1acad352aff8a5690b
push id30128
push userkwierso@gmail.com
push dateThu, 31 Mar 2016 20:04:34 +0000
treeherdermozilla-central@bccb11375f2a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs1211723
milestone48.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 1211723 - Share JS source text between JSRuntimes; r=jimb This commit adds `SharedImmutableStringsCache` which allows for de-duplication and sharing of immutable strings between threads and JSRuntimes. Each JSRuntime gets a SharedImmutableStringsCache member, but the accessor always returns the parent runtime's cache. The caches in child JSRuntime's are not wasting space, however, as initialization and allocation of the table happens lazily within SharedImmutableStringsCache. Furthermore, this commit removes `js::ScriptSource::Parent` and the `CompressedSourceSet`. They are unnecessary because source text is always shared via the parent runtime's `SharedImmutableStringsCache` now.
js/public/MemoryMetrics.h
js/src/jsapi-tests/moz.build
js/src/jsapi-tests/testSharedImmutableStringsCache.cpp
js/src/jsfun.cpp
js/src/jsscript.cpp
js/src/jsscript.h
js/src/jsstr.cpp
js/src/jsstr.h
js/src/moz.build
js/src/vm/HelperThreads.cpp
js/src/vm/MemoryMetrics.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/vm/SharedImmutableStringsCache.cpp
js/src/vm/SharedImmutableStringsCache.h
js/xpconnect/src/XPCJSRuntime.cpp
--- a/js/public/MemoryMetrics.h
+++ b/js/public/MemoryMetrics.h
@@ -392,18 +392,16 @@ struct NotableStringInfo : public String
 
 /**
  * This class holds information about the memory taken up by script sources
  * from a particular file.
  */
 struct ScriptSourceInfo
 {
 #define FOR_EACH_SIZE(macro) \
-    macro(_, MallocHeap, compressed) \
-    macro(_, MallocHeap, uncompressed) \
     macro(_, MallocHeap, misc)
 
     ScriptSourceInfo()
       : FOR_EACH_SIZE(ZERO_SIZE)
         numScripts(0)
     {}
 
     void add(const ScriptSourceInfo& other) {
@@ -467,18 +465,18 @@ struct RuntimeSizes
 {
 #define FOR_EACH_SIZE(macro) \
     macro(_, MallocHeap, object) \
     macro(_, MallocHeap, atomsTable) \
     macro(_, MallocHeap, contexts) \
     macro(_, MallocHeap, temporary) \
     macro(_, MallocHeap, interpreterStack) \
     macro(_, MallocHeap, mathCache) \
+    macro(_, MallocHeap, sharedImmutableStringsCache) \
     macro(_, MallocHeap, uncompressedSourceCache) \
-    macro(_, MallocHeap, compressedSourceSet) \
     macro(_, MallocHeap, scriptData)
 
     RuntimeSizes()
       : FOR_EACH_SIZE(ZERO_SIZE)
         scriptSourceInfo(),
         code(),
         gc(),
         notableScriptSources()
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -73,16 +73,17 @@ UNIFIED_SOURCES += [
     'testResolveRecursion.cpp',
     'tests.cpp',
     'testSameValue.cpp',
     'testSavedStacks.cpp',
     'testScriptInfo.cpp',
     'testScriptObject.cpp',
     'testSetProperty.cpp',
     'testSetPropertyIgnoringNamedGetter.cpp',
+    'testSharedImmutableStringsCache.cpp',
     'testSourcePolicy.cpp',
     'testStringBuffer.cpp',
     'testStructuredClone.cpp',
     'testSymbol.cpp',
     'testThreadingExclusiveData.cpp',
     'testThreadingMutex.cpp',
     'testToIntWidth.cpp',
     'testTypedArrays.cpp',
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testSharedImmutableStringsCache.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#include "mozilla/IntegerRange.h"
+
+#include "js/Vector.h"
+
+#include "vm/SharedImmutableStringsCache.h"
+
+const int NUM_THREADS = 256;
+const int NUM_ITERATIONS = 256;
+
+const int NUM_STRINGS = 4;
+const char16_t* STRINGS[NUM_STRINGS] = {
+    MOZ_UTF16("uno"),
+    MOZ_UTF16("dos"),
+    MOZ_UTF16("tres"),
+    MOZ_UTF16("quattro")
+};
+
+struct CacheAndIndex
+{
+    js::SharedImmutableStringsCache* cache;
+    int index;
+
+    CacheAndIndex(js::SharedImmutableStringsCache* cache, int index)
+      : cache(cache)
+      , index(index)
+    { }
+};
+
+static void
+getString(void* data)
+{
+    auto cacheAndIndex = reinterpret_cast<CacheAndIndex*>(data);
+
+    for (int i = 0; i < NUM_ITERATIONS; i++) {
+        auto str = STRINGS[cacheAndIndex->index % NUM_STRINGS];
+
+        auto dupe = js::DuplicateString(str);
+        MOZ_RELEASE_ASSERT(dupe);
+
+        auto deduped = cacheAndIndex->cache->getOrCreate(mozilla::Move(dupe), js_strlen(str));
+        MOZ_RELEASE_ASSERT(deduped.isSome());
+        MOZ_RELEASE_ASSERT(js_strcmp(str, deduped->chars()) == 0);
+
+        {
+            auto cloned = deduped->clone();
+            // We should be de-duplicating and giving back the same string.
+            MOZ_RELEASE_ASSERT(deduped->chars() == cloned.chars());
+        }
+    }
+
+    js_delete(cacheAndIndex);
+}
+
+BEGIN_TEST(testSharedImmutableStringsCache)
+{
+    js::SharedImmutableStringsCache cache;
+
+    js::Vector<PRThread*> threads(cx);
+    CHECK(threads.reserve(NUM_THREADS));
+
+    for (auto i : mozilla::MakeRange(NUM_THREADS)) {
+        auto cacheAndIndex = js_new<CacheAndIndex>(&cache, i);
+        CHECK(cacheAndIndex);
+        auto thread = PR_CreateThread(PR_USER_THREAD,
+                                      getString,
+                                      (void *) cacheAndIndex,
+                                      PR_PRIORITY_NORMAL,
+                                      PR_LOCAL_THREAD,
+                                      PR_JOINABLE_THREAD,
+                                      0);
+        CHECK(thread);
+        threads.infallibleAppend(thread);
+    }
+
+    for (auto thread : threads) {
+        CHECK(PR_JoinThread(thread) == PR_SUCCESS);
+    }
+
+    return true;
+}
+END_TEST(testSharedImmutableStringsCache)
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -38,16 +38,17 @@
 #include "jit/Ion.h"
 #include "jit/JitFrameIterator.h"
 #include "js/CallNonGenericMethod.h"
 #include "js/Proxy.h"
 #include "vm/Debugger.h"
 #include "vm/GlobalObject.h"
 #include "vm/Interpreter.h"
 #include "vm/Shape.h"
+#include "vm/SharedImmutableStringsCache.h"
 #include "vm/StringBuffer.h"
 #include "vm/WrapperObject.h"
 #include "vm/Xdr.h"
 
 #include "jsscriptinlines.h"
 
 #include "vm/Interpreter-inl.h"
 #include "vm/Stack-inl.h"
@@ -762,28 +763,33 @@ CreateFunctionPrototype(JSContext* cx, J
                              SingletonObject);
     if (!functionProto_)
         return nullptr;
 
     RootedFunction functionProto(cx, &functionProto_->as<JSFunction>());
 
     const char* rawSource = "() {\n}";
     size_t sourceLen = strlen(rawSource);
-    char16_t* source = InflateString(cx, rawSource, &sourceLen);
+    mozilla::UniquePtr<char16_t[], JS::FreePolicy> source(InflateString(cx, rawSource, &sourceLen));
     if (!source)
         return nullptr;
 
-    ScriptSource* ss =
-        cx->new_<ScriptSource>();
-    if (!ss) {
-        js_free(source);
+    ScriptSource* ss = cx->new_<ScriptSource>();
+    if (!ss)
+        return nullptr;
+    ScriptSourceHolder ssHolder(ss);
+
+    auto& cache = cx->runtime()->sharedImmutableStrings();
+    auto deduped = cache.getOrCreate(mozilla::Move(source), sourceLen);
+    if (!deduped) {
+        ReportOutOfMemory(cx);
         return nullptr;
     }
-    ScriptSourceHolder ssHolder(ss);
-    ss->setSource(source, sourceLen);
+    ss->setSource(mozilla::Move(*deduped));
+
     CompileOptions options(cx);
     options.setNoScriptRval(true)
            .setVersion(JSVERSION_DEFAULT);
     RootedScriptSource sourceObject(cx, ScriptSourceObject::create(cx, ss));
     if (!sourceObject || !ScriptSourceObject::initFromOptions(cx, sourceObject, options))
         return nullptr;
 
     RootedScript script(cx, JSScript::Create(cx,
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -42,16 +42,17 @@
 #include "js/MemoryMetrics.h"
 #include "js/Utility.h"
 #include "vm/ArgumentsObject.h"
 #include "vm/Compression.h"
 #include "vm/Debugger.h"
 #include "vm/Opcodes.h"
 #include "vm/SelfHosting.h"
 #include "vm/Shape.h"
+#include "vm/SharedImmutableStringsCache.h"
 #include "vm/Xdr.h"
 
 #include "jsfuninlines.h"
 #include "jsobjinlines.h"
 
 #include "vm/ScopeObject-inl.h"
 #include "vm/Stack-inl.h"
 
@@ -1787,17 +1788,26 @@ JSScript::loadSource(JSContext* cx, Scri
     if (!cx->runtime()->sourceHook || !ss->sourceRetrievable())
         return true;
     char16_t* src = nullptr;
     size_t length;
     if (!cx->runtime()->sourceHook->load(cx, ss->filename(), &src, &length))
         return false;
     if (!src)
         return true;
-    ss->setSource(src, length);
+
+    mozilla::UniquePtr<char16_t[], JS::FreePolicy> ownedSource(src);
+    auto& cache = cx->runtime()->sharedImmutableStrings();
+    auto deduped = cache.getOrCreate(mozilla::Move(ownedSource), length);
+    if (!deduped) {
+        ReportOutOfMemory(cx);
+        return false;
+    }
+    ss->setSource(mozilla::Move(*deduped));
+
     *worked = true;
     return true;
 }
 
 JSFlatString*
 JSScript::sourceData(JSContext* cx)
 {
     MOZ_ASSERT(scriptSource()->hasSourceData());
@@ -1943,17 +1953,17 @@ ScriptSource::chars(JSContext* cx, Uncom
         explicit CharsMatcher(JSContext* cx, ScriptSource& ss,
                               UncompressedSourceCache::AutoHoldEntry& holder)
           : cx(cx)
           , ss(ss)
           , holder(holder)
         { }
 
         ReturnType match(Uncompressed& u) {
-            return u.chars;
+            return u.string.chars();
         }
 
         ReturnType match(Compressed& c) {
             if (const char16_t* decompressed = cx->runtime()->uncompressedSourceCache.lookup(&ss, holder))
                 return decompressed;
 
             const size_t nbytes = sizeof(char16_t) * (ss.length() + 1);
             char16_t* decompressed = static_cast<char16_t*>(js_malloc(nbytes));
@@ -1975,20 +1985,16 @@ ScriptSource::chars(JSContext* cx, Uncom
                 JS_ReportOutOfMemory(cx);
                 js_free(decompressed);
                 return nullptr;
             }
 
             return decompressed;
         }
 
-        ReturnType match(Parent& p) {
-            return p.parent->chars(cx, holder);
-        }
-
         ReturnType match(Missing&) {
             MOZ_CRASH("ScriptSource::chars() on ScriptSource with SourceType = Missing");
             return nullptr;
         }
     };
 
     CharsMatcher cm(cx, *this, holder);
     return data.match(cm);
@@ -2012,88 +2018,49 @@ ScriptSource::substringDontDeflate(JSCon
     UncompressedSourceCache::AutoHoldEntry holder;
     const char16_t* chars = this->chars(cx, holder);
     if (!chars)
         return nullptr;
     return NewStringCopyNDontDeflate<CanGC>(cx, chars + start, stop - start);
 }
 
 void
-ScriptSource::setSource(const char16_t* chars, size_t length, bool ownsChars /* = true */)
+ScriptSource::setSource(SharedImmutableTwoByteString&& string)
 {
     MOZ_ASSERT(data.is<Missing>());
-
-    data = SourceType(Uncompressed(chars, ownsChars));
-    length_ = length;
-}
-
-void
-ScriptSource::setCompressedSource(JSRuntime* maybert, void* raw, size_t nbytes, HashNumber hash)
-{
-    MOZ_ASSERT(data.is<Missing>() || data.is<Uncompressed>());
-
-    if (data.is<Uncompressed>() && data.as<Uncompressed>().ownsChars)
-        js_free(const_cast<char16_t*>(uncompressedChars()));
-
-    data = SourceType(Compressed(raw, nbytes, hash));
-
-    if (maybert)
-        updateCompressedSourceSet(maybert);
+    data = SourceType(Uncompressed(mozilla::Move(string)));
 }
 
 void
-ScriptSource::updateCompressedSourceSet(JSRuntime* rt)
+ScriptSource::setCompressedSource(SharedImmutableString&& raw, size_t length)
 {
-    MOZ_ASSERT(data.is<Compressed>());
-    MOZ_ASSERT(!inCompressedSourceSet);
-
-    CompressedSourceSet::AddPtr p = rt->compressedSourceSet.lookupForAdd(this);
-    if (p) {
-        // There is another ScriptSource with the same compressed data.
-        // Mark that ScriptSource as the parent and use it for all attempts to
-        // get the source for this ScriptSource.
-        ScriptSource* parent = *p;
-        parent->incref();
-
-        js_free(compressedData());
-        data = SourceType(Parent(parent));
-    } else {
-        if (rt->compressedSourceSet.add(p, this))
-            inCompressedSourceSet = true;
-    }
-}
-
-bool
-ScriptSource::ensureOwnsSource(ExclusiveContext* cx)
-{
-    MOZ_ASSERT(data.is<Uncompressed>());
-    if (ownsUncompressedChars())
-        return true;
-
-    char16_t* uncompressed = cx->zone()->pod_malloc<char16_t>(Max<size_t>(length_, 1));
-    if (!uncompressed) {
-        ReportOutOfMemory(cx);
-        return false;
-    }
-    PodCopy(uncompressed, uncompressedChars(), length_);
-
-    data.as<Uncompressed>().chars = uncompressed;
-    data.as<Uncompressed>().ownsChars = true;
-    return true;
+    MOZ_ASSERT(data.is<Missing>() || data.is<Uncompressed>());
+    MOZ_ASSERT_IF(data.is<Uncompressed>(), data.as<Uncompressed>().string.length() == length);
+
+    data = SourceType(Compressed(mozilla::Move(raw), length));
 }
 
 bool
 ScriptSource::setSourceCopy(ExclusiveContext* cx, SourceBufferHolder& srcBuf,
                             bool argumentsNotIncluded, SourceCompressionTask* task)
 {
     MOZ_ASSERT(!hasSourceData());
     argumentsNotIncluded_ = argumentsNotIncluded;
 
-    bool owns = srcBuf.ownsChars();
-    setSource(owns ? srcBuf.take() : srcBuf.get(), srcBuf.length(), owns);
+    auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
+    auto deduped = cache.getOrCreate(srcBuf.get(), srcBuf.length(), [&]() {
+        return srcBuf.ownsChars()
+            ? mozilla::UniquePtr<char16_t[], JS::FreePolicy>(srcBuf.take())
+            : DuplicateString(srcBuf.get(), srcBuf.length());
+    });
+    if (!deduped) {
+        ReportOutOfMemory(cx);
+        return false;
+    }
+    setSource(mozilla::Move(*deduped));
 
     // There are several cases where source compression is not a good idea:
     //  - If the script is tiny, then compression will save little or no space.
     //  - If the script is enormous, then decompression can take seconds. With
     //    lazy parsing, decompression is not uncommon, so this can significantly
     //    increase latency.
     //  - If there is only one core, then compression will contend with JS
     //    execution (which hurts benchmarketing).
@@ -2116,18 +2083,16 @@ ScriptSource::setSourceCopy(ExclusiveCon
         HelperThreadState().threadCount >= 2 &&
         CanUseExtraThreads();
     const size_t TINY_SCRIPT = 256;
     const size_t HUGE_SCRIPT = 5 * 1024 * 1024;
     if (TINY_SCRIPT <= srcBuf.length() && srcBuf.length() < HUGE_SCRIPT && canCompressOffThread) {
         task->ss = this;
         if (!StartOffThreadCompression(cx, task))
             return false;
-    } else if (!ensureOwnsSource(cx)) {
-        return false;
     }
 
     return true;
 }
 
 SourceCompressionTask::ResultType
 SourceCompressionTask::work()
 {
@@ -2170,71 +2135,29 @@ SourceCompressionTask::work()
           case Compressor::DONE:
             cont = false;
             break;
           case Compressor::OOM:
             return OOM;
         }
     }
     compressedBytes = comp.outWritten();
-    compressedHash = CompressedSourceHasher::computeHash(compressed, compressedBytes);
+    compressedHash = mozilla::HashBytes(compressed, compressedBytes);
 
     // Shrink the buffer to the size of the compressed data.
     if (void* newCompressed = js_realloc(compressed, compressedBytes))
         compressed = newCompressed;
 
     return Success;
 }
 
-ScriptSource::~ScriptSource()
-{
-    struct DestroyMatcher
-    {
-        using ReturnType = void;
-
-        ScriptSource& ss;
-
-        explicit DestroyMatcher(ScriptSource& ss)
-          : ss(ss)
-        { }
-
-        ReturnType match(Uncompressed& u) {
-            if (u.ownsChars)
-                js_free(const_cast<char16_t*>(u.chars));
-        }
-
-        ReturnType match(Compressed& c) {
-            if (ss.inCompressedSourceSet)
-                TlsPerThreadData.get()->runtimeFromMainThread()->compressedSourceSet.remove(&ss);
-            js_free(c.raw);
-        }
-
-        ReturnType match(Parent& p) {
-            p.parent->decref();
-        }
-
-        ReturnType match(Missing&) {
-            // Nothing to do here.
-        }
-    };
-
-    MOZ_ASSERT_IF(inCompressedSourceSet, data.is<Compressed>());
-
-    DestroyMatcher dm(*this);
-    data.match(dm);
-}
-
 void
 ScriptSource::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                      JS::ScriptSourceInfo* info) const
 {
-    if (data.is<Uncompressed>() && ownsUncompressedChars())
-        info->uncompressed += mallocSizeOf(uncompressedChars());
-    else if (data.is<Compressed>())
-        info->compressed += mallocSizeOf(compressedData());
     info->misc += mallocSizeOf(this) +
                   mallocSizeOf(filename_.get()) +
                   mallocSizeOf(introducerFilename_.get());
     info->numScripts++;
 }
 
 template<XDRMode mode>
 bool
@@ -2244,43 +2167,35 @@ ScriptSource::performXDR(XDRState<mode>*
     {
         using ReturnType = size_t;
 
         ReturnType match(Uncompressed&) {
             return 0;
         }
 
         ReturnType match(Compressed& c) {
-            return c.nbytes;
-        }
-
-        ReturnType match(Parent& p) {
-            return p.parent->data.match(*this);
+            return c.nbytes();
         }
 
         ReturnType match(Missing&) {
             MOZ_CRASH("Missing source data in ScriptSource::performXDR");
             return 0;
         }
     };
 
     struct RawDataMatcher
     {
         using ReturnType = void*;
 
         ReturnType match(Uncompressed& u) {
-            return (void*) u.chars;
+            return (void*) u.string.chars();
         }
 
         ReturnType match(Compressed& c) {
-            return c.raw;
-        }
-
-        ReturnType match(Parent& p) {
-            return p.parent->data.match(*this);
+            return (void*) c.raw.chars();
         }
 
         ReturnType match(Missing&) {
             MOZ_CRASH("Missing source data in ScriptSource::performXDR");
             return nullptr;
         }
     };
 
@@ -2289,17 +2204,20 @@ ScriptSource::performXDR(XDRState<mode>*
         return false;
 
     uint8_t retrievable = sourceRetrievable_;
     if (!xdr->codeUint8(&retrievable))
         return false;
     sourceRetrievable_ = retrievable;
 
     if (hasSource && !sourceRetrievable_) {
-        if (!xdr->codeUint32(&length_))
+        uint32_t len = 0;
+        if (mode == XDR_ENCODE)
+            len = length();
+        if (!xdr->codeUint32(&len))
             return false;
 
         uint32_t compressedLength;
         if (mode == XDR_ENCODE) {
             CompressedLengthMatcher m;
             compressedLength = data.match(m);
         }
         if (!xdr->codeUint32(&compressedLength))
@@ -2310,29 +2228,45 @@ ScriptSource::performXDR(XDRState<mode>*
             if (mode == XDR_ENCODE)
                 argumentsNotIncluded = argumentsNotIncluded_;
             if (!xdr->codeUint8(&argumentsNotIncluded))
                 return false;
             if (mode == XDR_DECODE)
                 argumentsNotIncluded_ = argumentsNotIncluded;
         }
 
-        size_t byteLen = compressedLength ? compressedLength : (length_ * sizeof(char16_t));
+        size_t byteLen = compressedLength ? compressedLength : (len * sizeof(char16_t));
         if (mode == XDR_DECODE) {
             uint8_t* p = xdr->cx()->template pod_malloc<uint8_t>(Max<size_t>(byteLen, 1));
             if (!p || !xdr->codeBytes(p, byteLen)) {
                 js_free(p);
                 return false;
             }
 
-            if (compressedLength)
-                setCompressedSource(xdr->cx()->runtime(), p, compressedLength,
-                                    CompressedSourceHasher::computeHash(p, compressedLength));
-            else
-                setSource((const char16_t*) p, length_);
+            if (compressedLength) {
+                mozilla::UniquePtr<char[], JS::FreePolicy> compressedSource(
+                    reinterpret_cast<char*>(p));
+                auto& cache = xdr->cx()->runtime()->sharedImmutableStrings();
+                auto deduped = cache.getOrCreate(mozilla::Move(compressedSource), byteLen);
+                if (!deduped) {
+                    ReportOutOfMemory(xdr->cx());
+                    return false;
+                }
+                setCompressedSource(mozilla::Move(*deduped), len);
+            } else {
+                mozilla::UniquePtr<char16_t[], JS::FreePolicy> source(
+                    reinterpret_cast<char16_t*>(p));
+                auto& cache = xdr->cx()->runtime()->sharedImmutableStrings();
+                auto deduped = cache.getOrCreate(mozilla::Move(source), len);
+                if (!deduped) {
+                    ReportOutOfMemory(xdr->cx());
+                    return false;
+                }
+                setSource(mozilla::Move(*deduped));
+            }
         } else {
             RawDataMatcher rdm;
             void* p = data.match(rdm);
             if (!xdr->codeBytes(p, byteLen))
                 return false;
         }
     }
 
@@ -2503,26 +2437,16 @@ ScriptSource::setSourceMapURL(ExclusiveC
     size_t len = js_strlen(sourceMapURL) + 1;
     if (len == 1)
         return true;
 
     sourceMapURL_ = DuplicateString(cx, sourceMapURL);
     return sourceMapURL_ != nullptr;
 }
 
-size_t
-ScriptSource::computedSizeOfData() const
-{
-    if (data.is<Uncompressed>() && ownsUncompressedChars())
-        return sizeof(char16_t) * length_;
-    if (data.is<Compressed>())
-        return compressedBytes();
-    return 0;
-}
-
 /*
  * Shared script data management.
  */
 
 SharedScriptData*
 js::SharedScriptData::new_(ExclusiveContext* cx, uint32_t codeLength,
                            uint32_t srcnotesLength, uint32_t natoms)
 {
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -21,16 +21,17 @@
 
 #include "gc/Barrier.h"
 #include "gc/Rooting.h"
 #include "jit/IonCode.h"
 #include "js/UbiNode.h"
 #include "js/UniquePtr.h"
 #include "vm/NativeObject.h"
 #include "vm/Shape.h"
+#include "vm/SharedImmutableStringsCache.h"
 
 namespace JS {
 struct ScriptSourceInfo;
 } // namespace JS
 
 namespace js {
 
 namespace jit {
@@ -611,52 +612,39 @@ class ScriptSource
     // on the main thread.
 
     // Indicate which field in the |data| union is active.
 
     struct Missing { };
 
     struct Uncompressed
     {
-        Uncompressed(const char16_t* chars, bool ownsChars)
-          : chars(chars)
-          , ownsChars(ownsChars)
+        SharedImmutableTwoByteString string;
+
+        explicit Uncompressed(SharedImmutableTwoByteString&& str)
+          : string(mozilla::Move(str))
         { }
-
-        const char16_t* chars;
-        bool ownsChars;
     };
 
     struct Compressed
     {
-        Compressed(void* raw, size_t nbytes, HashNumber hash)
-          : raw(raw)
-          , nbytes(nbytes)
-          , hash(hash)
+        SharedImmutableString raw;
+        size_t length;
+
+        Compressed(SharedImmutableString&& raw, size_t length)
+          : raw(mozilla::Move(raw))
+          , length(length)
         { }
 
-        void* raw;
-        size_t nbytes;
-        HashNumber hash;
+        size_t nbytes() const { return raw.length(); }
     };
 
-    struct Parent
-    {
-        explicit Parent(ScriptSource* parent)
-          : parent(parent)
-        { }
-
-        ScriptSource* parent;
-    };
-
-    using SourceType = mozilla::Variant<Missing, Uncompressed, Compressed, Parent>;
+    using SourceType = mozilla::Variant<Missing, Uncompressed, Compressed>;
     SourceType data;
 
-    uint32_t length_;
-
     // The filename of this script.
     UniqueChars filename_;
 
     UniqueTwoByteChars displayURL_;
     UniqueTwoByteChars sourceMapURL_;
     bool mutedErrors_;
 
     // bytecode offset in caller script that generated this code.
@@ -691,95 +679,96 @@ class ScriptSource
 
     // True if we can call JSRuntime::sourceHook to load the source on
     // demand. If sourceRetrievable_ and hasSourceData() are false, it is not
     // possible to get source at all.
     bool sourceRetrievable_:1;
     bool argumentsNotIncluded_:1;
     bool hasIntroductionOffset_:1;
 
-    // Whether this is in the runtime's set of compressed ScriptSources.
-    bool inCompressedSourceSet:1;
-
   public:
     explicit ScriptSource()
       : refs(0),
         data(SourceType(Missing())),
-        length_(0),
         filename_(nullptr),
         displayURL_(nullptr),
         sourceMapURL_(nullptr),
         mutedErrors_(false),
         introductionOffset_(0),
         introducerFilename_(nullptr),
         introductionType_(nullptr),
         sourceRetrievable_(false),
         argumentsNotIncluded_(false),
-        hasIntroductionOffset_(false),
-        inCompressedSourceSet(false)
+        hasIntroductionOffset_(false)
     {
     }
-    ~ScriptSource();
+
     void incref() { refs++; }
     void decref() {
         MOZ_ASSERT(refs != 0);
         if (--refs == 0)
             js_delete(this);
     }
     bool initFromOptions(ExclusiveContext* cx, const ReadOnlyCompileOptions& options);
     bool setSourceCopy(ExclusiveContext* cx,
                        JS::SourceBufferHolder& srcBuf,
                        bool argumentsNotIncluded,
                        SourceCompressionTask* tok);
     void setSourceRetrievable() { sourceRetrievable_ = true; }
     bool sourceRetrievable() const { return sourceRetrievable_; }
     bool hasSourceData() const { return !data.is<Missing>(); }
     bool hasCompressedSource() const { return data.is<Compressed>(); }
+
     size_t length() const {
+        struct LengthMatcher
+        {
+            using ReturnType = size_t;
+
+            ReturnType match(const Uncompressed& u) {
+                return u.string.length();
+            }
+
+            ReturnType match(const Compressed& c) {
+                return c.length;
+            }
+
+            ReturnType match(const Missing& m) {
+                MOZ_CRASH("ScriptSource::length on a missing source");
+                return 0;
+            }
+        };
+
         MOZ_ASSERT(hasSourceData());
-        return length_;
+        return data.match(LengthMatcher());
     }
+
     bool argumentsNotIncluded() const {
         MOZ_ASSERT(hasSourceData());
         return argumentsNotIncluded_;
     }
     const char16_t* chars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& asp);
     JSFlatString* substring(JSContext* cx, uint32_t start, uint32_t stop);
     JSFlatString* substringDontDeflate(JSContext* cx, uint32_t start, uint32_t stop);
     void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                 JS::ScriptSourceInfo* info) const;
 
     const char16_t* uncompressedChars() const {
-        return data.as<Uncompressed>().chars;
+        return data.as<Uncompressed>().string.chars();
     }
 
-    bool ownsUncompressedChars() const {
-        return data.as<Uncompressed>().ownsChars;
-    }
-
-    void* compressedData() const {
-        return data.as<Compressed>().raw;
+    const void* compressedData() const {
+        return static_cast<const void*>(data.as<Compressed>().raw.chars());
     }
 
     size_t compressedBytes() const {
-        return data.as<Compressed>().nbytes;
-    }
-
-    HashNumber compressedHash() const {
-        return data.as<Compressed>().hash;
+        return data.as<Compressed>().nbytes();
     }
 
-    ScriptSource* parent() const {
-        return data.as<Parent>().parent;
-    }
-
-    void setSource(const char16_t* chars, size_t length, bool ownsChars = true);
-    void setCompressedSource(JSRuntime* maybert, void* raw, size_t nbytes, HashNumber hash);
-    void updateCompressedSourceSet(JSRuntime* rt);
-    bool ensureOwnsSource(ExclusiveContext* cx);
+    void setSource(SharedImmutableTwoByteString&& string);
+    void setCompressedSource(SharedImmutableString&& raw, size_t length);
 
     // XDR handling
     template <XDRMode mode>
     bool performXDR(XDRState<mode>* xdr);
 
     bool setFilename(ExclusiveContext* cx, const char* filename);
     const char* introducerFilename() const {
         return introducerFilename_ ? introducerFilename_.get() : filename_.get();
@@ -819,19 +808,16 @@ class ScriptSource
         return introductionOffset_;
     }
     void setIntroductionOffset(uint32_t offset) {
         MOZ_ASSERT(!hasIntroductionOffset());
         MOZ_ASSERT(offset <= (uint32_t)INT32_MAX);
         introductionOffset_ = offset;
         hasIntroductionOffset_ = true;
     }
-
-  private:
-    size_t computedSizeOfData() const;
 };
 
 class ScriptSourceHolder
 {
     ScriptSource* ss;
   public:
     ScriptSourceHolder()
       : ss(nullptr)
@@ -852,37 +838,16 @@ class ScriptSourceHolder
         ss = newss;
         ss->incref();
     }
     ScriptSource* get() const {
         return ss;
     }
 };
 
-struct CompressedSourceHasher
-{
-    typedef ScriptSource* Lookup;
-
-    static HashNumber computeHash(const void* data, size_t nbytes) {
-        return mozilla::HashBytes(data, nbytes);
-    }
-
-    static HashNumber hash(const ScriptSource* ss) {
-        return ss->compressedHash();
-    }
-
-    static bool match(const ScriptSource* a, const ScriptSource* b) {
-        return a->compressedBytes() == b->compressedBytes() &&
-               a->compressedHash() == b->compressedHash() &&
-               !memcmp(a->compressedData(), b->compressedData(), a->compressedBytes());
-    }
-};
-
-typedef HashSet<ScriptSource*, CompressedSourceHasher, SystemAllocPolicy> CompressedSourceSet;
-
 class ScriptSourceObject : public NativeObject
 {
   public:
     static const Class class_;
 
     static void trace(JSTracer* trc, JSObject* obj);
     static void finalize(FreeOp* fop, JSObject* obj);
     static ScriptSourceObject* create(ExclusiveContext* cx, ScriptSource* source);
--- a/js/src/jsstr.cpp
+++ b/js/src/jsstr.cpp
@@ -4742,24 +4742,41 @@ js::DuplicateString(js::ExclusiveContext
 }
 
 UniqueChars
 js::DuplicateString(const char* s)
 {
     return UniqueChars(js_strdup(s));
 }
 
+UniqueChars
+js::DuplicateString(const char* s, size_t n)
+{
+    UniqueChars ret(js_pod_malloc<char>(n + 1));
+    if (!ret)
+        return nullptr;
+    PodCopy(ret.get(), s, n);
+    ret[n] = 0;
+    return ret;
+}
+
 UniqueTwoByteChars
 js::DuplicateString(const char16_t* s)
 {
-    size_t n = js_strlen(s) + 1;
-    UniqueTwoByteChars ret(js_pod_malloc<char16_t>(n));
+    return DuplicateString(s, js_strlen(s));
+}
+
+UniqueTwoByteChars
+js::DuplicateString(const char16_t* s, size_t n)
+{
+    UniqueTwoByteChars ret(js_pod_malloc<char16_t>(n + 1));
     if (!ret)
         return nullptr;
     PodCopy(ret.get(), s, n);
+    ret[n] = 0;
     return ret;
 }
 
 template <typename CharT>
 const CharT*
 js_strchr_limit(const CharT* s, char16_t c, const CharT* limit)
 {
     while (s < limit) {
--- a/js/src/jsstr.h
+++ b/js/src/jsstr.h
@@ -131,19 +131,25 @@ DuplicateString(ExclusiveContext* cx, co
 
 /*
  * These variants do not report OOMs, you must arrange for OOMs to be reported
  * yourself.
  */
 extern UniqueChars
 DuplicateString(const char* s);
 
+extern UniqueChars
+DuplicateString(const char* s, size_t n);
+
 extern UniqueTwoByteChars
 DuplicateString(const char16_t* s);
 
+extern UniqueTwoByteChars
+DuplicateString(const char16_t* s, size_t n);
+
 /*
  * Convert a non-string value to a string, returning null after reporting an
  * error, otherwise returning a new string reference.
  */
 template <AllowGC allowGC>
 extern JSString*
 ToStringSlow(ExclusiveContext* cx, typename MaybeRooted<Value, allowGC>::HandleType arg);
 
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -333,16 +333,17 @@ UNIFIED_SOURCES += [
     'vm/RegExpObject.cpp',
     'vm/RegExpStatics.cpp',
     'vm/Runtime.cpp',
     'vm/SavedStacks.cpp',
     'vm/ScopeObject.cpp',
     'vm/SelfHosting.cpp',
     'vm/Shape.cpp',
     'vm/SharedArrayObject.cpp',
+    'vm/SharedImmutableStringsCache.cpp',
     'vm/SPSProfiler.cpp',
     'vm/Stack.cpp',
     'vm/Stopwatch.cpp',
     'vm/String.cpp',
     'vm/StringBuffer.cpp',
     'vm/StructuredClone.cpp',
     'vm/Symbol.cpp',
     'vm/TaggedProto.cpp',
--- a/js/src/vm/HelperThreads.cpp
+++ b/js/src/vm/HelperThreads.cpp
@@ -11,16 +11,17 @@
 #include "jsnativestack.h"
 #include "jsnum.h" // For FIX_FPU()
 
 #include "asmjs/WasmIonCompile.h"
 #include "frontend/BytecodeCompiler.h"
 #include "gc/GCInternals.h"
 #include "jit/IonBuilder.h"
 #include "vm/Debugger.h"
+#include "vm/SharedImmutableStringsCache.h"
 #include "vm/Time.h"
 #include "vm/TraceLogging.h"
 
 #include "jscntxtinlines.h"
 #include "jscompartmentinlines.h"
 #include "jsobjinlines.h"
 #include "jsscriptinlines.h"
 
@@ -1186,21 +1187,16 @@ GlobalHelperThreadState::finishParseTask
         // memory.
         ReportOutOfMemory(cx);
         return nullptr;
     }
 
     // The Debugger only needs to be told about the topmost script that was compiled.
     Debugger::onNewScript(cx, script);
 
-    // Update the compressed source table with the result. This is normally
-    // called by setCompressedSource when compilation occurs on the main thread.
-    if (script->scriptSource()->hasCompressedSource())
-        script->scriptSource()->updateCompressedSourceSet(rt);
-
     return script;
 }
 
 JSScript*
 GlobalHelperThreadState::finishScriptParseTask(JSContext* maybecx, JSRuntime* rt, void* token)
 {
     JSScript* script = finishParseTask(maybecx, rt, ParseTaskKind::Script, token);
     MOZ_ASSERT_IF(script, script->isGlobalCode());
@@ -1616,28 +1612,35 @@ SourceCompressionTask::complete()
 
     {
         AutoLockHelperThreadState lock;
         while (HelperThreadState().compressionInProgress(this))
             HelperThreadState().wait(GlobalHelperThreadState::CONSUMER);
     }
 
     if (result == Success) {
-        ss->setCompressedSource(cx->isJSContext() ? cx->asJSContext()->runtime() : nullptr,
-                                compressed, compressedBytes, compressedHash);
+        mozilla::UniquePtr<char[], JS::FreePolicy> compressedSource(
+            reinterpret_cast<char*>(compressed));
+        compressed = nullptr;
 
-        // Update memory accounting.
-        cx->updateMallocCounter(ss->computedSizeOfData());
+        auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
+        auto deduped = cache.getOrCreate(mozilla::Move(compressedSource), compressedBytes);
+        if (!deduped) {
+            ReportOutOfMemory(cx);
+            result = OOM;
+            ss = nullptr;
+            return false;
+        }
+
+        ss->setCompressedSource(mozilla::Move(*deduped), ss->length());
     } else {
         js_free(compressed);
 
         if (result == OOM)
             ReportOutOfMemory(cx);
-        else if (result == Aborted && !ss->ensureOwnsSource(cx))
-            result = OOM;
     }
 
     ss = nullptr;
     compressed = nullptr;
     MOZ_ASSERT(!active());
 
     return result != OOM;
 }
--- a/js/src/vm/MemoryMetrics.cpp
+++ b/js/src/vm/MemoryMetrics.cpp
@@ -448,17 +448,16 @@ StatsCellCallback(JSRuntime* rt, void* d
         ScriptSource* ss = script->scriptSource();
         SourceSet::AddPtr entry = closure->seenSources.lookupForAdd(ss);
         if (!entry) {
             bool ok = closure->seenSources.add(entry, ss);
             (void)ok; // Not much to be done on failure.
 
             JS::ScriptSourceInfo info;  // This zeroes all the sizes.
             ss->addSizeOfIncludingThis(rtStats->mallocSizeOf_, &info);
-            MOZ_ASSERT(info.compressed == 0 || info.uncompressed == 0);
 
             rtStats->runtime.scriptSourceInfo.add(info);
 
             if (granularity == FineGrained) {
                 const char* filename = ss->filename();
                 if (!filename)
                     filename = "<no filename>";
 
@@ -921,9 +920,8 @@ AddServoSizeOf(JSRuntime *rt, MallocSize
                          sizes->gcHeapDecommitted;
     MOZ_ASSERT(rtStats.gcHeapChunkTotal == gcHeapTotal - gcHeapTotalOriginal);
 #endif
 
     return true;
 }
 
 } // namespace JS
-
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -326,19 +326,16 @@ JSRuntime::init(uint32_t maxbytes, uint3
         return false;
 
     if (!scriptDataTable_.init())
         return false;
 
     if (!evalCache.init())
         return false;
 
-    if (!compressedSourceSet.init())
-        return false;
-
     /* The garbage collector depends on everything before this point being initialized. */
     gcInitialized = true;
 
     if (!InitRuntimeNumberState(this))
         return false;
 
     JS::ResetTimeZone();
 
@@ -534,19 +531,20 @@ JSRuntime::addSizeOfIncludingThis(mozill
         rtSizes->contexts += acx->sizeOfIncludingThis(mallocSizeOf);
 
     rtSizes->temporary += tempLifoAlloc.sizeOfExcludingThis(mallocSizeOf);
 
     rtSizes->interpreterStack += interpreterStack_.sizeOfExcludingThis(mallocSizeOf);
 
     rtSizes->mathCache += mathCache_ ? mathCache_->sizeOfIncludingThis(mallocSizeOf) : 0;
 
-    rtSizes->uncompressedSourceCache += uncompressedSourceCache.sizeOfExcludingThis(mallocSizeOf);
+    rtSizes->sharedImmutableStringsCache +=
+        sharedImmutableStrings_.sizeOfExcludingThis(mallocSizeOf);
 
-    rtSizes->compressedSourceSet += compressedSourceSet.sizeOfExcludingThis(mallocSizeOf);
+    rtSizes->uncompressedSourceCache += uncompressedSourceCache.sizeOfExcludingThis(mallocSizeOf);
 
     rtSizes->scriptData += scriptDataTable().sizeOfExcludingThis(mallocSizeOf);
     for (ScriptDataTable::Range r = scriptDataTable().all(); !r.empty(); r.popFront())
         rtSizes->scriptData += mallocSizeOf(r.front());
 
     if (jitRuntime_) {
         jitRuntime_->execAlloc().addSizeOfCode(&rtSizes->code);
         jitRuntime_->backedgeExecAlloc().addSizeOfCode(&rtSizes->code);
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -38,16 +38,17 @@
 # include "js/Proxy.h" // For AutoEnterPolicy
 #endif
 #include "js/UniquePtr.h"
 #include "js/Vector.h"
 #include "vm/CodeCoverage.h"
 #include "vm/CommonPropertyNames.h"
 #include "vm/DateTime.h"
 #include "vm/MallocProvider.h"
+#include "vm/SharedImmutableStringsCache.h"
 #include "vm/SPSProfiler.h"
 #include "vm/Stack.h"
 #include "vm/Stopwatch.h"
 #include "vm/Symbol.h"
 
 #ifdef _MSC_VER
 #pragma warning(push)
 #pragma warning(disable:4100) /* Silence unreferenced formal parameter warnings */
@@ -1260,34 +1261,36 @@ struct JSRuntime : public JS::shadow::Ru
     const char*         thousandsSeparator;
     const char*         decimalSeparator;
     const char*         numGrouping;
 #endif
 
   private:
     js::MathCache* mathCache_;
     js::MathCache* createMathCache(JSContext* cx);
+    js::SharedImmutableStringsCache sharedImmutableStrings_;
   public:
     js::MathCache* getMathCache(JSContext* cx) {
         return mathCache_ ? mathCache_ : createMathCache(cx);
     }
     js::MathCache* maybeGetMathCache() {
         return mathCache_;
     }
+    js::SharedImmutableStringsCache& sharedImmutableStrings() {
+        return parentRuntime ? parentRuntime->sharedImmutableStrings() : sharedImmutableStrings_;
+    }
 
     js::GSNCache        gsnCache;
     js::ScopeCoordinateNameCache scopeCoordinateNameCache;
     js::NewObjectCache  newObjectCache;
     js::NativeIterCache nativeIterCache;
     js::UncompressedSourceCache uncompressedSourceCache;
     js::EvalCache       evalCache;
     js::LazyScriptCache lazyScriptCache;
 
-    js::CompressedSourceSet compressedSourceSet;
-
     // Pool of maps used during parse/emit. This may be modified by threads
     // with an ExclusiveContext and requires a lock. Active compilations
     // prevent the pool from being purged during GCs.
   private:
     js::frontend::ParseMapPool parseMapPool_;
     unsigned activeCompilations_;
   public:
     js::frontend::ParseMapPool& parseMapPool() {
new file mode 100644
--- /dev/null
+++ b/js/src/vm/SharedImmutableStringsCache.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#include "vm/SharedImmutableStringsCache.h"
+
+#include "jsstr.h"
+
+namespace js {
+
+SharedImmutableString::~SharedImmutableString() {
+    MOZ_ASSERT(!!cache_ == !!chars_);
+    if (!cache_)
+        return;
+
+    MOZ_ASSERT(mozilla::HashString(chars(), length()) == hash_);
+    SharedImmutableStringsCache::Hasher::Lookup lookup(chars(), length());
+
+    auto locked = cache_->set_.lock();
+    auto entry = locked->lookup(lookup);
+
+    MOZ_ASSERT(entry);
+    MOZ_ASSERT(entry->refcount > 0);
+
+    entry->refcount--;
+    if (entry->refcount == 0)
+        locked->remove(entry);
+}
+
+SharedImmutableString
+SharedImmutableString::clone() const
+{
+    auto clone = cache_->getOrCreate(chars(), length(), [&]() {
+        MOZ_CRASH("Should not need to create an owned version, as this string is already in "
+                  "the cache");
+        return nullptr;
+    });
+    MOZ_ASSERT(clone.isSome());
+    return SharedImmutableString(mozilla::Move(*clone));
+}
+
+SharedImmutableTwoByteString
+SharedImmutableTwoByteString::clone() const
+{
+    return SharedImmutableTwoByteString(string_.clone());
+}
+
+MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableString>
+SharedImmutableStringsCache::getOrCreate(OwnedChars&& chars, size_t length)
+{
+    OwnedChars owned(mozilla::Move(chars));
+    return getOrCreate(owned.get(), length, [&]() { return mozilla::Move(owned); });
+}
+
+MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableString>
+SharedImmutableStringsCache::getOrCreate(const char* chars, size_t length)
+{
+    return getOrCreate(chars, length, [&]() { return DuplicateString(chars, length); });
+}
+
+MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableTwoByteString>
+SharedImmutableStringsCache::getOrCreate(OwnedTwoByteChars&& chars, size_t length)
+{
+    OwnedTwoByteChars owned(mozilla::Move(chars));
+    return getOrCreate(owned.get(), length, [&]() { return mozilla::Move(owned); });
+}
+
+MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableTwoByteString>
+SharedImmutableStringsCache::getOrCreate(const char16_t* chars, size_t length)
+{
+    return getOrCreate(chars, length, [&]() { return DuplicateString(chars, length); });
+}
+
+} // namespace js
new file mode 100644
--- /dev/null
+++ b/js/src/vm/SharedImmutableStringsCache.h
@@ -0,0 +1,430 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#ifndef vm_SharedImmutableStringsCache_h
+#define vm_SharedImmutableStringsCache_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+
+#include <cstring>
+#include <new> // for placement new
+
+#include "jsstr.h"
+
+#include "js/HashTable.h"
+#include "js/Utility.h"
+
+#include "threading/ExclusiveData.h"
+
+namespace js {
+
+class SharedImmutableStringsCache;
+class SharedImmutableTwoByteString;
+
+/**
+ * The `SharedImmutableString` class holds a reference to a `const char*` string
+ * from the `SharedImmutableStringsCache` and releases the reference upon
+ * destruction.
+ */
+class SharedImmutableString
+{
+    friend class SharedImmutableStringsCache;
+    friend class SharedImmutableTwoByteString;
+
+    // Never nullptr in a live instance. May be nullptr if this instance has
+    // been moved from.
+    SharedImmutableStringsCache* cache_;
+    const char* chars_;
+    size_t length_;
+#ifdef DEBUG
+    HashNumber hash_;
+#endif
+
+    SharedImmutableString(SharedImmutableStringsCache* cache, const char* chars, size_t length)
+      : cache_(cache)
+      , chars_(chars)
+      , length_(length)
+#ifdef DEBUG
+      , hash_(mozilla::HashString(chars, length))
+#endif
+    {
+        MOZ_ASSERT(cache && chars);
+    }
+
+  public:
+    /**
+     * `SharedImmutableString`s are move-able. It is an error to use a
+     * `SharedImmutableString` after it has been moved.
+     */
+    SharedImmutableString(SharedImmutableString&& rhs)
+      : cache_(rhs.cache_)
+      , chars_(rhs.chars_)
+      , length_(rhs.length_)
+#ifdef DEBUG
+      , hash_(mozilla::HashString(rhs.chars_, rhs.length_))
+#endif
+    {
+        MOZ_ASSERT(this != &rhs, "self move not allowed");
+        MOZ_ASSERT(rhs.cache_ && rhs.chars_);
+        MOZ_ASSERT(rhs.hash_ == hash_);
+
+        rhs.cache_ = nullptr;
+        rhs.chars_ = nullptr;
+    }
+    SharedImmutableString& operator=(SharedImmutableString&& rhs) {
+        this->~SharedImmutableString();
+        new (this) SharedImmutableString(mozilla::Move(rhs));
+        return *this;
+    }
+
+    /**
+     * Create another shared reference to the underlying string.
+     */
+    SharedImmutableString clone() const;
+
+    ~SharedImmutableString();
+
+    /**
+     * Get a raw pointer to the underlying string. It is only safe to use the
+     * resulting pointer while this `SharedImmutableString` exists.
+     */
+    const char* chars() const {
+        MOZ_ASSERT(cache_ && chars_);
+        return chars_;
+    }
+
+    /**
+     * Get the length of the underlying string.
+     */
+    size_t length() const {
+        MOZ_ASSERT(cache_ && chars_);
+        return length_;
+    }
+};
+
+/**
+ * The `SharedImmutableTwoByteString` class holds a reference to a `const
+ * char16_t*` string from the `SharedImmutableStringsCache` and releases the
+ * reference upon destruction.
+ */
+class SharedImmutableTwoByteString
+{
+    friend class SharedImmutableStringsCache;
+
+    // If a `char*` string and `char16_t*` string happen to have the same bytes,
+    // the bytes will be shared but handed out as different types.
+    SharedImmutableString string_;
+
+    explicit SharedImmutableTwoByteString(SharedImmutableString&& string)
+      : string_(mozilla::Move(string))
+    { }
+
+    SharedImmutableTwoByteString(SharedImmutableStringsCache* cache, const char* chars, size_t length)
+      : string_(cache, chars, length)
+    {
+        MOZ_ASSERT(length % sizeof(char16_t) == 0);
+    }
+
+  public:
+    /**
+     * `SharedImmutableTwoByteString`s are move-able. It is an error to use a
+     * `SharedImmutableTwoByteString` after it has been moved.
+     */
+    SharedImmutableTwoByteString(SharedImmutableTwoByteString&& rhs)
+      : string_(mozilla::Move(rhs.string_))
+    {
+        MOZ_ASSERT(this != &rhs, "self move not allowed");
+    }
+    SharedImmutableTwoByteString& operator=(SharedImmutableTwoByteString&& rhs) {
+        this->~SharedImmutableTwoByteString();
+        new (this) SharedImmutableTwoByteString(mozilla::Move(rhs));
+        return *this;
+    }
+
+    /**
+     * Create another shared reference to the underlying string.
+     */
+    SharedImmutableTwoByteString clone() const;
+
+    /**
+     * Get a raw pointer to the underlying string. It is only safe to use the
+     * resulting pointer while this `SharedImmutableTwoByteString` exists.
+     */
+    const char16_t* chars() const { return reinterpret_cast<const char16_t*>(string_.chars()); }
+
+    /**
+     * Get the length of the underlying string.
+     */
+    size_t length() const { return string_.length() / sizeof(char16_t); }
+};
+
+/**
+ * The `SharedImmutableStringsCache` allows for safely sharing and deduplicating
+ * immutable strings (either `const char*` or `const char16_t*`) between
+ * threads.
+ *
+ * The locking mechanism is dead-simple and coarse grained: a single lock guards
+ * all of the internal table itself, the table's entries, and the entries'
+ * reference counts. It is only safe to perform any mutation on the cache or any
+ * data stored within the cache when this lock is acquired.
+ */
+class SharedImmutableStringsCache
+{
+    friend class SharedImmutableString;
+    struct Hasher;
+
+  public:
+    using OwnedChars = mozilla::UniquePtr<char[], JS::FreePolicy>;
+    using OwnedTwoByteChars = mozilla::UniquePtr<char16_t[], JS::FreePolicy>;
+
+    /**
+     * Get the canonical, shared, and de-duplicated version of the given `const
+     * char*` string. If such a string does not exist, call `intoOwnedChars` and
+     * add the string it returns to the cache.
+     *
+     * `intoOwnedChars` must create an owned version of the given string, and
+     * must have one of the following types:
+     *
+     *     mozilla::UniquePtr<char[], JS::FreePolicy>   intoOwnedChars();
+     *     mozilla::UniquePtr<char[], JS::FreePolicy>&& intoOwnedChars();
+     *
+     * It can be used by callers to elide a copy of the string when it is safe
+     * to give up ownership of the lookup string to the cache. It must return a
+     * `nullptr` on failure.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    template <typename IntoOwnedChars>
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableString>
+    getOrCreate(const char* chars, size_t length, IntoOwnedChars intoOwnedChars) {
+        Hasher::Lookup lookup(chars, length);
+
+        auto locked = set_.lock();
+        if (!locked->initialized() && !locked->init())
+            return mozilla::Nothing();
+
+        auto entry = locked->lookupForAdd(lookup);
+        if (!entry) {
+            OwnedChars ownedChars(intoOwnedChars());
+            if (!ownedChars)
+                return mozilla::Nothing();
+            MOZ_ASSERT(ownedChars.get() == chars ||
+                       memcmp(ownedChars.get(), chars, length) == 0);
+            StringBox box(mozilla::Move(ownedChars), length);
+            if (!locked->add(entry, mozilla::Move(box)))
+                return mozilla::Nothing();
+        }
+
+        MOZ_ASSERT(entry);
+        entry->refcount++;
+        return mozilla::Some(SharedImmutableString(this, entry->chars(),
+                                                   entry->length()));
+    }
+
+    /**
+     * Take ownership of the given `chars` and return the canonical, shared and
+     * de-duplicated version.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableString>
+    getOrCreate(OwnedChars&& chars, size_t length);
+
+    /**
+     * Do not take ownership of the given `chars`. Return the canonical, shared
+     * and de-duplicated version. If there is no extant shared version of
+     * `chars`, make a copy and insert it into the cache.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableString>
+    getOrCreate(const char* chars, size_t length);
+
+    /**
+     * Get the canonical, shared, and de-duplicated version of the given `const
+     * char16_t*` string. If such a string does not exist, call `intoOwnedChars`
+     * and add the string it returns to the cache.
+     *
+     * `intoOwnedTwoByteChars` must create an owned version of the given string,
+     * and must have one of the following types:
+     *
+     *     mozilla::UniquePtr<char16_t[], JS::FreePolicy>   intoOwnedTwoByteChars();
+     *     mozilla::UniquePtr<char16_t[], JS::FreePolicy>&& intoOwnedTwoByteChars();
+     *
+     * It can be used by callers to elide a copy of the string when it is safe
+     * to give up ownership of the lookup string to the cache. It must return a
+     * `nullptr` on failure.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    template <typename IntoOwnedTwoByteChars>
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableTwoByteString>
+    getOrCreate(const char16_t* chars, size_t length, IntoOwnedTwoByteChars intoOwnedTwoByteChars) {
+        Hasher::Lookup lookup(chars, length);
+
+        auto locked = set_.lock();
+        if (!locked->initialized() && !locked->init())
+            return mozilla::Nothing();
+
+        auto entry = locked->lookupForAdd(lookup);
+        if (!entry) {
+            OwnedTwoByteChars ownedTwoByteChars(intoOwnedTwoByteChars());
+            if (!ownedTwoByteChars)
+                return mozilla::Nothing();
+            MOZ_ASSERT(ownedTwoByteChars.get() == chars ||
+                       memcmp(ownedTwoByteChars.get(), chars, length * sizeof(char16_t)) == 0);
+            OwnedChars ownedChars(reinterpret_cast<char*>(ownedTwoByteChars.release()));
+            StringBox box(mozilla::Move(ownedChars), length * sizeof(char16_t));
+            if (!locked->add(entry, mozilla::Move(box)))
+                return mozilla::Nothing();
+        }
+
+        MOZ_ASSERT(entry);
+        entry->refcount++;
+        return mozilla::Some(SharedImmutableTwoByteString(this, entry->chars(),
+                                                          entry->length()));
+    }
+
+    /**
+     * Take ownership of the given `chars` and return the canonical, shared and
+     * de-duplicated version.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableTwoByteString>
+    getOrCreate(OwnedTwoByteChars&& chars, size_t length);
+
+    /**
+     * Do not take ownership of the given `chars`. Return the canonical, shared
+     * and de-duplicated version. If there is no extant shared version of
+     * `chars`, then make a copy and insert it into the cache.
+     *
+     * On success, `Some` is returned. In the case of OOM failure, `Nothing` is
+     * returned.
+     */
+    MOZ_WARN_UNUSED_RESULT mozilla::Maybe<SharedImmutableTwoByteString>
+    getOrCreate(const char16_t* chars, size_t length);
+
+    SharedImmutableStringsCache()
+      : set_(Set())
+    { }
+
+    size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const {
+        size_t n = 0;
+
+        auto locked = set_.lock();
+        if (!locked->initialized())
+            return n;
+
+        // Size of the table.
+        n += locked->sizeOfExcludingThis(mallocSizeOf);
+
+        // Sizes of the strings.
+        for (auto r = locked->all(); !r.empty(); r.popFront())
+            n += mallocSizeOf(r.front().chars());
+
+        return n;
+    }
+
+  private:
+    class StringBox
+    {
+        OwnedChars chars_;
+        size_t length_;
+
+      public:
+        mutable size_t refcount;
+
+        StringBox(OwnedChars&& chars, size_t length)
+          : chars_(mozilla::Move(chars))
+          , length_(length)
+          , refcount(0)
+        {
+            MOZ_ASSERT(chars_);
+        }
+
+        StringBox(StringBox&& rhs)
+          : chars_(mozilla::Move(rhs.chars_))
+          , length_(rhs.length_)
+          , refcount(rhs.refcount)
+        {
+            MOZ_ASSERT(this != &rhs, "self move not allowed");
+            rhs.refcount = 0;
+        }
+
+        ~StringBox() {
+            MOZ_ASSERT(refcount == 0);
+        }
+
+        const char* chars() const { return chars_.get(); }
+        size_t length() const { return length_; }
+    };
+
+    struct Hasher
+    {
+        /**
+         * A structure used when querying for a `const char*` string in the cache.
+         */
+        class Lookup
+        {
+            friend struct Hasher;
+
+            const char* chars_;
+            size_t length_;
+
+          public:
+            Lookup(const char* chars, size_t length)
+              : chars_(chars)
+              , length_(length)
+            {
+                MOZ_ASSERT(chars_);
+            }
+
+            explicit Lookup(const char* chars)
+              : Lookup(chars, strlen(chars))
+            { }
+
+            Lookup(const char16_t* chars, size_t length)
+              : Lookup(reinterpret_cast<const char*>(chars), length * sizeof(char16_t))
+            { }
+
+            explicit Lookup(const char16_t* chars)
+              : Lookup(chars, js_strlen(chars))
+            { }
+        };
+
+        static HashNumber hash(const Lookup& lookup) {
+            MOZ_ASSERT(lookup.chars_);
+            return mozilla::HashString(lookup.chars_, lookup.length_);
+        }
+
+        static bool match(const StringBox& key, const Lookup& lookup) {
+            MOZ_ASSERT(lookup.chars_);
+            MOZ_ASSERT(key.chars());
+
+            if (key.length() != lookup.length_)
+                return false;
+
+            if (key.chars() == lookup.chars_)
+                return true;
+
+            return memcmp(key.chars(), lookup.chars_, key.length()) == 0;
+        }
+    };
+
+    using Set = HashSet<StringBox, Hasher, SystemAllocPolicy>;
+    ExclusiveData<Set> set_;
+};
+
+} // namespace js
+
+#endif // vm_SharedImmutableStringsCache_h
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -2412,28 +2412,16 @@ ReportCompartmentStats(const JS::Compart
 }
 
 static nsresult
 ReportScriptSourceStats(const ScriptSourceInfo& scriptSourceInfo,
                         const nsACString& path,
                         nsIHandleReportCallback* cb, nsISupports* closure,
                         size_t& rtTotal)
 {
-    if (scriptSourceInfo.compressed > 0) {
-        RREPORT_BYTES(path + NS_LITERAL_CSTRING("compressed"),
-            KIND_HEAP, scriptSourceInfo.compressed,
-            "Compressed JavaScript source code.");
-    }
-
-    if (scriptSourceInfo.uncompressed > 0) {
-        RREPORT_BYTES(path + NS_LITERAL_CSTRING("uncompressed"),
-            KIND_HEAP, scriptSourceInfo.uncompressed,
-            "Uncompressed JavaScript source code.");
-    }
-
     if (scriptSourceInfo.misc > 0) {
         RREPORT_BYTES(path + NS_LITERAL_CSTRING("misc"),
             KIND_HEAP, scriptSourceInfo.misc,
             "Miscellaneous data relating to JavaScript source code.");
     }
 
     return NS_OK;
 }
@@ -2494,24 +2482,24 @@ ReportJSRuntimeExplicitTreeStats(const J
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/interpreter-stack"),
         KIND_HEAP, rtStats.runtime.interpreterStack,
         "JS interpreter frames.");
 
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/math-cache"),
         KIND_HEAP, rtStats.runtime.mathCache,
         "The math cache.");
 
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/shared-immutable-strings-cache"),
+        KIND_HEAP, rtStats.runtime.sharedImmutableStringsCache,
+        "Immutable strings (such as JS scripts' source text) shared across all JSRuntimes.");
+
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/uncompressed-source-cache"),
         KIND_HEAP, rtStats.runtime.uncompressedSourceCache,
         "The uncompressed source code cache.");
 
-    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/compressed-source-sets"),
-        KIND_HEAP, rtStats.runtime.compressedSourceSet,
-        "The table indexing compressed source code in the runtime.");
-
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/script-data"),
         KIND_HEAP, rtStats.runtime.scriptData,
         "The table holding script data shared in the runtime.");
 
     nsCString nonNotablePath =
         rtPath + nsPrintfCString("runtime/script-sources/source(scripts=%d, <non-notable files>)/",
                                  rtStats.runtime.scriptSourceInfo.numScripts);