Bug 1664312 - Optimize tiny atoms in ParserAtomsTable r=djvj
authorTed Campbell <tcampbell@mozilla.com>
Wed, 16 Sep 2020 19:18:44 +0000
changeset 548987 c5bf76bc593f7e818502c120f0a2015303edf1a2
parent 548986 dc0371757070911b15fbe82974e6f8fe9d3f68b5
child 548988 75f7048e9d0af7f1ef5ff34fc6a807ecfb0fbd86
push id126512
push usertcampbell@mozilla.com
push dateWed, 16 Sep 2020 19:24:40 +0000
treeherderautoland@c5bf76bc593f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdjvj
bugs1664312
milestone82.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 1664312 - Optimize tiny atoms in ParserAtomsTable r=djvj This add a similar optimization as js::StaticStrings for tiny strings to the ParserAtoms mechanism. This includes (some) length 1 and length 2 atoms for fast lookup. This is effective for large minified JS files and can speed up syntax-parsing by up to 20% in some cases. We extend the atomIndex_ Variant to have new `StaticParserString{1,2}` types which we use to in toJSAtom to quickly translate to the corresponding StaticStrings. Depends on D90152 Differential Revision: https://phabricator.services.mozilla.com/D90153
js/src/frontend/ParserAtom.cpp
js/src/frontend/ParserAtom.h
js/src/jsapi-tests/moz.build
js/src/jsapi-tests/testParserAtom.cpp
js/src/vm/StringType.h
--- a/js/src/frontend/ParserAtom.cpp
+++ b/js/src/frontend/ParserAtom.cpp
@@ -146,16 +146,24 @@ bool ParserAtomEntry::isIndex(uint32_t* 
 JS::Result<JSAtom*, OOM&> ParserAtomEntry::toJSAtom(
     JSContext* cx, CompilationInfo& compilationInfo) const {
   if (atomIndex_.is<AtomIndex>()) {
     return compilationInfo.input.atoms[atomIndex_.as<AtomIndex>()];
   }
   if (atomIndex_.is<WellKnownAtomId>()) {
     return GetWellKnownAtom(cx, atomIndex_.as<WellKnownAtomId>());
   }
+  if (atomIndex_.is<StaticParserString1>()) {
+    char16_t ch = static_cast<char16_t>(atomIndex_.as<StaticParserString1>());
+    return cx->staticStrings().getUnit(ch);
+  }
+  if (atomIndex_.is<StaticParserString2>()) {
+    size_t index = static_cast<size_t>(atomIndex_.as<StaticParserString2>());
+    return cx->staticStrings().getLength2FromIndex(index);
+  }
 
   JSAtom* atom;
   if (hasLatin1Chars()) {
     atom = AtomizeChars(cx, latin1Chars(), length());
   } else {
     atom = AtomizeChars(cx, twoByteChars(), length());
   }
   if (!atom) {
@@ -185,16 +193,37 @@ void ParserAtomEntry::dumpCharsNoQuote(j
 #endif
 
 ParserAtomsTable::ParserAtomsTable(JSRuntime* rt)
     : wellKnownTable_(*rt->commonParserNames) {}
 
 template <typename CharT>
 ParserAtomsTable::AddPtr ParserAtomsTable::lookupForAdd(
     JSContext* cx, InflatedChar16Sequence<CharT> seq) {
+#ifdef DEBUG
+  {
+    // Sample more chars than longest tiny atom.
+    constexpr int SAMPLE_LEN = 3;
+    char16_t buf[SAMPLE_LEN];
+    uint32_t len = 0;
+
+    InflatedChar16Sequence<CharT> seqCopy = seq;
+
+    for (int i = 0; i < SAMPLE_LEN; ++i) {
+      if (!seqCopy.hasMore()) {
+        break;
+      }
+      buf[len++] = seqCopy.next();
+    }
+
+    MOZ_ASSERT(wellKnownTable_.lookupTiny(buf, len) == nullptr,
+               "Should have already checked for common tiny atoms");
+  }
+#endif
+
   // Check against well-known.
   SpecificParserAtomLookup<CharT> lookup(seq);
   const ParserAtom* wk = wellKnownTable_.lookupChar16Seq(lookup);
   if (wk) {
     return AddPtr(wk);
   }
 
   // Check for existing atom.
@@ -268,28 +297,41 @@ JS::Result<const ParserAtom*, OOM&> Pars
     JSContext* cx, const char* asciiPtr, uint32_t length) {
   // ASCII strings are strict subsets of Latin1 strings.
   const Latin1Char* latin1Ptr = reinterpret_cast<const Latin1Char*>(asciiPtr);
   return internLatin1(cx, latin1Ptr, length);
 }
 
 JS::Result<const ParserAtom*, OOM&> ParserAtomsTable::internLatin1(
     JSContext* cx, const Latin1Char* latin1Ptr, uint32_t length) {
+  // Check for tiny strings which are abundant in minified code.
+  if (const ParserAtom* tiny = wellKnownTable_.lookupTiny(latin1Ptr, length)) {
+    return tiny;
+  }
+
   // Check for well-known or existing.
   InflatedChar16Sequence<Latin1Char> seq(latin1Ptr, length);
   AddPtr addPtr = lookupForAdd(cx, seq);
   if (addPtr) {
     return addPtr.get()->asAtom();
   }
 
   return internLatin1Seq(cx, addPtr, latin1Ptr, length);
 }
 
 JS::Result<const ParserAtom*, OOM&> ParserAtomsTable::internUtf8(
     JSContext* cx, const mozilla::Utf8Unit* utf8Ptr, uint32_t nbyte) {
+  // Check for tiny strings which are abundant in minified code.
+  // NOTE: The tiny atoms are all ASCII-only so we can directly look at the
+  //        UTF-8 data without worrying about surrogates.
+  if (const ParserAtom* tiny = wellKnownTable_.lookupTiny(
+          reinterpret_cast<const Latin1Char*>(utf8Ptr), nbyte)) {
+    return tiny;
+  }
+
   // If source text is ASCII, then the length of the target char buffer
   // is the same as the length of the UTF8 input.  Convert it to a Latin1
   // encoded string on the heap.
   UTF8Chars utf8(utf8Ptr, nbyte);
   JS::SmallestEncoding minEncoding = FindSmallestEncoding(utf8);
   if (minEncoding == JS::SmallestEncoding::ASCII) {
     // As ascii strings are a subset of Latin1 strings, and each encoding
     // unit is the same size, we can reliably cast this `Utf8Unit*`
@@ -319,16 +361,21 @@ JS::Result<const ParserAtom*, OOM&> Pars
   // Otherwise, add new entry.
   bool wide = (minEncoding == JS::SmallestEncoding::UTF16);
   return wide ? internChar16Seq<char16_t>(cx, addPtr, seq, length)
               : internChar16Seq<Latin1Char>(cx, addPtr, seq, length);
 }
 
 JS::Result<const ParserAtom*, OOM&> ParserAtomsTable::internChar16(
     JSContext* cx, const char16_t* char16Ptr, uint32_t length) {
+  // Check for tiny strings which are abundant in minified code.
+  if (const ParserAtom* tiny = wellKnownTable_.lookupTiny(char16Ptr, length)) {
+    return tiny;
+  }
+
   InflatedChar16Sequence<char16_t> seq(char16Ptr, length);
 
   // Check for well-known or existing.
   AddPtr addPtr = lookupForAdd(cx, seq);
   if (addPtr) {
     return addPtr.get()->asAtom();
   }
 
@@ -373,17 +420,17 @@ JS::Result<const ParserAtom*, OOM&> Pars
       return RaiseParserAtomsOOMError(cx);
     }
     id->setAtomIndex(index);
   } else {
 #ifdef DEBUG
     if (id->atomIndex_.is<AtomIndex>()) {
       MOZ_ASSERT(compilationInfo.input.atoms[id->atomIndex_.as<AtomIndex>()] ==
                  atom);
-    } else {
+    } else if (id->atomIndex_.is<WellKnownAtomId>()) {
       MOZ_ASSERT(GetWellKnownAtom(cx, id->atomIndex_.as<WellKnownAtomId>()) ==
                  atom);
     }
 #endif
   }
   return id;
 }
 
@@ -431,16 +478,17 @@ JS::Result<const ParserAtom*, OOM&> Pars
       mozilla::PodCopy(copy.get() + offset, atom->latin1Chars(),
                        atom->length());
       offset += atom->length();
     }
 
     InflatedChar16Sequence<Latin1Char> seq(copy.get(), catLen);
 
     // Check for well-known or existing.
+    // NOTE: Tiny atoms are always Latin1/inline so we can ignore them here.
     AddPtr addPtr = lookupForAdd(cx, seq);
     if (addPtr) {
       return addPtr.get()->asAtom();
     }
 
     // Otherwise, add new entry.
     UniquePtr<ParserAtomEntry> entry;
     MOZ_TRY_VAR(entry, ParserAtomEntry::allocate(cx, std::move(copy), catLen,
@@ -454,16 +502,17 @@ JS::Result<const ParserAtom*, OOM&> Pars
     for (const ParserAtom* atom : atoms) {
       FillChar16Buffer(buf + offset, atom);
       offset += atom->length();
     }
 
     InflatedChar16Sequence<char16_t> seq(buf, catLen);
 
     // Check for well-known or existing.
+    // NOTE: Tiny atoms are always Latin1/inline so we can ignore them here.
     AddPtr addPtr = lookupForAdd(cx, seq);
     if (addPtr) {
       return addPtr.get()->asAtom();
     }
 
     // Otherwise, add new entry.
     UniquePtr<ParserAtomEntry> entry;
     MOZ_TRY_VAR(entry, ParserAtomEntry::allocateInline<char16_t>(
@@ -480,16 +529,17 @@ JS::Result<const ParserAtom*, OOM&> Pars
   for (const ParserAtom* atom : atoms) {
     FillChar16Buffer(copy.get() + offset, atom);
     offset += atom->length();
   }
 
   InflatedChar16Sequence<char16_t> seq(copy.get(), catLen);
 
   // Check for well-known or existing.
+  // NOTE: Tiny atoms are always Latin1/inline so we can ignore them here.
   AddPtr addPtr = lookupForAdd(cx, seq);
   if (addPtr) {
     return addPtr.get()->asAtom();
   }
 
   // Otherwise, add new entry.
   UniquePtr<ParserAtomEntry> entry;
   MOZ_TRY_VAR(entry, ParserAtomEntry::allocate(cx, std::move(copy), catLen,
@@ -511,16 +561,24 @@ bool WellKnownParserAtoms::initSingle(JS
                                       const char* str, WellKnownAtomId kind) {
   MOZ_ASSERT(name != nullptr);
 
   unsigned int len = strlen(str);
 
   MOZ_ASSERT(FindSmallestEncoding(UTF8Chars(str, len)) ==
              JS::SmallestEncoding::ASCII);
 
+  // If we already reserved a tiny name, reuse the allocation but still point
+  // the fixed `name` reference at it.
+  if (const ParserAtom* tiny = lookupTiny(str, len)) {
+    MOZ_ASSERT(len == 1 || len == 2);
+    *name = tiny->asName();
+    return true;
+  }
+
   InflatedChar16Sequence<Latin1Char> seq(
       reinterpret_cast<const Latin1Char*>(str), len);
   SpecificParserAtomLookup<Latin1Char> lookup(seq);
   HashNumber hash = lookup.hash();
 
   UniquePtr<ParserAtomEntry> entry = nullptr;
 
   // Check for inline allocation.
@@ -553,17 +611,75 @@ bool WellKnownParserAtoms::initSingle(JS
     js::ReportOutOfMemory(cx);
     return false;
   }
 
   *name = nm;
   return true;
 }
 
+bool WellKnownParserAtoms::initStaticStrings(JSContext* cx) {
+  // Create known ParserAtoms for length-1 Latin1 strings.
+  static_assert(WellKnownParserAtoms::ASCII_STATIC_LIMIT <=
+                StaticStrings::UNIT_STATIC_LIMIT);
+  constexpr size_t NUM_LENGTH1 = WellKnownParserAtoms::ASCII_STATIC_LIMIT;
+  for (size_t i = 0; i < NUM_LENGTH1; ++i) {
+    JS::AutoCheckCannotGC nogc;
+    JSAtom* atom = cx->staticStrings().getUnit(char16_t(i));
+
+    constexpr size_t len = 1;
+    MOZ_ASSERT(atom->length() == len);
+
+    InflatedChar16Sequence<Latin1Char> seq(atom->latin1Chars(nogc), len);
+    SpecificParserAtomLookup<Latin1Char> lookup(seq);
+    HashNumber hash = lookup.hash();
+
+    auto maybeEntry =
+        ParserAtomEntry::allocateInline<Latin1Char>(cx, seq, len, hash);
+    if (maybeEntry.isErr()) {
+      return false;
+    }
+
+    length1StaticTable_[i] = maybeEntry.unwrap();
+    length1StaticTable_[i]->setStaticParserString1(StaticParserString1(i));
+  }
+
+  // Create known ParserAtoms for length-2 alpha-num strings.
+  constexpr size_t NUM_LENGTH2 = NUM_SMALL_CHARS * NUM_SMALL_CHARS;
+  for (size_t i = 0; i < NUM_LENGTH2; ++i) {
+    JS::AutoCheckCannotGC nogc;
+    JSAtom* atom = cx->staticStrings().getLength2FromIndex(i);
+
+    constexpr size_t len = 2;
+    MOZ_ASSERT(atom->length() == len);
+
+    InflatedChar16Sequence<Latin1Char> seq(atom->latin1Chars(nogc), len);
+    SpecificParserAtomLookup<Latin1Char> lookup(seq);
+    HashNumber hash = lookup.hash();
+
+    auto maybeEntry =
+        ParserAtomEntry::allocateInline<Latin1Char>(cx, seq, len, hash);
+    if (maybeEntry.isErr()) {
+      return false;
+    }
+
+    length2StaticTable_[i] = maybeEntry.unwrap();
+    length2StaticTable_[i]->setStaticParserString2(StaticParserString2(i));
+  }
+
+  return true;
+}
+
 bool WellKnownParserAtoms::init(JSContext* cx) {
+  // Initialize the tiny strings before common names since there are some short
+  // common names.
+  if (!initStaticStrings(cx)) {
+    return false;
+  }
+
 #define COMMON_NAME_INIT_(idpart, id, text)                \
   if (!initSingle(cx, &(id), text, WellKnownAtomId::id)) { \
     return false;                                          \
   }
   FOR_EACH_COMMON_PROPERTYNAME(COMMON_NAME_INIT_)
 #undef COMMON_NAME_INIT_
 
 #define COMMON_NAME_INIT_(name, clasp)                          \
--- a/js/src/frontend/ParserAtom.h
+++ b/js/src/frontend/ParserAtom.h
@@ -48,16 +48,20 @@ enum class WellKnownAtomId : uint32_t {
   FOR_EACH_COMMON_PROPERTYNAME(ENUM_ENTRY_)
 #undef ENUM_ENTRY_
 
 #define ENUM_ENTRY_(name, clasp) name,
       JS_FOR_EACH_PROTOTYPE(ENUM_ENTRY_)
 #undef ENUM_ENTRY_
 };
 
+// These types correspond into indices in the StaticStrings arrays.
+enum class StaticParserString1 : uint8_t;
+enum class StaticParserString2 : uint16_t;
+
 /**
  * A ParserAtomEntry is an in-parser representation of an interned atomic
  * string.  It mostly mirrors the information carried by a JSAtom*.
  *
  * The atom contents are stored in one of four locations:
  *  1. Inline Latin1Char storage (immediately after the ParserAtomEntry memory).
  *  2. Inline char16_t storage (immediately after the ParserAtomEntry memory).
  *  3. An owned pointer to a heap-allocated Latin1Char buffer.
@@ -213,17 +217,18 @@ class alignas(alignof(void*)) ParserAtom
   // Used to dynamically optimize the mapping of ParserAtoms to JSAtom*s.
   //
   // If this ParserAtomEntry is a part of WellKnownParserAtoms, this should
   // hold WellKnownAtomId that maps to an item in cx->names().
   //
   // Otherwise, this should hold AtomIndex into CompilationInfo.atoms,
   // or empty if the JSAtom isn't yet allocated.
   using AtomIndexType =
-      mozilla::Variant<mozilla::Nothing, AtomIndex, WellKnownAtomId>;
+      mozilla::Variant<mozilla::Nothing, AtomIndex, WellKnownAtomId,
+                       StaticParserString1, StaticParserString2>;
   mutable AtomIndexType atomIndex_ = AtomIndexType(mozilla::Nothing());
 
  public:
   static const uint32_t MAX_LENGTH = JSString::MAX_LENGTH;
 
   template <typename CharT>
   ParserAtomEntry(mozilla::UniquePtr<CharT[], JS::FreePolicy> chars,
                   uint32_t length, HashNumber hash)
@@ -300,16 +305,22 @@ class alignas(alignof(void*)) ParserAtom
   bool equalsSeq(HashNumber hash, InflatedChar16Sequence<CharT> seq) const;
 
   void setAtomIndex(AtomIndex index) const {
     atomIndex_ = mozilla::AsVariant(index);
   }
   void setWellKnownAtomId(WellKnownAtomId kind) const {
     atomIndex_ = mozilla::AsVariant(kind);
   }
+  void setStaticParserString1(StaticParserString1 s) const {
+    atomIndex_ = mozilla::AsVariant(s);
+  }
+  void setStaticParserString2(StaticParserString2 s) const {
+    atomIndex_ = mozilla::AsVariant(s);
+  }
 
   // Convert this entry to a js-atom.  The first time this method is called
   // the entry will cache the JSAtom pointer to return later.
   JS::Result<JSAtom*, OOM&> toJSAtom(JSContext* cx,
                                      CompilationInfo& compilationInfo) const;
 
   // Convert this entry to a number.
   bool toNumber(JSContext* cx, double* result) const;
@@ -363,19 +374,25 @@ struct ParserAtomLookupHasher {
   static inline HashNumber hash(const Lookup& l) { return l.hash(); }
   static inline bool match(const UniquePtr<ParserAtomEntry>& entry,
                            const Lookup& l) {
     return l.equalsEntry(entry.get());
   }
 };
 
 /**
- * WellKnown maintains a well-structured reference to common names.
- * A single instance of it is held on the main Runtime, and allows
- * for the looking up of names, but not addition after initialization.
+ * WellKnownParserAtoms reserves a set of common ParserAtoms on the JSRuntime
+ * in a read-only format to be used by parser. These reserved atoms can be
+ * translated to equivalent JSAtoms in constant time.
+ *
+ * The common-names set allows the parser to lookup up specific atoms in
+ * constant time.
+ *
+ * We also reserve tiny (length 1/2) parser-atoms for fast lookup similar to
+ * the js::StaticStrings mechanism. This speeds up parsing minified code.
  */
 class WellKnownParserAtoms {
  public:
   /* Various built-in or commonly-used names. */
 #define PROPERTYNAME_FIELD_(idpart, id, text) const ParserName* id{};
   FOR_EACH_COMMON_PROPERTYNAME(PROPERTYNAME_FIELD_)
 #undef PROPERTYNAME_FIELD_
 
@@ -383,27 +400,71 @@ class WellKnownParserAtoms {
   JS_FOR_EACH_PROTOTYPE(PROPERTYNAME_FIELD_)
 #undef PROPERTYNAME_FIELD_
 
  private:
   using EntrySet = HashSet<UniquePtr<ParserAtomEntry>, ParserAtomLookupHasher,
                            js::SystemAllocPolicy>;
   EntrySet entrySet_;
 
+  static const size_t ASCII_STATIC_LIMIT = 128U;
+  static const size_t NUM_SMALL_CHARS = StaticStrings::NUM_SMALL_CHARS;
+  UniquePtr<ParserAtomEntry> length1StaticTable_[ASCII_STATIC_LIMIT] = {};
+  UniquePtr<ParserAtomEntry>
+      length2StaticTable_[NUM_SMALL_CHARS * NUM_SMALL_CHARS] = {};
+
   bool initSingle(JSContext* cx, const ParserName** name, const char* str,
                   WellKnownAtomId kind);
 
+  bool initStaticStrings(JSContext* cx);
+
+  const ParserAtom* getLength1String(char16_t ch) const {
+    MOZ_ASSERT(ch < ASCII_STATIC_LIMIT);
+    size_t index = static_cast<size_t>(ch);
+    return length1StaticTable_[index]->asAtom();
+  }
+  const ParserAtom* getLength2String(char16_t ch0, char16_t ch1) const {
+    size_t index = StaticStrings::getLength2Index(ch0, ch1);
+    return length2StaticTable_[index]->asAtom();
+  }
+
  public:
   WellKnownParserAtoms() = default;
 
   bool init(JSContext* cx);
 
   template <typename CharT>
   const ParserAtom* lookupChar16Seq(
       const SpecificParserAtomLookup<CharT>& lookup) const;
+
+  // Fast-path tiny strings since they are abundant in minified code.
+  template <typename CharT>
+  const ParserAtom* lookupTiny(const CharT* charPtr, uint32_t length) const {
+    switch (length) {
+      case 0:
+        return empty;
+
+      case 1: {
+        if (char16_t(charPtr[0]) < ASCII_STATIC_LIMIT) {
+          return getLength1String(charPtr[0]);
+        }
+        break;
+      }
+
+      case 2:
+        if (StaticStrings::fitsInSmallChar(charPtr[0]) &&
+            StaticStrings::fitsInSmallChar(charPtr[1])) {
+          return getLength2String(charPtr[0], charPtr[1]);
+        }
+        break;
+    }
+
+    // No match on tiny Atoms
+    return nullptr;
+  }
 };
 
 /**
  * A ParserAtomsTable owns and manages the vector of ParserAtom entries
  * associated with a given compile session.
  */
 class ParserAtomsTable {
  private:
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -78,16 +78,17 @@ UNIFIED_SOURCES += [
     'testMutedErrors.cpp',
     'testNewObject.cpp',
     'testNewTargetInvokeConstructor.cpp',
     'testNullRoot.cpp',
     'testNumberToString.cpp',
     'testObjectEmulatingUndefined.cpp',
     'testOOM.cpp',
     'testParseJSON.cpp',
+    'testParserAtom.cpp',
     'testPersistentRooted.cpp',
     'testPreserveJitCode.cpp',
     'testPrintf.cpp',
     'testPrivateGCThingValue.cpp',
     'testProfileStrings.cpp',
     'testPromise.cpp',
     'testPropCache.cpp',
     'testReadableStream.cpp',
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testParserAtom.cpp
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Range.h"  // mozilla::Range
+#include "mozilla/Utf8.h"   // mozilla::Utf8Unit
+
+#include "frontend/ParserAtom.h"  // js::frontend::ParserAtomsTable
+#include "js/TypeDecls.h"         // JS::Latin1Char
+#include "jsapi-tests/tests.h"
+
+// Test empty strings behave consistently.
+BEGIN_TEST(testParserAtom_empty) {
+  using js::frontend::ParserAtom;
+  using js::frontend::ParserAtomsTable;
+
+  ParserAtomsTable atomTable(cx->runtime());
+
+  constexpr size_t len = 0;
+
+  const char ascii[] = {};
+  const JS::Latin1Char latin1[] = {};
+  const mozilla::Utf8Unit utf8[] = {};
+  const char16_t char16[] = {};
+
+  // Check that the well-known empty atom matches for different entry points.
+  const ParserAtom* ref = cx->parserNames().empty;
+  CHECK(ref);
+  CHECK(atomTable.internAscii(cx, ascii, len).unwrap() == ref);
+  CHECK(atomTable.internLatin1(cx, latin1, len).unwrap() == ref);
+  CHECK(atomTable.internUtf8(cx, utf8, len).unwrap() == ref);
+  CHECK(atomTable.internChar16(cx, char16, len).unwrap() == ref);
+
+  // Check concatenation works on empty atoms.
+  const ParserAtom* concat[] = {
+      cx->parserNames().empty,
+      cx->parserNames().empty,
+  };
+  mozilla::Range<const ParserAtom*> concatRange(concat, 2);
+  CHECK(atomTable.concatAtoms(cx, concatRange).unwrap() == ref);
+
+  return true;
+}
+END_TEST(testParserAtom_empty)
+
+// Test length-1 fast-path is consistent across entry points.
+BEGIN_TEST(testParserAtom_tiny1) {
+  using js::frontend::ParserAtom;
+  using js::frontend::ParserAtomsTable;
+
+  ParserAtomsTable atomTable(cx->runtime());
+
+  char16_t a = 'a';
+  const char ascii[] = {'a'};
+  JS::Latin1Char latin1[] = {'a'};
+  const mozilla::Utf8Unit utf8[] = {mozilla::Utf8Unit('a')};
+  char16_t char16[] = {'a'};
+
+  const ParserAtom* ref = cx->parserNames().lookupTiny(&a, 1);
+  CHECK(ref);
+  CHECK(atomTable.internAscii(cx, ascii, 1).unwrap() == ref);
+  CHECK(atomTable.internLatin1(cx, latin1, 1).unwrap() == ref);
+  CHECK(atomTable.internUtf8(cx, utf8, 1).unwrap() == ref);
+  CHECK(atomTable.internChar16(cx, char16, 1).unwrap() == ref);
+
+  const ParserAtom* concat[] = {
+      ref,
+      cx->parserNames().empty,
+  };
+  mozilla::Range<const ParserAtom*> concatRange(concat, 2);
+  CHECK(atomTable.concatAtoms(cx, concatRange).unwrap() == ref);
+
+  // Note: If Latin1-Extended characters become supported, then UTF-8 behaviour
+  // should be tested.
+  char16_t ae = 0x00E6;
+  CHECK(cx->parserNames().lookupTiny(&ae, 1) == nullptr);
+
+  return true;
+}
+END_TEST(testParserAtom_tiny1)
+
+// Test length-2 fast-path is consistent across entry points.
+BEGIN_TEST(testParserAtom_tiny2) {
+  using js::frontend::ParserAtom;
+  using js::frontend::ParserAtomsTable;
+
+  ParserAtomsTable atomTable(cx->runtime());
+
+  const char ascii[] = {'a', '0'};
+  JS::Latin1Char latin1[] = {'a', '0'};
+  const mozilla::Utf8Unit utf8[] = {mozilla::Utf8Unit('a'),
+                                    mozilla::Utf8Unit('0')};
+  char16_t char16[] = {'a', '0'};
+
+  const ParserAtom* ref = cx->parserNames().lookupTiny(ascii, 2);
+  CHECK(ref);
+  CHECK(atomTable.internAscii(cx, ascii, 2).unwrap() == ref);
+  CHECK(atomTable.internLatin1(cx, latin1, 2).unwrap() == ref);
+  CHECK(atomTable.internUtf8(cx, utf8, 2).unwrap() == ref);
+  CHECK(atomTable.internChar16(cx, char16, 2).unwrap() == ref);
+
+  const ParserAtom* concat[] = {
+      cx->parserNames().lookupTiny(ascii + 0, 1),
+      cx->parserNames().lookupTiny(ascii + 1, 1),
+  };
+  mozilla::Range<const ParserAtom*> concatRange(concat, 2);
+  CHECK(atomTable.concatAtoms(cx, concatRange).unwrap() == ref);
+
+  // Note: If Latin1-Extended characters become supported, then UTF-8 behaviour
+  // should be tested.
+  char16_t ae0[] = {0x00E6, '0'};
+  CHECK(cx->parserNames().lookupTiny(ae0, 2) == nullptr);
+
+  return true;
+}
+END_TEST(testParserAtom_tiny2)
+
+// "æ"    U+00E6
+// "π"    U+03C0
+// "🍕"   U+1F355
--- a/js/src/vm/StringType.h
+++ b/js/src/vm/StringType.h
@@ -43,16 +43,18 @@ class JS_FRIEND_API AutoStableStringChar
 
 }  // namespace JS
 
 namespace js {
 
 namespace frontend {
 
 class ParserAtom;
+class ParserAtomEntry;
+class WellKnownParserAtoms;
 
 }  // namespace frontend
 
 class StaticStrings;
 class PropertyName;
 
 /* The buffer length required to contain any unsigned 32-bit integer. */
 static const size_t UINT32_CHAR_BUFFER_LENGTH = sizeof("4294967295") - 1;
@@ -1262,16 +1264,21 @@ class LittleEndianChars {
 
   constexpr const uint8_t* get() { return current; }
 
  private:
   const uint8_t* current;
 };
 
 class StaticStrings {
+  // NOTE: The WellKnownParserAtoms rely on these tables and may need to be
+  //       update if these tables are changed.
+  friend class js::frontend::ParserAtomEntry;
+  friend class js::frontend::WellKnownParserAtoms;
+
  private:
   /* Bigger chars cannot be in a length-2 string. */
   static const size_t SMALL_CHAR_LIMIT = 128U;
   static const size_t NUM_SMALL_CHARS = 64U;
 
   JSAtom* length2StaticTable[NUM_SMALL_CHARS * NUM_SMALL_CHARS] = {};  // zeroes
 
  public:
@@ -1398,22 +1405,29 @@ class StaticStrings {
   static constexpr Latin1Char fromSmallChar(SmallChar c);
 
   static constexpr SmallChar toSmallChar(uint32_t c);
 
   static constexpr SmallCharArray createSmallCharArray();
 
   static const SmallCharArray toSmallCharArray;
 
-  MOZ_ALWAYS_INLINE JSAtom* getLength2(char16_t c1, char16_t c2) {
+  static MOZ_ALWAYS_INLINE size_t getLength2Index(char16_t c1, char16_t c2) {
     MOZ_ASSERT(fitsInSmallChar(c1));
     MOZ_ASSERT(fitsInSmallChar(c2));
-    size_t index = (size_t(toSmallCharArray[c1]) << 6) + toSmallCharArray[c2];
+    return (size_t(toSmallCharArray[c1]) << 6) + toSmallCharArray[c2];
+  }
+
+  MOZ_ALWAYS_INLINE JSAtom* getLength2FromIndex(size_t index) {
     return length2StaticTable[index];
   }
+
+  MOZ_ALWAYS_INLINE JSAtom* getLength2(char16_t c1, char16_t c2) {
+    return getLength2FromIndex(getLength2Index(c1, c2));
+  }
 };
 
 /*
  * Represents an atomized string which does not contain an index (that is, an
  * unsigned 32-bit value).  Thus for any PropertyName propname,
  * ToString(ToUint32(propname)) never equals propname.
  *
  * To more concretely illustrate the utility of PropertyName, consider that it