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 489669 2ec2641a20ae953f6f6119777acb44c580310943
parent 489668 5b5648ea1b5b33d8406a0a78661dd7c86f4cd7b0
child 489670 ff4002836b9b87c8bcf0db5023a2147b33d7ac67
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerstcampbell
bugs1493441
milestone64.0a1
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;
 }
 
 ///////////////////////////////////////////////////////////////////////////////