Bug 1493441 - Allow ScriptSource to store UTF-8 script data in addition to UTF-16 script data (but don't create any UTF-8-backed ScriptSources yet). r=tcampbell
authorJeff Walden <jwalden@mit.edu>
Sat, 29 Sep 2018 17:28:31 -0400
changeset 441407 2ec2641a20ae953f6f6119777acb44c580310943
parent 441373 5b5648ea1b5b33d8406a0a78661dd7c86f4cd7b0
child 441408 ff4002836b9b87c8bcf0db5023a2147b33d7ac67
push id34863
push userebalazs@mozilla.com
push dateTue, 16 Oct 2018 09:31:43 +0000
treeherdermozilla-central@9079bbe83718 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstcampbell
bugs1493441
milestone64.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 1493441 - Allow ScriptSource to store UTF-8 script data in addition to UTF-16 script data (but don't create any UTF-8-backed ScriptSources yet). r=tcampbell
dom/script/ScriptLoader.cpp
js/src/vm/HelperThreads.h
js/src/vm/JSFunction.cpp
js/src/vm/JSScript.cpp
js/src/vm/JSScript.h
js/src/vm/SharedImmutableStringsCache.h
mfbt/RecordReplay.cpp
mfbt/RecordReplay.h
parser/html/nsHtml5StreamParser.cpp
toolkit/recordreplay/ipc/JSControl.cpp
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -2407,18 +2407,18 @@ ScriptLoader::EvaluateScript(ScriptLoadR
                                               aRequest->ScriptBinASTData().length(),
                                               &script);
               } else {
                 MOZ_ASSERT(aRequest->IsTextSource());
                 auto srcBuf = GetScriptSource(cx, aRequest);
 
                 if (srcBuf) {
                   if (recordreplay::IsRecordingOrReplaying()) {
-                    recordreplay::NoteContentParse(this, options.filename(), "application/javascript",
-                                                   srcBuf->get(), srcBuf->length());
+                    recordreplay::NoteContentParse16(this, options.filename(), "application/javascript",
+                                                     srcBuf->get(), srcBuf->length());
                   }
                   rv = exec.CompileAndExec(options, *srcBuf, &script);
                 } else {
                   rv = NS_ERROR_OUT_OF_MEMORY;
                 }
               }
             }
           }
--- a/js/src/vm/HelperThreads.h
+++ b/js/src/vm/HelperThreads.h
@@ -843,16 +843,25 @@ class SourceCompressionTask
     bool shouldCancel() const {
         // If the refcount is exactly 1, then nothing else is holding on to the
         // ScriptSource, so no reason to compress it and we should cancel the task.
         return sourceHolder_.get()->refs == 1;
     }
 
     void work();
     void complete();
+
+  private:
+    struct PerformTaskWork;
+    friend struct PerformTaskWork;
+
+    // The work algorithm, aware whether it's compressing one-byte UTF-8 source
+    // text or UTF-16, for CharT either Utf8Unit or char16_t.  Invoked by
+    // work() after doing a type-test of the ScriptSource*.
+    template<typename CharT> void workEncodingSpecific();
 };
 
 // A PromiseHelperTask is an OffThreadPromiseTask that executes a single job on
 // a helper thread. Derived classes do their helper-thread work by implementing
 // execute().
 struct PromiseHelperTask : OffThreadPromiseTask
 {
     PromiseHelperTask(JSContext* cx, Handle<PromiseObject*> promise)
--- a/js/src/vm/JSFunction.cpp
+++ b/js/src/vm/JSFunction.cpp
@@ -9,16 +9,17 @@
  */
 
 #include "vm/JSFunction-inl.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Range.h"
+#include "mozilla/Utf8.h"
 
 #include <string.h>
 
 #include "jsapi.h"
 #include "jstypes.h"
 
 #include "builtin/Array.h"
 #include "builtin/Eval.h"
@@ -60,16 +61,17 @@
 #include "vm/Stack-inl.h"
 
 using namespace js;
 
 using mozilla::ArrayLength;
 using mozilla::CheckedInt;
 using mozilla::Maybe;
 using mozilla::Some;
+using mozilla::Utf8Unit;
 
 using JS::AutoStableStringChars;
 using JS::CompileOptions;
 using JS::SourceBufferHolder;
 
 static bool
 fun_enumerate(JSContext* cx, HandleObject obj)
 {
@@ -1798,29 +1800,46 @@ JSFunction::createScriptForLazilyInterpr
 #else
             MOZ_CRASH("Trying to delazify BinAST function in non-BinAST build");
 #endif /*JS_BUILD_BINAST */
         } else {
             MOZ_ASSERT(lazy->scriptSource()->hasSourceText());
 
             // Parse and compile the script from source.
             UncompressedSourceCache::AutoHoldEntry holder;
-            ScriptSource::PinnedChars chars(cx, lazy->scriptSource(), holder,
-                                            lazy->sourceStart(), lazyLength);
-            if (!chars.get()) {
-                return false;
-            }
-
-            if (!frontend::CompileLazyFunction(cx, lazy, chars.get(), lazyLength)) {
-		// The frontend shouldn't fail after linking the function and the
-		// non-lazy script together.
-                MOZ_ASSERT(fun->isInterpretedLazy());
-                MOZ_ASSERT(fun->lazyScript() == lazy);
-                MOZ_ASSERT(!lazy->hasScript());
-                return false;
+
+            if (lazy->scriptSource()->hasSourceType<Utf8Unit>()) {
+                // UTF-8 source text.
+                ScriptSource::PinnedChars<Utf8Unit> chars(cx, lazy->scriptSource(), holder,
+                                                          lazy->sourceStart(), lazyLength);
+                if (!chars.get()) {
+                    return false;
+                }
+
+                // XXX There are no UTF-8 ScriptSources now, so just crash so this
+                //     gets filled in later.
+                MOZ_CRASH("UTF-8 lazy function compilation not implemented yet");
+            } else {
+                MOZ_ASSERT(lazy->scriptSource()->hasSourceType<char16_t>());
+
+                // UTF-16 source text.
+                ScriptSource::PinnedChars<char16_t> chars(cx, lazy->scriptSource(), holder,
+                                                          lazy->sourceStart(), lazyLength);
+                if (!chars.get()) {
+                    return false;
+                }
+
+                if (!frontend::CompileLazyFunction(cx, lazy, chars.get(), lazyLength)) {
+                    // The frontend shouldn't fail after linking the function and the
+                    // non-lazy script together.
+                    MOZ_ASSERT(fun->isInterpretedLazy());
+                    MOZ_ASSERT(fun->lazyScript() == lazy);
+                    MOZ_ASSERT(!lazy->hasScript());
+                    return false;
+                }
             }
         }
 
         script = fun->nonLazyScript();
 
         // Remember the compiled script on the lazy script itself, in case
         // there are clones of the function still pointing to the lazy script.
         if (!lazy->maybeScript()) {
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -17,16 +17,17 @@
 #include "mozilla/ScopeExit.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/Unused.h"
 #include "mozilla/Vector.h"
 
 #include <algorithm>
 #include <new>
 #include <string.h>
+#include <type_traits>
 #include <utility>
 
 #include "jsapi.h"
 #include "jstypes.h"
 #include "jsutil.h"
 
 #include "frontend/BytecodeCompiler.h"
 #include "frontend/BytecodeEmitter.h"
@@ -68,16 +69,19 @@
 #include "vm/NativeObject-inl.h"
 #include "vm/SharedImmutableStringsCache-inl.h"
 #include "vm/Stack-inl.h"
 
 using namespace js;
 
 using mozilla::Maybe;
 using mozilla::PodCopy;
+using mozilla::PointerRangeSize;
+using mozilla::Utf8AsUnsignedChars;
+using mozilla::Utf8Unit;
 
 using JS::CompileOptions;
 using JS::ReadOnlyCompileOptions;
 using JS::SourceBufferHolder;
 
 template<XDRMode mode>
 XDRResult
 js::XDRScriptConst(XDRState<mode>* xdr, MutableHandleValue vp)
@@ -1561,17 +1565,20 @@ JSScript::loadSource(JSContext* cx, Scri
     char16_t* src = nullptr;
     size_t length;
     if (!cx->runtime()->sourceHook->load(cx, ss->filename(), &src, &length)) {
         return false;
     }
     if (!src) {
         return true;
     }
-    if (!ss->setSource(cx, UniqueTwoByteChars(src), length)) {
+
+    // XXX On-demand source is currently only UTF-16.  Perhaps it should be
+    //     changed to UTF-8, or UTF-8 be allowed in addition to UTF-16?
+    if (!ss->setSource(cx, EntryChars<char16_t>(src), length)) {
         return false;
     }
 
     *worked = true;
     return true;
 }
 
 /* static */ JSFlatString*
@@ -1583,103 +1590,63 @@ JSScript::sourceData(JSContext* cx, Hand
 
 bool
 JSScript::appendSourceDataForToString(JSContext* cx, StringBuffer& buf)
 {
     MOZ_ASSERT(scriptSource()->hasSourceText());
     return scriptSource()->appendSubstring(cx, buf, toStringStart(), toStringEnd());
 }
 
-UncompressedSourceCache::AutoHoldEntry::AutoHoldEntry()
-  : cache_(nullptr), sourceChunk_()
-{
-}
-
-void
-UncompressedSourceCache::AutoHoldEntry::holdEntry(UncompressedSourceCache* cache,
-                                                  const ScriptSourceChunk& sourceChunk)
-{
-    // Initialise the holder for a specific cache and script source. This will
-    // hold on to the cached source chars in the event that the cache is purged.
-    MOZ_ASSERT(!cache_ && !sourceChunk_.valid() && !charsToFree_);
-    cache_ = cache;
-    sourceChunk_ = sourceChunk;
-}
-
-void
-UncompressedSourceCache::AutoHoldEntry::holdChars(UniqueTwoByteChars chars)
-{
-    MOZ_ASSERT(!cache_ && !sourceChunk_.valid() && !charsToFree_);
-    charsToFree_ = std::move(chars);
-}
-
-void
-UncompressedSourceCache::AutoHoldEntry::deferDelete(UniqueTwoByteChars chars)
-{
-    // Take ownership of source chars now the cache is being purged. Remove our
-    // reference to the ScriptSource which might soon be destroyed.
-    MOZ_ASSERT(cache_ && sourceChunk_.valid() && !charsToFree_);
-    cache_ = nullptr;
-    sourceChunk_ = ScriptSourceChunk();
-    charsToFree_ = std::move(chars);
-}
-
-UncompressedSourceCache::AutoHoldEntry::~AutoHoldEntry()
-{
-    if (cache_) {
-        MOZ_ASSERT(sourceChunk_.valid());
-        cache_->releaseEntry(*this);
-    }
-}
-
 void
 UncompressedSourceCache::holdEntry(AutoHoldEntry& holder, const ScriptSourceChunk& ssc)
 {
     MOZ_ASSERT(!holder_);
     holder.holdEntry(this, ssc);
     holder_ = &holder;
 }
 
 void
 UncompressedSourceCache::releaseEntry(AutoHoldEntry& holder)
 {
     MOZ_ASSERT(holder_ == &holder);
     holder_ = nullptr;
 }
 
-const char16_t*
+template<typename CharT>
+const CharT*
 UncompressedSourceCache::lookup(const ScriptSourceChunk& ssc, AutoHoldEntry& holder)
 {
     MOZ_ASSERT(!holder_);
+    MOZ_ASSERT(ssc.ss->compressedSourceIs<CharT>());
+
     if (!map_) {
         return nullptr;
     }
+
     if (Map::Ptr p = map_->lookup(ssc)) {
         holdEntry(holder, ssc);
-        return p->value().get();
-    }
+        return static_cast<const CharT*>(p->value().get());
+    }
+
     return nullptr;
 }
 
 bool
-UncompressedSourceCache::put(const ScriptSourceChunk& ssc, UniqueTwoByteChars str,
-                             AutoHoldEntry& holder)
+UncompressedSourceCache::put(const ScriptSourceChunk& ssc, SourceData data, AutoHoldEntry& holder)
 {
     MOZ_ASSERT(!holder_);
 
     if (!map_) {
-        UniquePtr<Map> map = MakeUnique<Map>();
-        if (!map) {
+        map_ = MakeUnique<Map>();
+        if (!map_) {
             return false;
         }
-
-        map_ = std::move(map);
-    }
-
-    if (!map_->put(ssc, std::move(str))) {
+    }
+
+    if (!map_->put(ssc, std::move(data))) {
         return false;
     }
 
     holdEntry(holder, ssc);
     return true;
 }
 
 void
@@ -1691,263 +1658,334 @@ UncompressedSourceCache::purge()
 
     for (Map::Range r = map_->all(); !r.empty(); r.popFront()) {
         if (holder_ && r.front().key() == holder_->sourceChunk()) {
             holder_->deferDelete(std::move(r.front().value()));
             holder_ = nullptr;
         }
     }
 
-    map_.reset();
+    map_ = nullptr;
 }
 
 size_t
 UncompressedSourceCache::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
 {
     size_t n = 0;
     if (map_ && !map_->empty()) {
         n += map_->shallowSizeOfIncludingThis(mallocSizeOf);
         for (Map::Range r = map_->all(); !r.empty(); r.popFront()) {
             n += mallocSizeOf(r.front().value().get());
         }
     }
     return n;
 }
 
-const char16_t*
+template<typename CharT>
+const CharT*
 ScriptSource::chunkChars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
                          size_t chunk)
 {
-    const Compressed& c = data.as<Compressed>();
+    const Compressed<CharT>& c = data.as<Compressed<CharT>>();
 
     ScriptSourceChunk ssc(this, chunk);
-    if (const char16_t* decompressed = cx->caches().uncompressedSourceCache.lookup(ssc, holder)) {
+    if (const CharT* decompressed = cx->caches().uncompressedSourceCache.lookup<CharT>(ssc, holder)) {
         return decompressed;
     }
 
-    size_t totalLengthInBytes = length() * sizeof(char16_t);
+    size_t totalLengthInBytes = length() * sizeof(CharT);
     size_t chunkBytes = Compressor::chunkSize(totalLengthInBytes, chunk);
 
-    MOZ_ASSERT((chunkBytes % sizeof(char16_t)) == 0);
-    const size_t lengthWithNull = (chunkBytes / sizeof(char16_t)) + 1;
-    UniqueTwoByteChars decompressed(js_pod_malloc<char16_t>(lengthWithNull));
+    MOZ_ASSERT((chunkBytes % sizeof(CharT)) == 0);
+    const size_t lengthWithNull = (chunkBytes / sizeof(CharT)) + 1;
+    EntryChars<CharT> decompressed(js_pod_malloc<CharT>(lengthWithNull));
     if (!decompressed) {
         JS_ReportOutOfMemory(cx);
         return nullptr;
     }
 
-    if (!DecompressStringChunk((const unsigned char*) c.raw.chars(),
+    // Compression treats input and output memory as plain ol' bytes. These
+    // reinterpret_cast<>s accord exactly with that.
+    if (!DecompressStringChunk(reinterpret_cast<const unsigned char*>(c.raw.chars()),
                                chunk,
                                reinterpret_cast<unsigned char*>(decompressed.get()),
                                chunkBytes))
     {
         JS_ReportOutOfMemory(cx);
         return nullptr;
     }
 
-    decompressed[lengthWithNull - 1] = '\0';
-
-    const char16_t* ret = decompressed.get();
-    if (!cx->caches().uncompressedSourceCache.put(ssc, std::move(decompressed), holder)) {
+    decompressed[lengthWithNull - 1] = CharT('\0');
+
+    const CharT* ret = decompressed.get();
+    if (!cx->caches().uncompressedSourceCache.put(ssc, ToSourceData(std::move(decompressed)),
+                                                  holder))
+    {
         JS_ReportOutOfMemory(cx);
         return nullptr;
     }
     return ret;
 }
 
-ScriptSource::PinnedChars::PinnedChars(JSContext* cx, ScriptSource* source,
-                                       UncompressedSourceCache::AutoHoldEntry& holder,
-                                       size_t begin, size_t len)
-  : stack_(nullptr),
-    prev_(nullptr),
-    source_(source)
+template<typename CharT>
+void
+ScriptSource::movePendingCompressedSource()
 {
-    chars_ = source->chars(cx, holder, begin, len);
-    if (chars_) {
-        stack_ = &source->pinnedCharsStack_;
-        prev_ = *stack_;
-        *stack_ = this;
-    }
+    if (pendingCompressed_.empty()) {
+        return;
+    }
+
+    Compressed<CharT>& pending = pendingCompressed_.ref<Compressed<CharT>>();
+
+    MOZ_ASSERT(!hasCompressedSource());
+    MOZ_ASSERT_IF(hasUncompressedSource(),
+                  pending.uncompressedLength == length());
+
+    data = SourceType(std::move(pending));
+    pendingCompressed_.destroy();
 }
 
-ScriptSource::PinnedChars::~PinnedChars()
+template<typename CharT>
+ScriptSource::PinnedChars<CharT>::~PinnedChars()
 {
     if (chars_) {
         MOZ_ASSERT(*stack_ == this);
         *stack_ = prev_;
         if (!prev_) {
-            source_->movePendingCompressedSource();
+            source_->movePendingCompressedSource<CharT>();
         }
     }
 }
 
-void
-ScriptSource::movePendingCompressedSource()
-{
-    if (!pendingCompressed_) {
-        return;
-    }
-
-    MOZ_ASSERT(data.is<Missing>() || data.is<Uncompressed>());
-    MOZ_ASSERT_IF(data.is<Uncompressed>(),
-                  data.as<Uncompressed>().string.length() ==
-                  pendingCompressed_->uncompressedLength);
-
-    data = SourceType(Compressed(std::move(pendingCompressed_->raw),
-                                 pendingCompressed_->uncompressedLength));
-    pendingCompressed_ = mozilla::Nothing();
-}
-
-const char16_t*
+template<typename CharT>
+const CharT*
 ScriptSource::chars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
                     size_t begin, size_t len)
 {
+    MOZ_ASSERT(begin <= length());
     MOZ_ASSERT(begin + len <= length());
 
-    if (data.is<Uncompressed>()) {
-        const char16_t* chars = data.as<Uncompressed>().string.chars();
+    if (data.is<Uncompressed<CharT>>()) {
+        const CharT* chars = data.as<Uncompressed<CharT>>().chars();
         if (!chars) {
             return nullptr;
         }
         return chars + begin;
     }
 
     if (data.is<Missing>()) {
         MOZ_CRASH("ScriptSource::chars() on ScriptSource with SourceType = Missing");
     }
 
-    MOZ_ASSERT(data.is<Compressed>());
+    MOZ_ASSERT(data.is<Compressed<CharT>>());
 
     // Determine which chunk(s) we are interested in, and the offsets within
     // these chunks.
     size_t firstChunk, lastChunk;
     size_t firstChunkOffset, lastChunkOffset;
     MOZ_ASSERT(len > 0);
-    Compressor::toChunkOffset(begin * sizeof(char16_t), &firstChunk, &firstChunkOffset);
-    Compressor::toChunkOffset((begin + len - 1) * sizeof(char16_t), &lastChunk, &lastChunkOffset);
-
-    MOZ_ASSERT(firstChunkOffset % sizeof(char16_t) == 0);
-    size_t firstChar = firstChunkOffset / sizeof(char16_t);
+    Compressor::toChunkOffset(begin * sizeof(CharT), &firstChunk, &firstChunkOffset);
+    Compressor::toChunkOffset((begin + len - 1) * sizeof(CharT), &lastChunk, &lastChunkOffset);
+
+    MOZ_ASSERT(firstChunkOffset % sizeof(CharT) == 0);
+    size_t firstChar = firstChunkOffset / sizeof(CharT);
 
     if (firstChunk == lastChunk) {
-        const char16_t* chars = chunkChars(cx, holder, firstChunk);
+        const CharT* chars = chunkChars<CharT>(cx, holder, firstChunk);
         if (!chars) {
             return nullptr;
         }
+
         return chars + firstChar;
     }
 
     // We need multiple chunks. Allocate a (null-terminated) buffer to hold
     // |len| chars and copy uncompressed chars from the chunks into it. We use
     // chunkChars() so we benefit from chunk caching by UncompressedSourceCache.
 
     MOZ_ASSERT(firstChunk < lastChunk);
 
     size_t lengthWithNull = len + 1;
-    UniqueTwoByteChars decompressed(js_pod_malloc<char16_t>(lengthWithNull));
+    EntryChars<CharT> decompressed(js_pod_malloc<CharT>(lengthWithNull));
     if (!decompressed) {
         JS_ReportOutOfMemory(cx);
         return nullptr;
     }
 
-    size_t totalLengthInBytes = length() * sizeof(char16_t);
-    char16_t* cursor = decompressed.get();
+    size_t totalLengthInBytes = length() * sizeof(CharT);
+    CharT* cursor = decompressed.get();
 
     for (size_t i = firstChunk; i <= lastChunk; i++) {
         UncompressedSourceCache::AutoHoldEntry chunkHolder;
-        const char16_t* chars = chunkChars(cx, chunkHolder, i);
+        const CharT* chars = chunkChars<CharT>(cx, chunkHolder, i);
         if (!chars) {
             return nullptr;
         }
 
-        size_t numChars = Compressor::chunkSize(totalLengthInBytes, i) / sizeof(char16_t);
+        size_t numChars = Compressor::chunkSize(totalLengthInBytes, i) / sizeof(CharT);
         if (i == firstChunk) {
             MOZ_ASSERT(firstChar < numChars);
             chars += firstChar;
             numChars -= firstChar;
         } else if (i == lastChunk) {
-            size_t numCharsNew = lastChunkOffset / sizeof(char16_t) + 1;
+            size_t numCharsNew = lastChunkOffset / sizeof(CharT) + 1;
             MOZ_ASSERT(numCharsNew <= numChars);
             numChars = numCharsNew;
         }
         mozilla::PodCopy(cursor, chars, numChars);
         cursor += numChars;
     }
 
-    *cursor++ = '\0';
-    MOZ_ASSERT(size_t(cursor - decompressed.get()) == lengthWithNull);
+    // XXX Bug 1499192: can we remove the null-termination?  It's unclear if
+    //     anyone uses chunk implicit null-termination, chunks can contain
+    //     nulls anyway, and the extra character risks size-class goofs.
+    *cursor++ = CharT('\0');
+    MOZ_ASSERT(PointerRangeSize(decompressed.get(), cursor) == lengthWithNull);
 
     // Transfer ownership to |holder|.
-    const char16_t* ret = decompressed.get();
+    const CharT* ret = decompressed.get();
     holder.holdChars(std::move(decompressed));
     return ret;
 }
 
+template<typename CharT>
+ScriptSource::PinnedChars<CharT>::PinnedChars(JSContext* cx, ScriptSource* source,
+                                              UncompressedSourceCache::AutoHoldEntry& holder,
+                                              size_t begin, size_t len)
+  : PinnedCharsBase(source)
+{
+    MOZ_ASSERT(source->hasSourceType<CharT>(),
+               "must pin chars of source's type");
+
+    chars_ = source->chars<CharT>(cx, holder, begin, len);
+    if (chars_) {
+        stack_ = &source->pinnedCharsStack_;
+        prev_ = *stack_;
+        *stack_ = this;
+    }
+}
+
+template class ScriptSource::PinnedChars<Utf8Unit>;
+template class ScriptSource::PinnedChars<char16_t>;
+
 JSFlatString*
 ScriptSource::substring(JSContext* cx, size_t start, size_t stop)
 {
     MOZ_ASSERT(start <= stop);
+
     size_t len = stop - start;
     UncompressedSourceCache::AutoHoldEntry holder;
-    PinnedChars chars(cx, this, holder, start, len);
+
+    // UTF-8 source text.
+    if (hasSourceType<Utf8Unit>()) {
+        PinnedChars<Utf8Unit> chars(cx, this, holder, start, len);
+        if (!chars.get()) {
+            return nullptr;
+        }
+
+        char* str = SourceTypeTraits<Utf8Unit>::toString(chars.get());
+        return NewStringCopyUTF8N<CanGC>(cx, JS::UTF8Chars(str, len));
+    }
+
+    // UTF-16 source text.
+    PinnedChars<char16_t> chars(cx, this, holder, start, len);
     if (!chars.get()) {
         return nullptr;
     }
+
     return NewStringCopyN<CanGC>(cx, chars.get(), len);
 }
 
 JSFlatString*
 ScriptSource::substringDontDeflate(JSContext* cx, size_t start, size_t stop)
 {
     MOZ_ASSERT(start <= stop);
+
     size_t len = stop - start;
     UncompressedSourceCache::AutoHoldEntry holder;
-    PinnedChars chars(cx, this, holder, start, len);
+
+    // UTF-8 source text.
+    if (hasSourceType<Utf8Unit>()) {
+        PinnedChars<Utf8Unit> chars(cx, this, holder, start, len);
+        if (!chars.get()) {
+            return nullptr;
+        }
+
+        char* str = SourceTypeTraits<Utf8Unit>::toString(chars.get());
+
+        // There doesn't appear to be a non-deflating UTF-8 string creation
+        // function -- but then again, it's not entirely clear how current
+        // callers benefit from non-deflation.
+        return NewStringCopyUTF8N<CanGC>(cx, JS::UTF8Chars(str, len));
+    }
+
+    // UTF-16 source text.
+    PinnedChars<char16_t> chars(cx, this, holder, start, len);
     if (!chars.get()) {
         return nullptr;
     }
+
     return NewStringCopyNDontDeflate<CanGC>(cx, chars.get(), len);
 }
 
 bool
 ScriptSource::appendSubstring(JSContext* cx, StringBuffer& buf, size_t start, size_t stop)
 {
     MOZ_ASSERT(start <= stop);
+
     size_t len = stop - start;
     UncompressedSourceCache::AutoHoldEntry holder;
-    PinnedChars chars(cx, this, holder, start, len);
-    if (!chars.get()) {
+
+    if (hasSourceType<Utf8Unit>()) {
+        MOZ_CRASH("for now");
         return false;
-    }
-    if (len > SourceDeflateLimit && !buf.ensureTwoByteChars()) {
-        return false;
-    }
-    return buf.append(chars.get(), len);
+    } else {
+        PinnedChars<char16_t> chars(cx, this, holder, start, len);
+        if (!chars.get()) {
+            return false;
+        }
+        if (len > SourceDeflateLimit && !buf.ensureTwoByteChars()) {
+            return false;
+        }
+        return buf.append(chars.get(), len);
+    }
 }
 
 JSFlatString*
 ScriptSource::functionBodyString(JSContext* cx)
 {
     MOZ_ASSERT(isFunctionBody());
 
     size_t start = parameterListEnd_ + (sizeof(FunctionConstructorMedialSigils) - 1);
     size_t stop = length() - (sizeof(FunctionConstructorFinalBrace) - 1);
     return substring(cx, start, stop);
 }
 
+template<typename CharT>
+void
+ScriptSource::setSource(typename SourceTypeTraits<CharT>::SharedImmutableString uncompressed)
+{
+    MOZ_ASSERT(data.is<Missing>());
+    data = SourceType(Uncompressed<CharT>(std::move(uncompressed)));
+}
+
+template<typename CharT>
 MOZ_MUST_USE bool
-ScriptSource::setSource(JSContext* cx, UniqueTwoByteChars&& source, size_t length)
+ScriptSource::setSource(JSContext* cx, EntryChars<CharT>&& source, size_t length)
 {
     auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
-    auto deduped = cache.getOrCreate(std::move(source), length);
+
+    auto uniqueChars = SourceTypeTraits<CharT>::toCacheable(std::move(source));
+    auto deduped = cache.getOrCreate(std::move(uniqueChars), length);
     if (!deduped) {
         ReportOutOfMemory(cx);
         return false;
     }
-    setSource(std::move(*deduped));
+
+    setSource<CharT>(std::move(*deduped));
     return true;
 }
 
 #if defined(JS_BUILD_BINAST)
 
 MOZ_MUST_USE bool
 ScriptSource::setBinASTSourceCopy(JSContext* cx, const uint8_t* buf, size_t len)
 {
@@ -1980,27 +2018,21 @@ const uint8_t*
 ScriptSource::binASTSource()
 {
     MOZ_ASSERT(hasBinASTSource());
     return reinterpret_cast<const uint8_t*>(data.as<BinAST>().string.chars());
 }
 
 #endif /* JS_BUILD_BINAST */
 
-void
-ScriptSource::setSource(SharedImmutableTwoByteString&& string)
-{
-    MOZ_ASSERT(data.is<Missing>());
-    data = SourceType(Uncompressed(std::move(string)));
-}
-
 bool
 ScriptSource::tryCompressOffThread(JSContext* cx)
 {
-    if (!data.is<Uncompressed>()) {
+    if (!hasUncompressedSource()) {
+        // This excludes already-compressed, missing, and BinAST source.
         return true;
     }
 
     // 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 there is only one core, then compression will contend with JS
     //    execution (which hurts benchmarketing).
     //
@@ -2034,62 +2066,67 @@ ScriptSource::tryCompressOffThread(JSCon
     auto task = MakeUnique<SourceCompressionTask>(cx->runtime(), this);
     if (!task) {
         ReportOutOfMemory(cx);
         return false;
     }
     return EnqueueOffThreadCompression(cx, std::move(task));
 }
 
+template<typename CharT>
+void
+ScriptSource::setCompressedSource(SharedImmutableString raw, size_t uncompressedLength)
+{
+    MOZ_ASSERT(data.is<Missing>() || hasUncompressedSource());
+    MOZ_ASSERT_IF(hasUncompressedSource(), length() == uncompressedLength);
+
+    if (pinnedCharsStack_) {
+        MOZ_ASSERT(pendingCompressed_.empty());
+        pendingCompressed_.construct<Compressed<CharT>>(std::move(raw), uncompressedLength);
+    } else {
+        data = SourceType(Compressed<CharT>(std::move(raw), uncompressedLength));
+    }
+}
+
+template<typename CharT>
 MOZ_MUST_USE bool
-ScriptSource::setCompressedSource(JSContext* cx, UniqueChars&& raw, size_t rawLength,
+ScriptSource::setCompressedSource(JSContext* cx, UniqueChars&& compressed, size_t rawLength,
                                   size_t sourceLength)
 {
-    MOZ_ASSERT(raw);
+    MOZ_ASSERT(compressed);
+
     auto& cache = cx->zone()->runtimeFromAnyThread()->sharedImmutableStrings();
-    auto deduped = cache.getOrCreate(std::move(raw), rawLength);
+    auto deduped = cache.getOrCreate(std::move(compressed), rawLength);
     if (!deduped) {
         ReportOutOfMemory(cx);
         return false;
     }
-    setCompressedSource(std::move(*deduped), sourceLength);
+
+    setCompressedSource<CharT>(std::move(*deduped), sourceLength);
     return true;
 }
 
-void
-ScriptSource::setCompressedSource(SharedImmutableString&& raw, size_t uncompressedLength)
-{
-    MOZ_ASSERT(data.is<Missing>() || data.is<Uncompressed>());
-    MOZ_ASSERT_IF(data.is<Uncompressed>(),
-                  data.as<Uncompressed>().string.length() == uncompressedLength);
-    if (pinnedCharsStack_) {
-        pendingCompressed_ = mozilla::Some(Compressed(std::move(raw), uncompressedLength));
-    } else {
-        data = SourceType(Compressed(std::move(raw), uncompressedLength));
-    }
-}
-
 bool
 ScriptSource::setSourceCopy(JSContext* cx, SourceBufferHolder& srcBuf)
 {
     MOZ_ASSERT(!hasSourceText());
 
     JSRuntime* runtime = cx->zone()->runtimeFromAnyThread();
     auto& cache = runtime->sharedImmutableStrings();
-    auto deduped = cache.getOrCreate(srcBuf.get(), srcBuf.length(), [&]() {
+    auto deduped = cache.getOrCreate(srcBuf.get(), srcBuf.length(), [&srcBuf]() {
         return srcBuf.ownsChars()
                ? UniqueTwoByteChars(srcBuf.take())
                : DuplicateString(srcBuf.get(), srcBuf.length());
     });
     if (!deduped) {
         ReportOutOfMemory(cx);
         return false;
     }
-    setSource(std::move(*deduped));
-
+
+    setSource<char16_t>(std::move(*deduped));
     return true;
 }
 
 void
 ScriptSource::trace(JSTracer* trc)
 {
 #ifdef JS_BUILD_BINAST
     if (binASTMetadata_) {
@@ -2109,38 +2146,34 @@ reallocUniquePtr(UniqueChars& unique, si
     }
 
     // Since the realloc succeeded, unique is now holding a freed pointer.
     mozilla::Unused << unique.release();
     unique.reset(newPtr);
     return true;
 }
 
+template<typename CharT>
 void
-SourceCompressionTask::work()
+SourceCompressionTask::workEncodingSpecific()
 {
-    if (shouldCancel()) {
-        return;
-    }
-
     ScriptSource* source = sourceHolder_.get();
-    MOZ_ASSERT(source->data.is<ScriptSource::Uncompressed>());
+    MOZ_ASSERT(source->data.is<ScriptSource::Uncompressed<CharT>>());
 
     // Try to keep the maximum memory usage down by only allocating half the
     // size of the string, first.
-    size_t inputBytes = source->length() * sizeof(char16_t);
+    size_t inputBytes = source->length() * sizeof(CharT);
     size_t firstSize = inputBytes / 2;
     UniqueChars compressed(js_pod_malloc<char>(firstSize));
     if (!compressed) {
         return;
     }
 
-    const char16_t* chars = source->data.as<ScriptSource::Uncompressed>().string.chars();
-    Compressor comp(reinterpret_cast<const unsigned char*>(chars),
-                    inputBytes);
+    const CharT* chars = source->data.as<ScriptSource::Uncompressed<CharT>>().chars();
+    Compressor comp(reinterpret_cast<const unsigned char*>(chars), inputBytes);
     if (!comp.init()) {
         return;
     }
 
     comp.setOutput(reinterpret_cast<unsigned char*>(compressed.get()), firstSize);
     bool cont = true;
     bool reallocated = false;
     while (cont) {
@@ -2187,22 +2220,68 @@ SourceCompressionTask::work()
     if (shouldCancel()) {
         return;
     }
 
     auto& strings = runtime_->sharedImmutableStrings();
     resultString_ = strings.getOrCreate(std::move(compressed), totalBytes);
 }
 
+struct SourceCompressionTask::PerformTaskWork
+{
+    SourceCompressionTask* const task_;
+
+    explicit PerformTaskWork(SourceCompressionTask* task)
+      : task_(task)
+    {}
+
+    template<typename CharT>
+    void match(const ScriptSource::Uncompressed<CharT>&) {
+        task_->workEncodingSpecific<CharT>();
+    }
+
+    template<typename T>
+    void match (const T&) {
+        MOZ_CRASH("why are we compressing missing, already-compressed, or "
+                  "BinAST source?");
+    }
+};
+
+void
+ScriptSource::performTaskWork(SourceCompressionTask* task)
+{
+    MOZ_ASSERT(hasUncompressedSource());
+    data.match(SourceCompressionTask::PerformTaskWork(task));
+}
+
+void
+SourceCompressionTask::work()
+{
+    if (shouldCancel()) {
+        return;
+    }
+
+    ScriptSource* source = sourceHolder_.get();
+    MOZ_ASSERT(source->hasUncompressedSource());
+
+    source->performTaskWork(this);
+}
+
+void
+ScriptSource::setCompressedSourceFromTask(SharedImmutableString compressed)
+{
+    data.match(SetCompressedSourceFromTask(this, compressed));
+}
+
 void
 SourceCompressionTask::complete()
 {
-    if (!shouldCancel() && resultString_) {
+    if (!shouldCancel() && resultString_.isSome()) {
         ScriptSource* source = sourceHolder_.get();
-        source->setCompressedSource(std::move(*resultString_), source->length());
+        source->setCompressedSourceFromTask(std::move(*resultString_));
     }
 }
 
 void
 ScriptSource::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                      JS::ScriptSourceInfo* info) const
 {
     info->misc += mallocSizeOf(this) +
@@ -2281,62 +2360,116 @@ ScriptSource::xdrFinalizeEncoder(JS::Tra
     auto cleanup = mozilla::MakeScopeExit([&] {
         xdrEncoder_.reset(nullptr);
     });
 
     XDRResult res = xdrEncoder_->linearize(buffer);
     return res.isOk();
 }
 
+template<typename CharT>
+struct SourceDecoder
+{
+    XDRState<XDR_DECODE>* const xdr_;
+    ScriptSource* const scriptSource_;
+    const uint32_t uncompressedLength_;
+
+  public:
+    SourceDecoder(XDRState<XDR_DECODE>* xdr, ScriptSource* scriptSource,
+                  uint32_t uncompressedLength)
+      : xdr_(xdr),
+        scriptSource_(scriptSource),
+        uncompressedLength_(uncompressedLength)
+    {}
+
+    XDRResult decode() {
+        auto sourceChars =
+            xdr_->cx()->make_pod_array<CharT>(Max<size_t>(uncompressedLength_, 1));
+        if (!sourceChars) {
+            return xdr_->fail(JS::TranscodeResult_Throw);
+        }
+
+        MOZ_TRY(xdr_->codeChars(sourceChars.get(), uncompressedLength_));
+
+        if (!scriptSource_->setSource(xdr_->cx(), std::move(sourceChars),
+                                      uncompressedLength_))
+        {
+            return xdr_->fail(JS::TranscodeResult_Throw);
+        }
+
+        return Ok();
+    }
+};
+
+namespace js {
+
+template<>
+XDRResult
+ScriptSource::xdrUncompressedSource<XDR_DECODE>(XDRState<XDR_DECODE>* xdr,
+                                                uint8_t sourceCharSize,
+                                                uint32_t uncompressedLength)
+{
+    MOZ_ASSERT(sourceCharSize == 1 || sourceCharSize == 2);
+
+    if (sourceCharSize == 1) {
+        SourceDecoder<Utf8Unit> decoder(xdr, this, uncompressedLength);
+        return decoder.decode();
+    }
+
+    SourceDecoder<char16_t> decoder(xdr, this, uncompressedLength);
+    return decoder.decode();
+}
+
+} // namespace js
+
+template<typename CharT>
+struct SourceEncoder
+{
+    XDRState<XDR_ENCODE>* const xdr_;
+    ScriptSource* const source_;
+    const uint32_t uncompressedLength_;
+
+    SourceEncoder(XDRState<XDR_ENCODE>* xdr, ScriptSource* source, uint32_t uncompressedLength)
+      : xdr_(xdr),
+        source_(source),
+        uncompressedLength_(uncompressedLength)
+    {}
+
+    XDRResult encode() {
+        CharT* sourceChars = const_cast<CharT*>(source_->uncompressedData<CharT>());
+
+        return xdr_->codeChars(sourceChars, uncompressedLength_);
+    }
+};
+
+namespace js {
+
+template<>
+XDRResult
+ScriptSource::xdrUncompressedSource<XDR_ENCODE>(XDRState<XDR_ENCODE>* xdr,
+                                                uint8_t sourceCharSize,
+                                                uint32_t uncompressedLength)
+{
+    MOZ_ASSERT(sourceCharSize == 1 || sourceCharSize == 2);
+
+    if (sourceCharSize == 1) {
+        SourceEncoder<Utf8Unit> encoder(xdr, this, uncompressedLength);
+        return encoder.encode();
+    }
+
+    SourceEncoder<char16_t> encoder(xdr, this, uncompressedLength);
+    return encoder.encode();
+}
+
+} // namespace js
+
 template<XDRMode mode>
 XDRResult
 ScriptSource::performXDR(XDRState<mode>* xdr)
 {
-    struct CompressedLengthMatcher
-    {
-        size_t match(Uncompressed&) {
-            // Return 0 for uncompressed source so that |if (compressedLength)|
-            // can be used to distinguish compressed and uncompressed source.
-            return 0;
-        }
-
-        size_t match(Compressed& c) {
-            return c.raw.length();
-        }
-
-        size_t match(BinAST&) {
-            return 0;
-        }
-
-        size_t match(Missing&) {
-            MOZ_CRASH("Missing source data in ScriptSource::performXDR");
-            return 0;
-        }
-    };
-
-    struct RawDataMatcher
-    {
-        void* match(Uncompressed& u) {
-            return (void*) u.string.chars();
-        }
-
-        void* match(Compressed& c) {
-            return (void*) c.raw.chars();
-        }
-
-        void* match(BinAST& b) {
-            return (void*) b.string.chars();
-        }
-
-        void* match(Missing&) {
-            MOZ_CRASH("Missing source data in ScriptSource::performXDR");
-            return nullptr;
-        }
-    };
-
     uint8_t hasSource = hasSourceText();
     MOZ_TRY(xdr->codeUint8(&hasSource));
 
     uint8_t hasBinSource = hasBinASTSource();
     MOZ_TRY(xdr->codeUint8(&hasBinSource));
 
     uint8_t retrievable = sourceRetrievable_;
     MOZ_TRY(xdr->codeUint8(&retrievable));
@@ -2344,78 +2477,83 @@ ScriptSource::performXDR(XDRState<mode>*
 
     if ((hasSource || hasBinSource) && !sourceRetrievable_) {
         uint32_t uncompressedLength = 0;
         if (mode == XDR_ENCODE) {
             uncompressedLength = length();
         }
         MOZ_TRY(xdr->codeUint32(&uncompressedLength));
 
-        // A compressed length of 0 indicates source is uncompressed.
+        // A compressed length of 0 indicates source is uncompressed (or is
+        // BinAST if |hasBinSource|).
         uint32_t compressedLength;
         if (mode == XDR_ENCODE) {
-            CompressedLengthMatcher m;
-            compressedLength = data.match(m);
+            compressedLength = compressedLengthOrZero();
         }
         MOZ_TRY(xdr->codeUint32(&compressedLength));
 
-        if (mode == XDR_DECODE) {
-            if (hasBinSource) {
+        uint8_t srcCharSize;
+        if (mode == XDR_ENCODE) {
+            srcCharSize = sourceCharSize();
+        }
+        MOZ_TRY(xdr->codeUint8(&srcCharSize));
+
+        if (srcCharSize != 1 && srcCharSize != 2) {
+            // Fail in debug, but only soft-fail in release, if the source-char
+            // size is invalid.
+            MOZ_ASSERT_UNREACHABLE("bad XDR source chars size");
+            return xdr->fail(JS::TranscodeResult_Failure_BadDecode);
+        }
+
+        if (hasBinSource) {
+            if (mode == XDR_DECODE) {
 #if defined(JS_BUILD_BINAST)
                 auto bytes =
                     xdr->cx()->template make_pod_array<char>(Max<size_t>(uncompressedLength, 1));
                 if (!bytes) {
                     return xdr->fail(JS::TranscodeResult_Throw);
                 }
                 MOZ_TRY(xdr->codeBytes(bytes.get(), uncompressedLength));
 
                 if (!setBinASTSource(xdr->cx(), std::move(bytes), uncompressedLength)) {
                     return xdr->fail(JS::TranscodeResult_Throw);
                 }
 #else
                 MOZ_ASSERT(mode != XDR_ENCODE);
                 return xdr->fail(JS::TranscodeResult_Throw);
 #endif /* JS_BUILD_BINAST */
-            } else if (compressedLength) {
-                auto bytes =
-                    xdr->cx()->template make_pod_array<char>(Max<size_t>(compressedLength, 1));
+            } else {
+                void* bytes = binASTData();
+                MOZ_TRY(xdr->codeBytes(bytes, uncompressedLength));
+            }
+        } else if (compressedLength) {
+            if (mode == XDR_DECODE) {
+                // Compressed data is always single-byte chars.
+                auto bytes = xdr->cx()->template make_pod_array<char>(compressedLength);
                 if (!bytes) {
                     return xdr->fail(JS::TranscodeResult_Throw);
                 }
                 MOZ_TRY(xdr->codeBytes(bytes.get(), compressedLength));
 
-                if (!setCompressedSource(xdr->cx(), std::move(bytes), compressedLength,
-                                         uncompressedLength))
+                if (!(srcCharSize == 1
+                      ? setCompressedSource<Utf8Unit>(xdr->cx(), std::move(bytes),
+                                                      compressedLength, uncompressedLength)
+                      : setCompressedSource<char16_t>(xdr->cx(), std::move(bytes),
+                                                      compressedLength, uncompressedLength)))
                 {
                     return xdr->fail(JS::TranscodeResult_Throw);
                 }
             } else {
-                auto sourceChars =
-                    xdr->cx()->template make_pod_array<char16_t>(Max<size_t>(uncompressedLength,
-                                                                             1));
-                if (!sourceChars) {
-                    return xdr->fail(JS::TranscodeResult_Throw);
-                }
-                MOZ_TRY(xdr->codeChars(sourceChars.get(), uncompressedLength));
-
-                if (!setSource(xdr->cx(), std::move(sourceChars), uncompressedLength)) {
-                    return xdr->fail(JS::TranscodeResult_Throw);
-                }
+                void* bytes = srcCharSize == 1
+                              ? compressedData<Utf8Unit>()
+                              : compressedData<char16_t>();
+                MOZ_TRY(xdr->codeBytes(bytes, compressedLength));
             }
         } else {
-            if (hasBinSource) {
-                void* bytes = data.match(RawDataMatcher());
-                MOZ_TRY(xdr->codeBytes(bytes, uncompressedLength));
-            } else if (compressedLength) {
-                void* bytes = data.match(RawDataMatcher());
-                MOZ_TRY(xdr->codeBytes(bytes, compressedLength));
-            } else {
-                char16_t* sourceChars = static_cast<char16_t*>(data.match(RawDataMatcher()));
-                MOZ_TRY(xdr->codeChars(sourceChars, uncompressedLength));
-            }
+            MOZ_TRY(xdrUncompressedSource(xdr, srcCharSize, uncompressedLength));
         }
 
         uint8_t hasMetadata = !!binASTMetadata_;
         MOZ_TRY(xdr->codeUint8(&hasMetadata));
         if (hasMetadata) {
 #if defined(JS_BUILD_BINAST)
             uint32_t numBinKinds;
             uint32_t numStrings;
@@ -2546,22 +2684,34 @@ ScriptSource::performXDR(XDRState<mode>*
         }
 
         // Note the content of sources decoded when recording or replaying.
         if (mode == XDR_DECODE &&
             hasSourceText() &&
             mozilla::recordreplay::IsRecordingOrReplaying())
         {
             UncompressedSourceCache::AutoHoldEntry holder;
-            ScriptSource::PinnedChars chars(xdr->cx(), this, holder, 0, length());
-            if (!chars.get()) {
-                return xdr->fail(JS::TranscodeResult_Throw);
+
+            if (hasSourceType<Utf8Unit>()) {
+                // UTF-8 source text.
+                ScriptSource::PinnedChars<Utf8Unit> chars(xdr->cx(), this, holder, 0, length());
+                if (!chars.get()) {
+                    return xdr->fail(JS::TranscodeResult_Throw);
+                }
+                mozilla::recordreplay::NoteContentParse8(this, filename(), "application/javascript",
+                                                         chars.get(), length());
+            } else {
+                // UTF-16 source text.
+                ScriptSource::PinnedChars<char16_t> chars(xdr->cx(), this, holder, 0, length());
+                if (!chars.get()) {
+                    return xdr->fail(JS::TranscodeResult_Throw);
+                }
+                mozilla::recordreplay::NoteContentParse16(this, filename(), "application/javascript",
+                                                          chars.get(), length());
             }
-            mozilla::recordreplay::NoteContentParse(this, filename(), "application/javascript",
-                                                    chars.get(), length());
         }
     }
 
     return Ok();
 }
 
 // Format and return a cx->pod_malloc'ed URL for a generated script like:
 //   {filename} line {lineno} > {introducer}
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -7,30 +7,37 @@
 /* JS script descriptor. */
 
 #ifndef vm_JSScript_h
 #define vm_JSScript_h
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Maybe.h"
+#include "mozilla/MaybeOneOf.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Span.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Utf8.h"
 #include "mozilla/Variant.h"
 
+#include <type_traits> // std::is_same
+#include <utility> // std::move
+
 #include "jstypes.h"
 
 #include "frontend/BinSourceRuntimeSupport.h"
 #include "frontend/NameAnalysisTypes.h"
 #include "gc/Barrier.h"
 #include "gc/Rooting.h"
 #include "jit/IonCode.h"
 #include "js/CompileOptions.h"
 #include "js/UbiNode.h"
 #include "js/UniquePtr.h"
+#include "js/Utility.h"
 #include "vm/BytecodeUtil.h"
 #include "vm/JSAtom.h"
 #include "vm/NativeObject.h"
 #include "vm/Scope.h"
 #include "vm/Shape.h"
 #include "vm/SharedImmutableStringsCache.h"
 #include "vm/Time.h"
 
@@ -290,27 +297,27 @@ using DebugScriptMap = HashMap<JSScript*
                                UniqueDebugScript,
                                DefaultHasher<JSScript*>,
                                SystemAllocPolicy>;
 
 class ScriptSource;
 
 struct ScriptSourceChunk
 {
-    ScriptSource* ss;
-    uint32_t chunk;
-
-    ScriptSourceChunk()
-      : ss(nullptr), chunk(0)
-    {}
+    ScriptSource* ss = nullptr;
+    uint32_t chunk = 0;
+
+    ScriptSourceChunk() = default;
+
     ScriptSourceChunk(ScriptSource* ss, uint32_t chunk)
       : ss(ss), chunk(chunk)
     {
-        MOZ_ASSERT(valid());;
+        MOZ_ASSERT(valid());
     }
+
     bool valid() const { return ss != nullptr; }
 
     bool operator==(const ScriptSourceChunk& other) const {
         return ss == other.ss && chunk == other.chunk;
     }
 };
 
 struct ScriptSourceChunkHasher
@@ -320,138 +327,274 @@ struct ScriptSourceChunkHasher
     static HashNumber hash(const ScriptSourceChunk& ssc) {
         return mozilla::AddToHash(DefaultHasher<ScriptSource*>::hash(ssc.ss), ssc.chunk);
     }
     static bool match(const ScriptSourceChunk& c1, const ScriptSourceChunk& c2) {
         return c1 == c2;
     }
 };
 
+template<typename CharT>
+using EntryChars = mozilla::UniquePtr<CharT[], JS::FreePolicy>;
+
+// The uncompressed source cache contains *either* UTF-8 source data *or*
+// UTF-16 source data.  ScriptSourceChunk implies a ScriptSource that
+// contains either UTF-8 data or UTF-16 data, so the nature of the key to
+// Map below indicates how each SourceData ought to be interpreted.
+using SourceData = mozilla::UniquePtr<void, JS::FreePolicy>;
+
+template<typename CharT>
+inline SourceData
+ToSourceData(EntryChars<CharT> chars)
+{
+    static_assert(std::is_same<SourceData::DeleterType,
+                               typename EntryChars<CharT>::DeleterType>::value,
+                  "EntryChars and SourceData must share the same deleter "
+                  "type, that need not know the type of the data being freed, "
+                  "for the upcast below to be safe");
+    return SourceData(chars.release());
+}
+
 class UncompressedSourceCache
 {
-    typedef HashMap<ScriptSourceChunk,
-                    UniqueTwoByteChars,
-                    ScriptSourceChunkHasher,
-                    SystemAllocPolicy> Map;
+    using Map = HashMap<ScriptSourceChunk,
+                        SourceData,
+                        ScriptSourceChunkHasher,
+                        SystemAllocPolicy>;
 
   public:
     // Hold an entry in the source data cache and prevent it from being purged on GC.
     class AutoHoldEntry
     {
-        UncompressedSourceCache* cache_;
-        ScriptSourceChunk sourceChunk_;
-        UniqueTwoByteChars charsToFree_;
+        UncompressedSourceCache* cache_ = nullptr;
+        ScriptSourceChunk sourceChunk_ = {};
+        SourceData data_ = nullptr;
+
       public:
-        explicit AutoHoldEntry();
-        ~AutoHoldEntry();
-        void holdChars(UniqueTwoByteChars chars);
+        explicit AutoHoldEntry() = default;
+
+        ~AutoHoldEntry() {
+            if (cache_) {
+                MOZ_ASSERT(sourceChunk_.valid());
+                cache_->releaseEntry(*this);
+            }
+        }
+
+        template<typename CharT>
+        void holdChars(EntryChars<CharT> chars) {
+            MOZ_ASSERT(!cache_);
+            MOZ_ASSERT(!sourceChunk_.valid());
+            MOZ_ASSERT(!data_);
+
+            data_ = ToSourceData(std::move(chars));
+        }
+
       private:
-        void holdEntry(UncompressedSourceCache* cache, const ScriptSourceChunk& sourceChunk);
-        void deferDelete(UniqueTwoByteChars chars);
+        void holdEntry(UncompressedSourceCache* cache, const ScriptSourceChunk& sourceChunk) {
+            // Initialise the holder for a specific cache and script source.
+            // This will hold on to the cached source chars in the event that
+            // the cache is purged.
+            MOZ_ASSERT(!cache_);
+            MOZ_ASSERT(!sourceChunk_.valid());
+            MOZ_ASSERT(!data_);
+
+            cache_ = cache;
+            sourceChunk_ = sourceChunk;
+        }
+
+        void deferDelete(SourceData data) {
+            // Take ownership of source chars now the cache is being purged. Remove our
+            // reference to the ScriptSource which might soon be destroyed.
+            MOZ_ASSERT(cache_);
+            MOZ_ASSERT(sourceChunk_.valid());
+            MOZ_ASSERT(!data_);
+
+            cache_ = nullptr;
+            sourceChunk_ = ScriptSourceChunk();
+
+            data_ = std::move(data);
+        }
+
+
         const ScriptSourceChunk& sourceChunk() const { return sourceChunk_; }
         friend class UncompressedSourceCache;
     };
 
   private:
-    UniquePtr<Map> map_;
-    AutoHoldEntry* holder_;
+    UniquePtr<Map> map_ = nullptr;
+    AutoHoldEntry* holder_ = nullptr;
 
   public:
-    UncompressedSourceCache() : holder_(nullptr) {}
-
-    const char16_t* lookup(const ScriptSourceChunk& ssc, AutoHoldEntry& asp);
-    bool put(const ScriptSourceChunk& ssc, UniqueTwoByteChars chars, AutoHoldEntry& asp);
+    UncompressedSourceCache() = default;
+
+    template<typename CharT>
+    const CharT* lookup(const ScriptSourceChunk& ssc, AutoHoldEntry& asp);
+
+    bool put(const ScriptSourceChunk& ssc, SourceData data, AutoHoldEntry& asp);
 
     void purge();
 
     size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf);
 
   private:
     void holdEntry(AutoHoldEntry& holder, const ScriptSourceChunk& ssc);
     void releaseEntry(AutoHoldEntry& holder);
 };
 
+template<typename CharT>
+struct SourceTypeTraits;
+
+template<>
+struct SourceTypeTraits<mozilla::Utf8Unit>
+{
+    using SharedImmutableString = js::SharedImmutableString;
+
+    static const mozilla::Utf8Unit* chars(const SharedImmutableString& string) {
+        // Casting |char| data to |Utf8Unit| is safe because |Utf8Unit|
+        // contains a |char|.  See the long comment in |Utf8Unit|'s definition.
+        return reinterpret_cast<const mozilla::Utf8Unit*>(string.chars());
+    }
+
+    static char* toString(const mozilla::Utf8Unit* units) {
+        auto asUnsigned = const_cast<unsigned char*>(mozilla::Utf8AsUnsignedChars(units));
+        return reinterpret_cast<char*>(asUnsigned);
+    }
+
+    static UniqueChars toCacheable(EntryChars<mozilla::Utf8Unit> str) {
+        // The cache only stores strings of |char| or |char16_t|, and right now
+        // it seems best not to gunk up the cache with |Utf8Unit| too.  So
+        // cache |Utf8Unit| strings by interpreting them as |char| strings.
+        char* chars = toString(str.release());
+        return UniqueChars(chars);
+    }
+};
+
+template<>
+struct SourceTypeTraits<char16_t>
+{
+    using SharedImmutableString = js::SharedImmutableTwoByteString;
+
+    static const char16_t* chars(const SharedImmutableString& string) {
+        return string.chars();
+    }
+
+    static char16_t* toString(char16_t* units) {
+        return units;
+    }
+
+    static UniqueTwoByteChars toCacheable(EntryChars<char16_t> str) {
+        return UniqueTwoByteChars(std::move(str));
+    }
+};
+
 class ScriptSource
 {
     friend class SourceCompressionTask;
 
+    class PinnedCharsBase
+    {
+      protected:
+        PinnedCharsBase** stack_ = nullptr;
+        PinnedCharsBase* prev_ = nullptr;
+
+        ScriptSource* source_;
+
+        explicit PinnedCharsBase(ScriptSource* source)
+          : source_(source)
+        {}
+    };
+
   public:
     // Any users that wish to manipulate the char buffer of the ScriptSource
     // needs to do so via PinnedChars for GC safety. A GC may compress
     // ScriptSources. If the source were initially uncompressed, then any raw
     // pointers to the char buffer would now point to the freed, uncompressed
     // chars. This is analogous to Rooted.
-    class PinnedChars
+    template<typename CharT>
+    class PinnedChars : public PinnedCharsBase
     {
-        PinnedChars** stack_;
-        PinnedChars* prev_;
-
-        ScriptSource* source_;
-        const char16_t* chars_;
+        const CharT* chars_;
 
       public:
         PinnedChars(JSContext* cx, ScriptSource* source,
                     UncompressedSourceCache::AutoHoldEntry& holder,
                     size_t begin, size_t len);
 
         ~PinnedChars();
 
-        const char16_t* get() const {
+        const CharT* get() const {
             return chars_;
         }
     };
 
   private:
     mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire,
                     mozilla::recordreplay::Behavior::DontPreserve> refs;
 
     // Note: while ScriptSources may be compressed off thread, they are only
     // modified by the main thread, and all members are always safe to access
     // on the main thread.
 
     // Indicate which field in the |data| union is active.
-
     struct Missing { };
 
-    struct Uncompressed
+    template<typename CharT>
+    class Uncompressed
     {
-        SharedImmutableTwoByteString string;
-
-        explicit Uncompressed(SharedImmutableTwoByteString&& str)
-          : string(std::move(str))
-        { }
+        typename SourceTypeTraits<CharT>::SharedImmutableString string_;
+
+
+      public:
+        explicit Uncompressed(typename SourceTypeTraits<CharT>::SharedImmutableString str)
+          : string_(std::move(str))
+        {}
+
+        const CharT* chars() const {
+            return SourceTypeTraits<CharT>::chars(string_);
+        }
+
+        size_t length() const {
+            return string_.length();
+        }
     };
 
+    template<typename CharT>
     struct Compressed
     {
+        // Single-byte compressed text, regardless whether the original text
+        // was single-byte or two-byte.
         SharedImmutableString raw;
         size_t uncompressedLength;
 
-        Compressed(SharedImmutableString&& raw, size_t uncompressedLength)
-          : raw(std::move(raw))
-          , uncompressedLength(uncompressedLength)
+        Compressed(SharedImmutableString raw, size_t uncompressedLength)
+          : raw(std::move(raw)),
+            uncompressedLength(uncompressedLength)
         { }
     };
 
     struct BinAST
     {
         SharedImmutableString string;
         explicit BinAST(SharedImmutableString&& str)
           : string(std::move(str))
         { }
     };
 
-    using SourceType = mozilla::Variant<Missing, Uncompressed, Compressed, BinAST>;
+    using SourceType =
+        mozilla::Variant<Compressed<mozilla::Utf8Unit>, Uncompressed<mozilla::Utf8Unit>,
+                         Compressed<char16_t>, Uncompressed<char16_t>,
+                         Missing,
+                         BinAST>;
     SourceType data;
 
     // If the GC attempts to call setCompressedSource with PinnedChars
     // present, the first PinnedChars (that is, bottom of the stack) will set
     // the compressed chars upon destruction.
-    PinnedChars* pinnedCharsStack_;
-    mozilla::Maybe<Compressed> pendingCompressed_;
+    PinnedCharsBase* pinnedCharsStack_;
+    mozilla::MaybeOneOf<Compressed<mozilla::Utf8Unit>, Compressed<char16_t>> pendingCompressed_;
 
     // The filename of this script.
     UniqueChars filename_;
 
     UniqueTwoByteChars displayURL_;
     UniqueTwoByteChars sourceMapURL_;
     bool mutedErrors_;
 
@@ -507,27 +650,30 @@ class ScriptSource
     // demand. If sourceRetrievable_ and hasSourceText() are false, it is not
     // possible to get source at all.
     bool sourceRetrievable_:1;
     bool hasIntroductionOffset_:1;
     bool containsAsmJS_:1;
 
     UniquePtr<frontend::BinASTSourceMetadata> binASTMetadata_;
 
-    const char16_t* chunkChars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
-                               size_t chunk);
+    template<typename CharT>
+    const CharT* chunkChars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& holder,
+                            size_t chunk);
 
     // Return a string containing the chars starting at |begin| and ending at
     // |begin + len|.
     //
     // Warning: this is *not* GC-safe! Any chars to be handed out should use
     // PinnedChars. See comment below.
-    const char16_t* chars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& asp,
-                          size_t begin, size_t len);
-
+    template<typename CharT>
+    const CharT* chars(JSContext* cx, UncompressedSourceCache::AutoHoldEntry& asp,
+                       size_t begin, size_t len);
+
+    template<typename CharT>
     void movePendingCompressedSource();
 
   public:
     // When creating a JSString* from TwoByte source characters, we don't try to
     // to deflate to Latin1 for longer strings, because this can be slow.
     static const size_t SourceDeflateLimit = 100;
 
     explicit ScriptSource()
@@ -563,78 +709,290 @@ class ScriptSource
     MOZ_MUST_USE bool initFromOptions(JSContext* cx,
                                       const JS::ReadOnlyCompileOptions& options,
                                       const mozilla::Maybe<uint32_t>& parameterListEnd = mozilla::Nothing());
     MOZ_MUST_USE bool setSourceCopy(JSContext* cx, JS::SourceBufferHolder& srcBuf);
     void setSourceRetrievable() { sourceRetrievable_ = true; }
     bool sourceRetrievable() const { return sourceRetrievable_; }
     bool hasSourceText() const { return hasUncompressedSource() || hasCompressedSource(); }
     bool hasBinASTSource() const { return data.is<BinAST>(); }
-    bool hasUncompressedSource() const { return data.is<Uncompressed>(); }
-    bool hasCompressedSource() const { return data.is<Compressed>(); }
 
     void setBinASTSourceMetadata(frontend::BinASTSourceMetadata* metadata) {
         MOZ_ASSERT(hasBinASTSource());
         binASTMetadata_.reset(metadata);
     }
     frontend::BinASTSourceMetadata* binASTSourceMetadata() const {
         MOZ_ASSERT(hasBinASTSource());
         return binASTMetadata_.get();
     }
 
+  private:
+    struct UncompressedDataMatcher
+    {
+        template<typename CharT>
+        const void* match(const Uncompressed<CharT>& u) {
+            return u.chars();
+        }
+
+        template<typename T>
+        const void* match(const T&) {
+            MOZ_CRASH("attempting to access uncompressed data in a "
+                      "ScriptSource not containing it");
+            return nullptr;
+        }
+    };
+
+  public:
+    template<typename CharT>
+    const CharT* uncompressedData() {
+        return static_cast<const CharT*>(data.match(UncompressedDataMatcher()));
+    }
+
+  private:
+    struct CompressedDataMatcher
+    {
+        template<typename CharT>
+        char* match(const Compressed<CharT>& c) {
+            return const_cast<char*>(c.raw.chars());
+        }
+
+        template<typename T>
+        char* match(const T&) {
+            MOZ_CRASH("attempting to access compressed data in a ScriptSource "
+                      "not containing it");
+            return nullptr;
+        }
+    };
+
+  public:
+    template<typename CharT>
+    char* compressedData() {
+        return data.match(CompressedDataMatcher());
+    }
+
+  private:
+    struct BinASTDataMatcher
+    {
+        void* match(const BinAST& b) {
+            return const_cast<char*>(b.string.chars());
+        }
+
+        void notBinAST() {
+            MOZ_CRASH("ScriptSource isn't backed by BinAST data");
+        }
+
+        template<typename T>
+        void* match(const T&) {
+            notBinAST();
+            return nullptr;
+        }
+    };
+
+  public:
+    void* binASTData() {
+        return data.match(BinASTDataMatcher());
+    }
+
+  private:
+    struct HasUncompressedSource
+    {
+        template<typename CharT>
+        bool match(const Uncompressed<CharT>&) { return true; }
+
+        template<typename CharT>
+        bool match(const Compressed<CharT>&) { return false; }
+
+        bool match(const BinAST&) { return false; }
+
+        bool match(const Missing&) { return false; }
+    };
+
+  public:
+    bool hasUncompressedSource() const {
+        return data.match(HasUncompressedSource());
+    }
+
+    template<typename CharT>
+    bool uncompressedSourceIs() const {
+        MOZ_ASSERT(hasUncompressedSource());
+        return data.is<Uncompressed<CharT>>();
+    }
+
+  private:
+    struct HasCompressedSource
+    {
+        template<typename CharT>
+        bool match(const Compressed<CharT>&) { return true; }
+
+        template<typename CharT>
+        bool match(const Uncompressed<CharT>&) { return false; }
+
+        bool match(const BinAST&) { return false; }
+
+        bool match(const Missing&) { return false; }
+    };
+
+  public:
+    bool hasCompressedSource() const {
+        return data.match(HasCompressedSource());
+    }
+
+    template<typename CharT>
+    bool compressedSourceIs() const {
+        MOZ_ASSERT(hasCompressedSource());
+        return data.is<Compressed<CharT>>();
+    }
+
+  private:
+    template<typename CharT>
+    struct SourceTypeMatcher
+    {
+        template<template<typename C> class Data>
+        bool match(const Data<CharT>&) {
+            return true;
+        }
+
+        template<template<typename C> class Data, typename NotCharT>
+        bool match(const Data<NotCharT>&) {
+            return false;
+        }
+
+        bool match(const BinAST&) {
+            MOZ_CRASH("doesn't make sense to ask source type of BinAST data");
+            return false;
+        }
+
+        bool match(const Missing&) {
+            MOZ_CRASH("doesn't make sense to ask source type when missing");
+            return false;
+        }
+    };
+
+  public:
+    template<typename CharT>
+    bool hasSourceType() const {
+        return data.match(SourceTypeMatcher<CharT>());
+    }
+
+  private:
+    struct SourceCharSizeMatcher
+    {
+        template<template<typename C> class Data, typename CharT>
+        uint8_t match(const Data<CharT>& data) {
+            static_assert(std::is_same<CharT, mozilla::Utf8Unit>::value ||
+                          std::is_same<CharT, char16_t>::value,
+                          "should only have UTF-8 or UTF-16 source char");
+            return sizeof(CharT);
+        }
+
+        uint8_t match(const BinAST&) {
+            MOZ_CRASH("BinAST source has no source-char size");
+            return 0;
+        }
+
+        uint8_t match(const Missing&) {
+            MOZ_CRASH("missing source has no source-char size");
+            return 0;
+        }
+    };
+
+  public:
+    uint8_t sourceCharSize() const {
+        return data.match(SourceCharSizeMatcher());
+    }
+
+  private:
+    struct UncompressedLengthMatcher
+    {
+        template<typename CharT>
+        size_t match(const Uncompressed<CharT>& u) {
+            return u.length();
+        }
+
+        template<typename CharT>
+        size_t match(const Compressed<CharT>& u) {
+            return u.uncompressedLength;
+        }
+
+        size_t match(const BinAST& b) {
+            return b.string.length();
+        }
+
+        size_t match(const Missing& m) {
+            MOZ_CRASH("ScriptSource::length on a missing source");
+            return 0;
+        }
+    };
+
+  public:
     size_t length() const {
-        struct LengthMatcher
-        {
-            size_t match(const Uncompressed& u) {
-                return u.string.length();
-            }
-
-            size_t match(const Compressed& c) {
-                return c.uncompressedLength;
-            }
-
-            size_t match(const BinAST& b) {
-                return b.string.length();
-            }
-
-            size_t match(const Missing& m) {
-                MOZ_CRASH("ScriptSource::length on a missing source");
-                return 0;
-            }
-        };
-
         MOZ_ASSERT(hasSourceText() || hasBinASTSource());
-        return data.match(LengthMatcher());
+        return data.match(UncompressedLengthMatcher());
+    }
+
+  private:
+    struct CompressedLengthOrZeroMatcher
+    {
+        template<typename CharT>
+        size_t match(const Uncompressed<CharT>&) {
+            return 0;
+        }
+
+        template<typename CharT>
+        size_t match(const Compressed<CharT>& c) {
+            return c.raw.length();
+        }
+
+        size_t match(const BinAST&) {
+            MOZ_CRASH("trying to get compressed length for BinAST data");
+            return 0;
+        }
+
+        size_t match(const Missing&) {
+            MOZ_CRASH("missing source data");
+            return 0;
+        }
+    };
+
+  public:
+    size_t compressedLengthOrZero() const {
+        return data.match(CompressedLengthOrZeroMatcher());
     }
 
     JSFlatString* substring(JSContext* cx, size_t start, size_t stop);
     JSFlatString* substringDontDeflate(JSContext* cx, size_t start, size_t stop);
 
     MOZ_MUST_USE bool appendSubstring(JSContext* cx, js::StringBuffer& buf, size_t start, size_t stop);
 
     bool isFunctionBody() {
         return parameterListEnd_ != 0;
     }
     JSFlatString* functionBodyString(JSContext* cx);
 
     void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                 JS::ScriptSourceInfo* info) const;
 
+    template<typename CharT>
     MOZ_MUST_USE bool setSource(JSContext* cx,
-                                UniqueTwoByteChars&& source,
+                                EntryChars<CharT>&& source,
                                 size_t length);
-    void setSource(SharedImmutableTwoByteString&& string);
+
+    template<typename CharT>
+    void setSource(typename SourceTypeTraits<CharT>::SharedImmutableString uncompressed);
 
     MOZ_MUST_USE bool tryCompressOffThread(JSContext* cx);
 
-    MOZ_MUST_USE bool setCompressedSource(JSContext* cx,
-                                          UniqueChars&& raw,
-                                          size_t rawLength,
+    // The CharT parameter determines which type of compressed source is
+    // recorded, but raw compressed source is always single-byte.
+    template<typename CharT>
+    void setCompressedSource(SharedImmutableString compressed, size_t sourceLength);
+
+    template<typename CharT>
+    MOZ_MUST_USE bool setCompressedSource(JSContext* cx, UniqueChars&& raw, size_t rawLength,
                                           size_t sourceLength);
-    void setCompressedSource(SharedImmutableString&& raw, size_t sourceLength);
 
 #if defined(JS_BUILD_BINAST)
 
     /*
      * Do not take ownership of the given `buf`. Store the canonical, shared
      * and de-duplicated version. If there is no extant shared version of
      * `buf`, make a copy.
      */
@@ -645,20 +1003,71 @@ class ScriptSource
      * de-duplicated version.
      */
     MOZ_MUST_USE bool setBinASTSource(JSContext* cx, UniqueChars&& buf, size_t len);
 
     const uint8_t* binASTSource();
 
 #endif /* JS_BUILD_BINAST */
 
+  private:
+    void performTaskWork(SourceCompressionTask* task);
+
+    struct SetCompressedSourceFromTask
+    {
+        ScriptSource* const source_;
+        SharedImmutableString& compressed_;
+
+        SetCompressedSourceFromTask(ScriptSource* source, SharedImmutableString& compressed)
+          : source_(source),
+            compressed_(compressed)
+        {}
+
+        template<typename CharT>
+        void match(const Uncompressed<CharT>&) {
+            source_->setCompressedSource<CharT>(std::move(compressed_), source_->length());
+        }
+
+        template<typename CharT>
+        void match(const Compressed<CharT>&) {
+            MOZ_CRASH("can't set compressed source when source is already "
+                      "compressed -- ScriptSource::tryCompressOffThread "
+                      "shouldn't have queued up this task?");
+        }
+
+        void match(const BinAST&) {
+            MOZ_CRASH("doesn't make sense to set compressed source for BinAST "
+                      "data");
+        }
+
+        void match(const Missing&) {
+            MOZ_CRASH("doesn't make sense to set compressed source for "
+                      "missing source -- ScriptSource::tryCompressOffThread "
+                      "shouldn't have queued up this task?");
+        }
+    };
+
+    void setCompressedSourceFromTask(SharedImmutableString compressed);
+
+  public:
     // XDR handling
     template <XDRMode mode>
     MOZ_MUST_USE XDRResult performXDR(XDRState<mode>* xdr);
 
+  private:
+    // It'd be better to make this function take <XDRMode, CharT>, as both
+    // specializations of this function contain nested CharT-parametrized
+    // helper classes that do everything the function needs to do.  But then
+    // we'd need template function partial specialization to hold XDRMode
+    // constant while varying CharT, so that idea's no dice.
+    template <XDRMode mode>
+    MOZ_MUST_USE XDRResult xdrUncompressedSource(XDRState<mode>* xdr, uint8_t sourceCharSize,
+                                                 uint32_t uncompressedLength);
+
+  public:
     MOZ_MUST_USE bool setFilename(JSContext* cx, const char* filename);
     const char* introducerFilename() const {
         return introducerFilename_ ? introducerFilename_.get() : filename_.get();
     }
     bool hasIntroductionType() const {
         return introductionType_;
     }
     const char* introductionType() const {
--- a/js/src/vm/SharedImmutableStringsCache.h
+++ b/js/src/vm/SharedImmutableStringsCache.h
@@ -7,16 +7,17 @@
 #ifndef vm_SharedImmutableStringsCache_h
 #define vm_SharedImmutableStringsCache_h
 
 #include "mozilla/Maybe.h"
 #include "mozilla/UniquePtr.h"
 
 #include <cstring>
 #include <new> // for placement new
+#include <utility> // std::move
 
 #include "builtin/String.h"
 
 #include "js/HashTable.h"
 #include "js/UniquePtr.h"
 #include "js/Utility.h"
 
 #include "threading/ExclusiveData.h"
@@ -24,19 +25,19 @@
 #include "vm/MutexIDs.h"
 
 namespace js {
 
 class SharedImmutableString;
 class SharedImmutableTwoByteString;
 
 /**
- * The `SharedImmutableStringsCache` allows for safely sharing and deduplicating
- * immutable strings (either `const char*` or `const char16_t*`) between
- * threads.
+ * The `SharedImmutableStringsCache` allows safely sharing and deduplicating
+ * immutable strings (either `const char*` [any encoding, not restricted to
+ * only Latin-1 or only UTF-8] 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
 {
--- a/mfbt/RecordReplay.cpp
+++ b/mfbt/RecordReplay.cpp
@@ -4,16 +4,17 @@
  * 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 "RecordReplay.h"
 
 #include "js/GCAnnotations.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Casting.h"
+#include "mozilla/Utf8.h"
 
 #include <stdlib.h>
 
 // Recording and replaying is only enabled on Mac nightlies.
 #if defined(XP_MACOSX) && defined(NIGHTLY_BUILD)
 #define ENABLE_RECORD_REPLAY
 #endif
 
@@ -71,17 +72,20 @@ namespace recordreplay {
   Macro(InternalRecordReplayAssertBytes,                        \
         (const void* aData, size_t aSize), (aData, aSize))      \
   Macro(InternalRegisterThing, (void* aThing), (aThing))        \
   Macro(InternalUnregisterThing, (void* aThing), (aThing))      \
   Macro(InternalRecordReplayDirective, (long aDirective), (aDirective)) \
   Macro(BeginContentParse,                                      \
         (const void* aToken, const char* aURL, const char* aContentType), \
         (aToken, aURL, aContentType))                           \
-  Macro(AddContentParseData,                                    \
+  Macro(AddContentParseData8,                                   \
+        (const void* aToken, const mozilla::Utf8Unit* aUtf8Buffer, size_t aLength), \
+        (aToken, aUtf8Buffer, aLength))                         \
+  Macro(AddContentParseData16,                                  \
         (const void* aToken, const char16_t* aBuffer, size_t aLength), \
         (aToken, aBuffer, aLength))                             \
   Macro(EndContentParse, (const void* aToken), (aToken))
 
 #define DECLARE_SYMBOL(aName, aReturnType, aFormals, _) \
   static aReturnType (*gPtr ##aName) aFormals;
 #define DECLARE_SYMBOL_VOID(aName, aFormals, _)  DECLARE_SYMBOL(aName, void, aFormals, _)
 
--- a/mfbt/RecordReplay.h
+++ b/mfbt/RecordReplay.h
@@ -8,16 +8,17 @@
 
 #ifndef mozilla_RecordReplay_h
 #define mozilla_RecordReplay_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/GuardObjects.h"
 #include "mozilla/TemplateLib.h"
 #include "mozilla/Types.h"
+#include "mozilla/Utf8.h"
 
 #include <functional>
 #include <stdarg.h>
 
 struct PLDHashTableOps;
 struct JSContext;
 class JSObject;
 
@@ -348,31 +349,46 @@ MFBT_API bool DefineRecordReplayControlO
 // being parsed. This is used to provide the complete contents of the URL to
 // devtools code when it is inspecting the state of this process; that devtools
 // code can't simply fetch the URL itself since it may have been changed since
 // the recording was made or may no longer exist. The token for a parse may not
 // be used in other parses until after EndContentParse() is called.
 MFBT_API void BeginContentParse(const void* aToken,
                                 const char* aURL, const char* aContentType);
 
-// Add some parse data to an existing content parse.
-MFBT_API void AddContentParseData(const void* aToken,
-                                  const char16_t* aBuffer, size_t aLength);
+// Add some UTF-8 parse data to an existing content parse.
+MFBT_API void AddContentParseData8(const void* aToken,
+                                   const Utf8Unit* aUtf8Buffer, size_t aLength);
+
+// Add some UTF-16 parse data to an existing content parse.
+MFBT_API void AddContentParseData16(const void* aToken,
+                                    const char16_t* aBuffer, size_t aLength);
 
 // Mark a content parse as having completed.
 MFBT_API void EndContentParse(const void* aToken);
 
 // Perform an entire content parse, when the entire URL is available at once.
 static inline void
-NoteContentParse(const void* aToken,
-                 const char* aURL, const char* aContentType,
-                 const char16_t* aBuffer, size_t aLength)
+NoteContentParse8(const void* aToken,
+                  const char* aURL, const char* aContentType,
+                  const mozilla::Utf8Unit* aUtf8Buffer, size_t aLength)
 {
   BeginContentParse(aToken, aURL, aContentType);
-  AddContentParseData(aToken, aBuffer, aLength);
+  AddContentParseData8(aToken, aUtf8Buffer, aLength);
+  EndContentParse(aToken);
+}
+
+// Perform an entire content parse, when the entire URL is available at once.
+static inline void
+NoteContentParse16(const void* aToken,
+                   const char* aURL, const char* aContentType,
+                   const char16_t* aBuffer, size_t aLength)
+{
+  BeginContentParse(aToken, aURL, aContentType);
+  AddContentParseData16(aToken, aBuffer, aLength);
   EndContentParse(aToken);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // API inline function implementation
 ///////////////////////////////////////////////////////////////////////////////
 
 // Define inline wrappers on builds where recording/replaying is enabled.
--- a/parser/html/nsHtml5StreamParser.cpp
+++ b/parser/html/nsHtml5StreamParser.cpp
@@ -849,17 +849,17 @@ nsHtml5StreamParser::WriteStreamBytes(co
     auto dst = mLastBuffer->TailAsSpan(NS_HTML5_STREAM_PARSER_READ_BUFFER_SIZE);
     uint32_t result;
     size_t read;
     size_t written;
     bool hadErrors;
     Tie(result, read, written, hadErrors) =
       mUnicodeDecoder->DecodeToUTF16(src, dst, false);
     if (recordreplay::IsRecordingOrReplaying()) {
-      recordreplay::AddContentParseData(this, dst.data(), written);
+      recordreplay::AddContentParseData16(this, dst.data(), written);
     }
     if (hadErrors && !mHasHadErrors) {
       mHasHadErrors = true;
       if (mEncoding == UTF_8_ENCODING) {
         mTreeBuilder->TryToEnableEncodingMenu();
       }
     }
     src = src.From(read);
@@ -1108,17 +1108,17 @@ nsHtml5StreamParser::DoStopRequest()
     auto dst = mLastBuffer->TailAsSpan(NS_HTML5_STREAM_PARSER_READ_BUFFER_SIZE);
     uint32_t result;
     size_t read;
     size_t written;
     bool hadErrors;
     Tie(result, read, written, hadErrors) =
       mUnicodeDecoder->DecodeToUTF16(src, dst, true);
     if (recordreplay::IsRecordingOrReplaying()) {
-      recordreplay::AddContentParseData(this, dst.data(), written);
+      recordreplay::AddContentParseData16(this, dst.data(), written);
     }
     if (hadErrors && !mHasHadErrors) {
       mHasHadErrors = true;
       if (mEncoding == UTF_8_ENCODING) {
         mTreeBuilder->TryToEnableEncodingMenu();
       }
     }
     MOZ_ASSERT(read == 0, "How come an empty span was read form?");
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -571,29 +571,31 @@ GetEntryPosition(const BreakpointPositio
 // Replaying process content
 ///////////////////////////////////////////////////////////////////////////////
 
 struct ContentInfo
 {
   const void* mToken;
   char* mURL;
   char* mContentType;
-  InfallibleVector<char16_t> mContent;
+  InfallibleVector<char> mContent8;
+  InfallibleVector<char16_t> mContent16;
 
   ContentInfo(const void* aToken, const char* aURL, const char* aContentType)
     : mToken(aToken),
       mURL(strdup(aURL)),
       mContentType(strdup(aContentType))
   {}
 
   ContentInfo(ContentInfo&& aOther)
     : mToken(aOther.mToken),
       mURL(aOther.mURL),
       mContentType(aOther.mContentType),
-      mContent(std::move(aOther.mContent))
+      mContent8(std::move(aOther.mContent8)),
+      mContent16(std::move(aOther.mContent16))
   {
     aOther.mURL = nullptr;
     aOther.mContentType = nullptr;
   }
 
   ~ContentInfo()
   {
     free(mURL);
@@ -618,28 +620,47 @@ RecordReplayInterface_BeginContentParse(
   MonitorAutoLock lock(*child::gMonitor);
   for (ContentInfo& info : gContent) {
     MOZ_RELEASE_ASSERT(info.mToken != aToken);
   }
   gContent.emplaceBack(aToken, aURL, aContentType);
 }
 
 MOZ_EXPORT void
-RecordReplayInterface_AddContentParseData(const void* aToken,
-                                          const char16_t* aBuffer, size_t aLength)
+RecordReplayInterface_AddContentParseData8(const void* aToken,
+                                           const Utf8Unit* aUtf8Buffer, size_t aLength)
 {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
   MOZ_RELEASE_ASSERT(aToken);
 
-  RecordReplayAssert("AddContentParseDataForRecordReplay %d", (int) aLength);
+  RecordReplayAssert("AddContentParseData8ForRecordReplay %d", (int) aLength);
 
   MonitorAutoLock lock(*child::gMonitor);
   for (ContentInfo& info : gContent) {
     if (info.mToken == aToken) {
-      info.mContent.append(aBuffer, aLength);
+      info.mContent8.append(reinterpret_cast<const char*>(aUtf8Buffer), aLength);
+      return;
+    }
+  }
+  MOZ_CRASH("Unknown content parse token");
+}
+
+MOZ_EXPORT void
+RecordReplayInterface_AddContentParseData16(const void* aToken,
+                                            const char16_t* aBuffer, size_t aLength)
+{
+  MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
+  MOZ_RELEASE_ASSERT(aToken);
+
+  RecordReplayAssert("AddContentParseData16ForRecordReplay %d", (int) aLength);
+
+  MonitorAutoLock lock(*child::gMonitor);
+  for (ContentInfo& info : gContent) {
+    if (info.mToken == aToken) {
+      info.mContent16.append(aBuffer, aLength);
       return;
     }
   }
   MOZ_CRASH("Unknown content parse token");
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_EndContentParse(const void* aToken)
@@ -662,19 +683,29 @@ RecordReplayInterface_EndContentParse(co
 static bool
 FetchContent(JSContext* aCx, HandleString aURL,
              MutableHandleString aContentType, MutableHandleString aContent)
 {
   MonitorAutoLock lock(*child::gMonitor);
   for (ContentInfo& info : gContent) {
     if (JS_FlatStringEqualsAscii(JS_ASSERT_STRING_IS_FLAT(aURL), info.mURL)) {
       aContentType.set(JS_NewStringCopyZ(aCx, info.mContentType));
-      aContent.set(JS_NewUCStringCopyN(aCx, (const char16_t*) info.mContent.begin(),
-                                       info.mContent.length()));
-      return aContentType && aContent;
+      if (!aContentType) {
+        return false;
+      }
+
+      MOZ_ASSERT(info.mContent8.length() == 0 ||
+                 info.mContent16.length() == 0,
+                 "should have content data of only one type");
+
+      aContent.set(info.mContent8.length() > 0
+                   ? JS_NewStringCopyUTF8N(aCx, JS::UTF8Chars(info.mContent8.begin(),
+                                                              info.mContent8.length()))
+                   : JS_NewUCStringCopyN(aCx, info.mContent16.begin(), info.mContent16.length()));
+      return aContent != nullptr;
     }
   }
   aContentType.set(JS_NewStringCopyZ(aCx, "text/plain"));
   aContent.set(JS_NewStringCopyZ(aCx, "Could not find record/replay content"));
   return aContentType && aContent;
 }
 
 ///////////////////////////////////////////////////////////////////////////////