[dartdevc] Containerizing top-level variables to improve stepping times.

This change selectively containerizes many top-level JS constructs. Containers maintain a max size of 500 elements for JS optimization purposes. Containers with unspecified key names are emitted as JS arrays. Containerization occurs for symbols when the number of top level symbols exceeds 600 but is automatic for other constructs. However, containerization in the Dart SDK only happens for web libraries for runtime optimization purposes.

Two types of common DDC constructs result in their capture by all subsequent closures:
* Self-referential variables:
    var selfLambda = () => selfLambda = "foo";
* Variables referenced in sister functions:
    var unused = "foo";
    function unusedF() => console.log(unused);

The following are now accessed through an additional level of indirection:
* Type generators (frequently follow the self-referential variable pattern)
* Constant cache variables
* Dart private names (used for virtual method dispatch)
* Top-level string constants (such as URI strings)

Additional changes:
* The ModuleItemContainer handles automatic renaming of arbitrary top-level JS expressions. This is a multimap that maps keys of an arbitrary type to JS key-value pairs.
* The CacheTable and TypeTable have been unified. The old cache table seemed to have been created with a similar use case as the ModuleItemContainer but had become too specialized.
* The new TypeTable unifies much of the logic for emitting types, including explicitly separating the discharge of types dependent on free variables (such as generic types, generic functions, and their bounds) from types that are fully bound.

Reduces top-level captured variables in the SDK by ~5711 per recursive callback.

Change-Id: Id44efef8e93d1a83fdac808814d9ace99c4d27bd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/173800
Commit-Queue: Mark Zhou <markzipan@google.com>
Reviewed-by: Anna Gringauze <annagrin@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
diff --git a/pkg/dev_compiler/lib/src/compiler/module_containers.dart b/pkg/dev_compiler/lib/src/compiler/module_containers.dart
new file mode 100644
index 0000000..82f06b4
--- /dev/null
+++ b/pkg/dev_compiler/lib/src/compiler/module_containers.dart
@@ -0,0 +1,259 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+import '../compiler/js_names.dart' as js_ast;
+import '../js_ast/js_ast.dart' as js_ast;
+import '../js_ast/js_ast.dart' show js;
+
+/// Represents a top-level property hoisted to a top-level object.
+class ModuleItemData {
+  /// The container that holds this module item in the emitted JS.
+  js_ast.Identifier id;
+
+  /// This module item's key in the emitted JS.
+  ///
+  /// A LiteralString if this object is backed by a JS Object/Map.
+  /// A LiteralNumber if this object is backed by a JS Array.
+  js_ast.Literal jsKey;
+
+  /// This module item's value in the emitted JS.
+  js_ast.Expression jsValue;
+
+  ModuleItemData(this.id, this.jsKey, this.jsValue);
+}
+
+/// Holds variables emitted during code gen.
+///
+/// Associates a [K] with a container-unique JS key and arbitrary JS value.
+/// The container is emitted as a single object:
+/// ```
+/// var C = {
+///   jsKey: jsValue,
+///   ...
+/// };
+/// ```
+class ModuleItemContainer<K> {
+  /// Name of the container in the emitted JS.
+  String name;
+
+  /// If null, this container will be automatically renamed based on [name].
+  js_ast.Identifier containerId;
+
+  final Map<K, ModuleItemData> moduleItems = {};
+
+  /// Holds keys that will not be emitted when calling [emit].
+  final Set<K> _noEmit = {};
+
+  ModuleItemContainer._(this.name, this.containerId);
+
+  /// Creates an automatically sharding container backed by JS Objects.
+  factory ModuleItemContainer.asObject(String name,
+      {String Function(K) keyToString}) {
+    return ModuleItemObjectContainer<K>(name, keyToString);
+  }
+
+  /// Creates a container backed by a JS Array.
+  factory ModuleItemContainer.asArray(String name) {
+    return ModuleItemArrayContainer<K>(name);
+  }
+
+  bool get isNotEmpty => moduleItems.isNotEmpty;
+
+  Iterable<K> get keys => moduleItems.keys;
+
+  int get length => moduleItems.keys.length;
+
+  js_ast.Expression operator [](K key) => moduleItems[key]?.jsValue;
+
+  void operator []=(K key, js_ast.Expression value) {
+    if (moduleItems.containsKey(key)) {
+      moduleItems[key].jsValue = value;
+      return;
+    }
+    var fieldString = '$key';
+    // Avoid shadowing common JS properties.
+    if (js_ast.objectProperties.contains(fieldString)) {
+      fieldString += '\$';
+    }
+    moduleItems[key] = ModuleItemData(
+        containerId, js_ast.LiteralString("'$fieldString'"), value);
+  }
+
+  /// Returns the expression that retrieves [key]'s corresponding JS value via
+  /// a property access through its container.
+  js_ast.Expression access(K key) {
+    return js.call('#.#', [containerId, moduleItems[key].jsKey]);
+  }
+
+  /// Emit the container declaration/initializer.
+  ///
+  /// May be multiple statements if the container is automatically sharded.
+  List<js_ast.Statement> emit() {
+    var properties = <js_ast.Property>[];
+    moduleItems.forEach((k, v) {
+      if (!_noEmit.contains(k)) return;
+      properties.add(js_ast.Property(v.jsKey, v.jsValue));
+    });
+    var containerObject =
+        js_ast.ObjectInitializer(properties, multiline: properties.length > 1);
+    return [
+      js.statement('var # = Object.create(#)', [containerId, containerObject])
+    ];
+  }
+
+  bool contains(K key) => moduleItems.containsKey(key);
+
+  bool canEmit(K key) => !_noEmit.contains(key);
+
+  /// Indicates that [K] should be treated as if it weren't hoisted.
+  ///
+  /// Used when we are managing the variable declarations manually (such as
+  /// unhoisting specific symbols for performance reasons).
+  void setNoEmit(K key) {
+    _noEmit.add(key);
+  }
+}
+
+/// Associates a [K] with a container-unique JS key and arbitrary JS value.
+///
+/// Emitted as a series of JS Objects, splitting them into groups of 500 for
+/// JS optimization purposes:
+/// ```
+/// var C = {
+///   jsKey: jsValue,
+///   ...
+/// };
+/// var C$1 = { ... };
+/// ```
+class ModuleItemObjectContainer<K> extends ModuleItemContainer<K> {
+  /// Holds the TemporaryId for the current container shard.
+  js_ast.Identifier _currentContainerId;
+
+  /// Tracks how often JS emitted field names appear.
+  ///
+  /// [keyToString] may resolve multiple unique keys to the same JS string.
+  /// When this occurs, the resolved JS string will automatically be renamed.
+  final Map<String, int> _nameFrequencies = {};
+
+  /// Transforms a [K] into a valid name for a JS object property key.
+  ///
+  /// Non-unique generated strings are automatically renamed.
+  String Function(K) keyToString;
+
+  ModuleItemObjectContainer(String name, this.keyToString)
+      : super._(name, null);
+
+  @override
+  void operator []=(K key, js_ast.Expression value) {
+    if (this.contains(key)) {
+      moduleItems[key].jsValue = value;
+      return;
+    }
+    if (length % 500 == 0) _currentContainerId = js_ast.TemporaryId(name);
+    // Create a unique name for K when emitted as a JS field.
+    var fieldString = keyToString(key);
+    _nameFrequencies.update(fieldString, (v) {
+      fieldString += '\$${v + 1}';
+      return v + 1;
+    }, ifAbsent: () {
+      // Avoid shadowing common JS properties.
+      if (js_ast.objectProperties.contains(fieldString)) {
+        fieldString += '\$';
+      }
+      return 0;
+    });
+    moduleItems[key] = ModuleItemData(
+        _currentContainerId, js_ast.LiteralString("'$fieldString'"), value);
+  }
+
+  @override
+  js_ast.Expression access(K key) {
+    return js.call('#.#', [moduleItems[key].id, moduleItems[key].jsKey]);
+  }
+
+  @override
+  List<js_ast.Statement> emit() {
+    var containersToProperties = <js_ast.Identifier, List<js_ast.Property>>{};
+    moduleItems.forEach((k, v) {
+      if (_noEmit.contains(k)) return;
+      if (!containersToProperties.containsKey(v.id)) {
+        containersToProperties[v.id] = <js_ast.Property>[];
+      }
+      containersToProperties[v.id].add(js_ast.Property(v.jsKey, v.jsValue));
+    });
+
+    var statements = <js_ast.Statement>[];
+    containersToProperties.forEach((containerId, properties) {
+      var containerObject = js_ast.ObjectInitializer(properties,
+          multiline: properties.length > 1);
+      statements.add(js.statement(
+          'var # = Object.create(#)', [containerId, containerObject]));
+    });
+    return statements;
+  }
+}
+
+/// Associates a unique [K] with an arbitrary JS value.
+///
+/// Emitted as a JS Array:
+/// ```
+/// var C = [
+///   jsValue,
+///   ...
+/// ];
+/// ```
+class ModuleItemArrayContainer<K> extends ModuleItemContainer<K> {
+  ModuleItemArrayContainer(String name)
+      : super._(name, js_ast.TemporaryId(name));
+
+  @override
+  void operator []=(K key, js_ast.Expression value) {
+    if (moduleItems.containsKey(key)) {
+      moduleItems[key].jsValue = value;
+      return;
+    }
+    moduleItems[key] =
+        ModuleItemData(containerId, js_ast.LiteralNumber('$length'), value);
+  }
+
+  @override
+  js_ast.Expression access(K key) {
+    return js.call('#[#]', [containerId, moduleItems[key].jsKey]);
+  }
+
+  @override
+  List<js_ast.Statement> emit() {
+    if (moduleItems.isEmpty) return [];
+    var properties = List<js_ast.Expression>(length);
+
+    // If the entire array holds just one value, generate a short initializer.
+    var valueSet = <js_ast.Expression>{};
+    moduleItems.forEach((k, v) {
+      if (_noEmit.contains(k)) return;
+      valueSet.add(v.jsValue);
+      properties[int.parse((v.jsKey as js_ast.LiteralNumber).value)] =
+          v.jsValue;
+    });
+
+    if (valueSet.length == 1 && moduleItems.length > 1) {
+      return [
+        js.statement('var # = Array(#).fill(#)', [
+          containerId,
+          js_ast.LiteralNumber('${properties.length}'),
+          valueSet.first
+        ])
+      ];
+    }
+    // Array containers are not sharded, as we do not expect to hit V8's
+    // dictionary-mode limit of 99999 elements.
+    return [
+      js.statement('var # = #', [
+        containerId,
+        js_ast.ArrayInitializer(properties, multiline: properties.length > 1)
+      ])
+    ];
+  }
+}
diff --git a/pkg/dev_compiler/lib/src/compiler/shared_compiler.dart b/pkg/dev_compiler/lib/src/compiler/shared_compiler.dart
index e9527d9..2fcad3e 100644
--- a/pkg/dev_compiler/lib/src/compiler/shared_compiler.dart
+++ b/pkg/dev_compiler/lib/src/compiler/shared_compiler.dart
@@ -8,6 +8,7 @@
 import 'package:meta/meta.dart';
 
 import '../compiler/js_names.dart' as js_ast;
+import '../compiler/module_containers.dart' show ModuleItemContainer;
 import '../js_ast/js_ast.dart' as js_ast;
 import '../js_ast/js_ast.dart' show js;
 
@@ -25,6 +26,10 @@
   /// Private member names in this module, organized by their library.
   final _privateNames = HashMap<Library, HashMap<String, js_ast.TemporaryId>>();
 
+  /// Holds all top-level JS symbols (used for caching or indexing fields).
+  final _symbolContainer = ModuleItemContainer<js_ast.Identifier>.asObject('S',
+      keyToString: (js_ast.Identifier i) => '${i.name}');
+
   /// Extension member symbols for adding Dart members to JS types.
   ///
   /// These are added to the [extensionSymbolsModule]; see that field for more
@@ -51,11 +56,18 @@
   /// Whether we're currently building the SDK, which may require special
   /// bootstrapping logic.
   ///
-  /// This is initialized by [startModule], which must be called before
+  /// This is initialized by [emitModule], which must be called before
   /// accessing this field.
   @protected
   bool isBuildingSdk;
 
+  /// Whether or not to move top level symbols into top-level containers.
+  ///
+  /// This is set in both [emitModule] and [emitLibrary].
+  /// Depends on [isBuildingSdk].
+  @protected
+  bool containerizeSymbols;
+
   /// The temporary variable that stores named arguments (these are passed via a
   /// JS object literal, to match JS conventions).
   @protected
@@ -250,10 +262,16 @@
       var idName = name.endsWith('=') ? name.replaceAll('=', '_') : name;
       idName = idName.replaceAll(js_ast.invalidCharInIdentifier, '_');
       id ??= js_ast.TemporaryId(idName);
-      // TODO(vsm): Change back to `const`.
-      // See https://github.com/dart-lang/sdk/issues/40380.
-      moduleItems.add(js.statement('var # = #.privateName(#, #)',
-          [id, runtimeModule, emitLibraryName(library), js.string(name)]));
+      addSymbol(
+          id,
+          js.call('#.privateName(#, #)',
+              [runtimeModule, emitLibraryName(library), js.string(name)]));
+      if (!containerizeSymbols) {
+        // TODO(vsm): Change back to `const`.
+        // See https://github.com/dart-lang/sdk/issues/40380.
+        moduleItems.add(js.statement('var # = #.privateName(#, #)',
+            [id, runtimeModule, emitLibraryName(library), js.string(name)]));
+      }
       return id;
     }
 
@@ -338,9 +356,13 @@
     var name = js.escapedString(symbolName, "'");
     js_ast.Expression result;
     if (last.startsWith('_')) {
-      var nativeSymbol = emitPrivateNameSymbol(currentLibrary, last);
-      result = js.call('new #.new(#, #)',
-          [emitConstructorAccess(privateSymbolType), name, nativeSymbol]);
+      var nativeSymbolAccessor =
+          getSymbol(emitPrivateNameSymbol(currentLibrary, last));
+      result = js.call('new #.new(#, #)', [
+        emitConstructorAccess(privateSymbolType),
+        name,
+        nativeSymbolAccessor
+      ]);
     } else {
       result = js.call(
           'new #.new(#)', [emitConstructorAccess(internalSymbolType), name]);
@@ -366,12 +388,11 @@
   /// symbols into the list returned by this method. Finally, [finishModule]
   /// can be called to complete the module and return the resulting JS AST.
   ///
-  /// This also initializes several fields: [isBuildingSdk], [runtimeModule],
-  /// [extensionSymbolsModule], as well as the [_libraries] map needed by
+  /// This also initializes several fields: [runtimeModule],
+  /// [extensionSymbolsModule], and the [_libraries] map needed by
   /// [emitLibraryName].
   @protected
   List<js_ast.ModuleItem> startModule(Iterable<Library> libraries) {
-    isBuildingSdk = libraries.any(isSdkInternalRuntime);
     if (isBuildingSdk) {
       // Don't allow these to be renamed when we're building the SDK.
       // There is JS code in dart:* that depends on their names.
@@ -505,9 +526,13 @@
       if (isBuildingSdk) {
         value = js.call('# = Symbol(#)', [value, js.string('dartx.$name')]);
       }
-      // TODO(vsm): Change back to `const`.
-      // See https://github.com/dart-lang/sdk/issues/40380.
-      items.add(js.statement('var # = #;', [id, value]));
+      if (!_symbolContainer.canEmit(id)) {
+        // Extension symbols marked with noEmit are managed manually.
+        // TODO(vsm): Change back to `const`.
+        // See https://github.com/dart-lang/sdk/issues/40380.
+        items.add(js.statement('var # = #;', [id, value]));
+      }
+      _symbolContainer[id] = value;
     });
   }
 
@@ -534,6 +559,28 @@
         [runtimeModule, js.string(name), module, partMap]));
   }
 
+  /// Returns an accessor for [id] via the symbol container.
+  /// E.g., transforms $sym to S$5.$sym.
+  ///
+  /// A symbol lookup on an id marked no emit omits the symbol accessor.
+  js_ast.Expression getSymbol(js_ast.Identifier id) {
+    return _symbolContainer.canEmit(id) ? _symbolContainer.access(id) : id;
+  }
+
+  /// Returns the raw JS value associated with [id].
+  js_ast.Expression getSymbolValue(js_ast.Identifier id) {
+    return _symbolContainer[id];
+  }
+
+  /// Inserts a symbol into the symbol table.
+  js_ast.Expression addSymbol(js_ast.Identifier id, js_ast.Expression symbol) {
+    _symbolContainer[id] = symbol;
+    if (!containerizeSymbols) {
+      _symbolContainer.setNoEmit(id);
+    }
+    return _symbolContainer[id];
+  }
+
   /// Finishes the module created by [startModule], by combining the preable
   /// [items] with the [moduleItems] that have been emitted.
   ///
@@ -552,6 +599,9 @@
     // code between `startModule` and `finishModule` is very similar in both.
     _emitDebuggerExtensionInfo(moduleName);
 
+    // Emit all top-level JS symbol containers.
+    items.addAll(_symbolContainer.emit());
+
     // Add the module's code (produced by visiting compilation units, above)
     _copyAndFlattenBlocks(items, moduleItems);
     moduleItems.clear();
@@ -582,10 +632,13 @@
   /// handle the many details involved in naming.
   @protected
   js_ast.TemporaryId getExtensionSymbolInternal(String name) {
-    return _extensionSymbols.putIfAbsent(
-        name,
-        () => js_ast.TemporaryId(
-            '\$${js_ast.friendlyNameForDartOperator[name] ?? name}'));
+    if (!_extensionSymbols.containsKey(name)) {
+      var id = js_ast.TemporaryId(
+          '\$${js_ast.friendlyNameForDartOperator[name] ?? name}');
+      _extensionSymbols[name] = id;
+      addSymbol(id, id);
+    }
+    return _extensionSymbols[name];
   }
 
   /// Shorthand for identifier-like property names.
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index db85b79..8800d4c 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -22,6 +22,7 @@
 import '../compiler/js_utils.dart' as js_ast;
 import '../compiler/module_builder.dart'
     show isSdkInternalRuntimeUri, libraryUriToJsIdentifier, pathToJSIdentifier;
+import '../compiler/module_containers.dart' show ModuleItemContainer;
 import '../compiler/shared_command.dart' show SharedCompilerOptions;
 import '../compiler/shared_compiler.dart';
 import '../js_ast/js_ast.dart' as js_ast;
@@ -69,11 +70,14 @@
   /// Let variables collected for the given function.
   List<js_ast.TemporaryId> _letVariables;
 
-  final _constTable = _emitTemporaryId('CT');
+  final _constTable = js_ast.TemporaryId('CT');
 
-  // Constant getters used to populate the constant table.
+  /// Constant getters used to populate the constant table.
   final _constLazyAccessors = <js_ast.Method>[];
 
+  /// Container for holding the results of lazily-evaluated constants.
+  final _constTableCache = ModuleItemContainer<String>.asArray('C');
+
   /// Tracks the index in [moduleItems] where the const table must be inserted.
   /// Required for SDK builds due to internal circular dependencies.
   /// E.g., dart.constList depends on JSArray.
@@ -216,7 +220,7 @@
   final constAliasCache = HashMap<Constant, js_ast.Expression>();
 
   /// Maps uri strings in asserts and elsewhere to hoisted identifiers.
-  final _uriMap = HashMap<String, js_ast.Identifier>();
+  final _uriContainer = ModuleItemContainer<String>.asArray('I');
 
   final Class _jsArrayClass;
   final Class _privateSymbolClass;
@@ -323,7 +327,32 @@
 
     var libraries = component.libraries;
 
-    // Initialize our library variables.
+    // Initialize library variables.
+    isBuildingSdk = libraries.any(isSdkInternalRuntime);
+
+    // For runtime performance reasons, we only containerize SDK symbols in web
+    // libraries. Otherwise, we use a 600-member cutoff before a module is
+    // containerized. This is somewhat arbitrary but works promisingly for the
+    // SDK and Flutter Web.
+    if (!isBuildingSdk) {
+      // The number of DDC top-level symbols scales with the number of
+      // non-static class members across an entire module.
+      var uniqueNames = HashSet<String>();
+      libraries.forEach((Library l) {
+        l.classes.forEach((Class c) {
+          c.members.forEach((m) {
+            var isStatic =
+                m is Field ? m.isStatic : (m is Procedure ? m.isStatic : false);
+            if (isStatic) return;
+            var name = js_ast.toJSIdentifier(
+                m.name.text.replaceAll(js_ast.invalidCharInIdentifier, '_'));
+            uniqueNames.add(name);
+          });
+        });
+      });
+      containerizeSymbols = uniqueNames.length > 600;
+    }
+
     var items = startModule(libraries);
     _nullableInference.allowNotNullDeclarations = isBuildingSdk;
     _typeTable = TypeTable(runtimeModule);
@@ -335,10 +364,12 @@
       _pendingClasses.addAll(l.classes);
     }
 
-    // TODO(markzipan): Don't emit this when compiling the SDK.
-    moduleItems
-        .add(js.statement('const # = Object.create(null);', [_constTable]));
-    _constTableInsertionIndex = moduleItems.length;
+    // Record a safe index after the declaration of type generators and
+    // top-level symbols but before the declaration of any functions.
+    // Various preliminary data structures must be inserted here prior before
+    // referenced by the rest of the module.
+    var safeDeclarationIndex = moduleItems.length;
+    _constTableInsertionIndex = safeDeclarationIndex;
 
     // Add implicit dart:core dependency so it is first.
     emitLibraryName(_coreTypes.coreLibrary);
@@ -350,22 +381,25 @@
     // This is done by forward declaring items.
     libraries.forEach(_emitLibrary);
 
+    // Emit hoisted assert strings
+    moduleItems.insertAll(safeDeclarationIndex, _uriContainer.emit());
+
+    if (_constTableCache.isNotEmpty) {
+      moduleItems.insertAll(safeDeclarationIndex, _constTableCache.emit());
+    }
+
     // This can cause problems if it's ever true during the SDK build, as it's
     // emitted before dart.defineLazy.
     if (_constLazyAccessors.isNotEmpty) {
+      var constTableDeclaration =
+          js.statement('const # = Object.create(null);', [_constTable]);
       var constTableBody = runtimeStatement(
           'defineLazy(#, { # }, false)', [_constTable, _constLazyAccessors]);
-      moduleItems.insert(_constTableInsertionIndex, constTableBody);
+      moduleItems.insertAll(
+          _constTableInsertionIndex, [constTableDeclaration, constTableBody]);
       _constLazyAccessors.clear();
     }
 
-    // Add assert locations
-    _uriMap.forEach((location, id) {
-      var value = location == null ? 'null' : js.escapedString(location);
-      moduleItems.insert(
-          _constTableInsertionIndex, js.statement('var # = #;', [id, value]));
-    });
-
     moduleItems.addAll(afterClassDefItems);
     afterClassDefItems.clear();
 
@@ -375,9 +409,8 @@
     // Declare imports and extension symbols
     emitImportsAndExtensionSymbols(items);
 
-    // Discharge the type table cache variables and
-    // hoisted definitions.
-    items.addAll(_typeTable.discharge());
+    // Emit the hoisted type table cache variables
+    items.addAll(_typeTable.dischargeBoundTypes());
 
     return finishModule(items, _options.moduleName);
   }
@@ -442,6 +475,10 @@
     _currentLibrary = library;
     _staticTypeContext.enterLibrary(_currentLibrary);
 
+    if (isBuildingSdk) {
+      containerizeSymbols = _isWebLibrary(library.importUri);
+    }
+
     if (isSdkInternalRuntime(library)) {
       // `dart:_runtime` uses a different order for bootstrapping.
       //
@@ -566,10 +603,29 @@
     _classProperties =
         ClassPropertyModel.build(_types, _extensionTypes, _virtualFields, c);
 
+    var body = <js_ast.Statement>[];
+
+    // ClassPropertyModel.build introduces symbols for virtual field accessors.
+    _classProperties.virtualFields.forEach((field, virtualField) {
+      // TODO(vsm): Clean up this logic.
+      //
+      // Typically, [emitClassPrivateNameSymbol] creates a new symbol.  If it
+      // is called multiple times, that symbol is cached.  If the former,
+      // assign directly to [virtualField].  If the latter, copy the old
+      // variable to [virtualField].
+      var symbol = emitClassPrivateNameSymbol(c.enclosingLibrary,
+          getLocalClassName(c), field.name.text, virtualField);
+      if (symbol != virtualField) {
+        addSymbol(virtualField, getSymbolValue(symbol));
+        if (!containerizeSymbols) {
+          body.add(js.statement('const # = #;', [virtualField, symbol]));
+        }
+      }
+    });
+
     var jsCtors = _defineConstructors(c, className);
     var jsMethods = _emitClassMethods(c);
 
-    var body = <js_ast.Statement>[];
     _emitSuperHelperSymbols(body);
     // Deferred supertypes must be evaluated lazily while emitting classes to
     // prevent evaluating a JS expression for a deferred type from influencing
@@ -593,7 +649,6 @@
     // Attach caches on all canonicalized types.
     body.add(runtimeStatement('addTypeCaches(#)', [className]));
 
-    _emitVirtualFieldSymbols(c, body);
     _emitClassSignature(c, className, body);
     _initExtensionSymbols(c);
     if (!c.isMixinDeclaration) {
@@ -644,7 +699,7 @@
 
     var typeConstructor = js.call('(#) => { #; #; return #; }', [
       jsFormals,
-      _typeTable.discharge(formals),
+      _typeTable.dischargeFreeTypes(formals),
       body,
       className ?? _emitIdentifier(name)
     ]);
@@ -1525,7 +1580,13 @@
 
     var body = <js_ast.Statement>[];
     void emitFieldInit(Field f, Expression initializer, TreeNode hoverInfo) {
-      var access = _classProperties.virtualFields[f] ?? _declareMemberName(f);
+      var virtualField = _classProperties.virtualFields[f];
+
+      // Avoid calling getSymbol on _declareMemberName since _declareMemberName
+      // calls _emitMemberName downstream, which already invokes getSymbol.
+      var access = virtualField == null
+          ? _declareMemberName(f)
+          : getSymbol(virtualField);
       var jsInit = _visitInitializer(initializer, f.annotations);
       body.add(jsInit
           .toAssignExpression(js.call('this.#', [access])
@@ -1912,14 +1973,16 @@
   /// wrong behavior if a new field was declared.
   List<js_ast.Method> _emitVirtualFieldAccessor(Field field) {
     var virtualField = _classProperties.virtualFields[field];
+    var virtualFieldSymbol = getSymbol(virtualField);
     var name = _declareMemberName(field);
 
-    var getter = js.fun('function() { return this[#]; }', [virtualField]);
+    var getter = js.fun('function() { return this[#]; }', [virtualFieldSymbol]);
     var jsGetter = js_ast.Method(name, getter, isGetter: true)
       ..sourceInformation = _nodeStart(field);
 
-    var args =
-        field.isFinal ? [js_ast.Super(), name] : [js_ast.This(), virtualField];
+    var args = field.isFinal
+        ? [js_ast.Super(), name]
+        : [js_ast.This(), virtualFieldSymbol];
 
     js_ast.Expression value = _emitIdentifier('value');
     if (!field.isFinal && isCovariantField(field)) {
@@ -2235,13 +2298,13 @@
       var memberLibrary = member?.name?.library ??
           memberClass?.enclosingLibrary ??
           _currentLibrary;
-      return emitPrivateNameSymbol(memberLibrary, name);
+      return getSymbol(emitPrivateNameSymbol(memberLibrary, name));
     }
 
     useExtension ??= _isSymbolizedMember(memberClass, name);
     name = js_ast.memberNameForDartMember(name, _isExternal(member));
     if (useExtension) {
-      return getExtensionSymbolInternal(name);
+      return getSymbol(getExtensionSymbolInternal(name));
     }
     return propertyName(name);
   }
@@ -2858,7 +2921,7 @@
 
       js_ast.Expression addTypeFormalsAsParameters(
           List<js_ast.Expression> elements) {
-        var names = _typeTable.discharge(typeFormals);
+        var names = _typeTable.dischargeFreeTypes(typeFormals);
         return names.isEmpty
             ? js.call('(#) => [#]', [tf, elements])
             : js.call('(#) => {#; return [#];}', [tf, names, elements]);
@@ -3006,9 +3069,8 @@
     // Issue: https://github.com/dart-lang/sdk/issues/43288
     var fun = _emitFunction(functionNode, name);
 
-    var types = _typeTable.discharge();
+    var types = _typeTable?.dischargeFreeTypes();
     var constants = _dischargeConstTable();
-
     var body = js_ast.Block([...?types, ...?constants, ...fun.body.statements]);
     return js_ast.Fun(fun.params, body);
   }
@@ -3102,22 +3164,6 @@
     return result;
   }
 
-  void _emitVirtualFieldSymbols(Class c, List<js_ast.Statement> body) {
-    _classProperties.virtualFields.forEach((field, virtualField) {
-      // TODO(vsm): Clean up this logic.  See comments on the following method.
-      //
-      // Typically, [emitClassPrivateNameSymbol] creates a new symbol.  If it
-      // is called multiple times, that symbol is cached.  If the former,
-      // assign directly to [virtualField].  If the latter, copy the old
-      // variable to [virtualField].
-      var symbol = emitClassPrivateNameSymbol(c.enclosingLibrary,
-          getLocalClassName(c), field.name.text, virtualField);
-      if (symbol != virtualField) {
-        body.add(js.statement('const # = #;', [virtualField, symbol]));
-      }
-    });
-  }
-
   List<js_ast.Identifier> _emitTypeFormals(List<TypeParameter> typeFormals) {
     return typeFormals
         .map((t) => _emitIdentifier(getTypeParameterName(t)))
@@ -3644,14 +3690,11 @@
 
   // Replace a string `uri` literal with a cached top-level variable containing
   // the value to reduce overall code size.
-  js_ast.Identifier _cacheUri(String uri) {
-    var id = _uriMap[uri];
-    if (id == null) {
-      var name = 'L${_uriMap.length}';
-      id = js_ast.TemporaryId(name);
-      _uriMap[uri] = id;
+  js_ast.Expression _cacheUri(String uri) {
+    if (!_uriContainer.contains(uri)) {
+      _uriContainer[uri] = js_ast.LiteralString('"$uri"');
     }
-    return id;
+    return _uriContainer.access(uri);
   }
 
   @override
@@ -5004,7 +5047,7 @@
           return _emitType(firstArg.type);
         }
         if (name == 'extensionSymbol' && firstArg is StringLiteral) {
-          return getExtensionSymbolInternal(firstArg.value);
+          return getSymbol(getExtensionSymbolInternal(firstArg.value));
         }
 
         if (name == 'compileTimeFlag' && firstArg is StringLiteral) {
@@ -5880,19 +5923,19 @@
     }
     var constAliasString = 'C${constAliasCache.length}';
     var constAliasProperty = propertyName(constAliasString);
-    var constAliasId = _emitTemporaryId(constAliasString);
-    var constAccessor =
-        js.call('# || #.#', [constAliasId, _constTable, constAliasProperty]);
+
+    _constTableCache[constAliasString] = js.call('void 0');
+    var constAliasAccessor = _constTableCache.access(constAliasString);
+
+    var constAccessor = js.call(
+        '# || #.#', [constAliasAccessor, _constTable, constAliasProperty]);
     constAliasCache[node] = constAccessor;
     var constJs = super.visitConstant(node);
-    // TODO(vsm): Change back to `let`.
-    // See https://github.com/dart-lang/sdk/issues/40380.
-    moduleItems.add(js.statement('var #;', [constAliasId]));
 
     var func = js_ast.Fun(
         [],
         js_ast.Block([
-          js.statement('return # = #;', [constAliasId, constJs])
+          js.statement('return # = #;', [constAliasAccessor, constJs])
         ]));
     var accessor = js_ast.Method(constAliasProperty, func, isGetter: true);
     _constLazyAccessors.add(accessor);
@@ -5978,8 +6021,8 @@
       // was overridden.
       var symbol = cls.isEnum
           ? _emitMemberName(member.name.text, member: member)
-          : emitClassPrivateNameSymbol(
-              cls.enclosingLibrary, getLocalClassName(cls), member.name.text);
+          : getSymbol(emitClassPrivateNameSymbol(
+              cls.enclosingLibrary, getLocalClassName(cls), member.name.text));
       return js_ast.Property(symbol, constant);
     }
 
diff --git a/pkg/dev_compiler/lib/src/kernel/type_table.dart b/pkg/dev_compiler/lib/src/kernel/type_table.dart
index ddaf27f..b9b73fd 100644
--- a/pkg/dev_compiler/lib/src/kernel/type_table.dart
+++ b/pkg/dev_compiler/lib/src/kernel/type_table.dart
@@ -4,13 +4,17 @@
 
 // @dart = 2.9
 
+import 'dart:collection';
+
 import 'package:kernel/kernel.dart';
 
 import '../compiler/js_names.dart' as js_ast;
+import '../compiler/module_containers.dart' show ModuleItemContainer;
 import '../js_ast/js_ast.dart' as js_ast;
 import '../js_ast/js_ast.dart' show js;
 import 'kernel_helpers.dart';
 
+/// Returns all non-locally defined type parameters referred to by [t].
 Set<TypeParameter> freeTypeParameters(DartType t) {
   assert(isKnownDartTypeImplementor(t));
   var result = <TypeParameter>{};
@@ -36,160 +40,144 @@
   return result;
 }
 
-/// _CacheTable tracks cache variables for variables that
-/// are emitted in place with a hoisted variable for a cache.
-class _CacheTable {
-  /// Mapping from types to their canonical names.
-  // Use a LinkedHashMap to maintain key insertion order so the generated code
-  // is stable under slight perturbation.  (If this is not good enough we could
-  // sort by name to canonicalize order.)
-  final _names = <DartType, js_ast.TemporaryId>{};
-  Iterable<DartType> get keys => _names.keys.toList();
-
-  js_ast.Statement _dischargeType(DartType type) {
-    var name = _names.remove(type);
-    if (name != null) {
-      return js.statement('let #;', [name]);
-    }
-    return null;
+/// A name for a type made of JS identifier safe characters.
+///
+/// 'L' and 'N' are prepended to a type name to represent a legacy or nullable
+/// flavor of a type.
+String _typeString(DartType type, {bool flat = false}) {
+  var nullability = type.declaredNullability == Nullability.legacy
+      ? 'L'
+      : type.declaredNullability == Nullability.nullable
+          ? 'N'
+          : '';
+  assert(isKnownDartTypeImplementor(type));
+  if (type is InterfaceType) {
+    var name = '${type.classNode.name}$nullability';
+    var typeArgs = type.typeArguments;
+    if (typeArgs == null) return name;
+    if (typeArgs.every((p) => p == const DynamicType())) return name;
+    return "${name}Of${typeArgs.map(_typeString).join("\$")}";
   }
-
-  /// Emit a list of statements declaring the cache variables for
-  /// types tracked by this table.  If [typeFilter] is given,
-  /// only emit the types listed in the filter.
-  List<js_ast.Statement> discharge([Iterable<DartType> typeFilter]) {
-    var decls = <js_ast.Statement>[];
-    var types = typeFilter ?? keys;
-    for (var t in types) {
-      var stmt = _dischargeType(t);
-      if (stmt != null) decls.add(stmt);
-    }
-    return decls;
+  if (type is FutureOrType) {
+    var name = 'FutureOr$nullability';
+    if (type.typeArgument == const DynamicType()) return name;
+    return '${name}Of${_typeString(type.typeArgument)}';
   }
-
-  bool isNamed(DartType type) => _names.containsKey(type);
-
-  /// A name for a type made of JS identifier safe characters.
-  ///
-  /// 'L' and 'N' are prepended to a type name to represent a legacy or nullable
-  /// flavor of a type.
-  String _typeString(DartType type, {bool flat = false}) {
-    var nullability = type.declaredNullability == Nullability.legacy
-        ? 'L'
-        : type.declaredNullability == Nullability.nullable
-            ? 'N'
-            : '';
-    assert(isKnownDartTypeImplementor(type));
-    if (type is InterfaceType) {
-      var name = '${type.classNode.name}$nullability';
-      var typeArgs = type.typeArguments;
-      if (typeArgs == null) return name;
-      if (typeArgs.every((p) => p == const DynamicType())) return name;
-      return "${name}Of${typeArgs.map(_typeString).join("\$")}";
-    }
-    if (type is FutureOrType) {
-      var name = 'FutureOr$nullability';
-      if (type.typeArgument == const DynamicType()) return name;
-      return '${name}Of${_typeString(type.typeArgument)}';
-    }
-    if (type is TypedefType) {
-      var name = '${type.typedefNode.name}$nullability';
-      var typeArgs = type.typeArguments;
-      if (typeArgs == null) return name;
-      if (typeArgs.every((p) => p == const DynamicType())) return name;
-      return "${name}Of${typeArgs.map(_typeString).join("\$")}";
-    }
-    if (type is FunctionType) {
-      if (flat) return 'Fn';
-      var rType = _typeString(type.returnType, flat: true);
-      var params = type.positionalParameters
-          .take(3)
-          .map((p) => _typeString(p, flat: true));
-      var paramList = params.join('And');
-      var count = type.positionalParameters.length;
-      if (count > 3 || type.namedParameters.isNotEmpty) {
-        paramList = '${paramList}__';
-      } else if (count == 0) {
-        paramList = 'Void';
-      }
-      return '${paramList}To$nullability$rType';
-    }
-    if (type is TypeParameterType) return '${type.parameter.name}$nullability';
-    if (type is DynamicType) return 'dynamic';
-    if (type is VoidType) return 'void';
-    if (type is NeverType) return 'Never$nullability';
-    if (type is BottomType) return 'bottom';
-    if (type is NullType) return 'Null';
-    return 'invalid';
+  if (type is TypedefType) {
+    var name = '${type.typedefNode.name}$nullability';
+    var typeArgs = type.typeArguments;
+    if (typeArgs == null) return name;
+    if (typeArgs.every((p) => p == const DynamicType())) return name;
+    return "${name}Of${typeArgs.map(_typeString).join("\$")}";
   }
-
-  /// Heuristically choose a good name for the cache and generator
-  /// variables.
-  js_ast.TemporaryId chooseTypeName(DartType type) {
-    return js_ast.TemporaryId(escapeIdentifier(_typeString(type)));
-  }
-}
-
-/// _GeneratorTable tracks types which have been
-/// named and hoisted.
-class _GeneratorTable extends _CacheTable {
-  final _defs = <DartType, js_ast.Expression>{};
-
-  final js_ast.Identifier _runtimeModule;
-
-  _GeneratorTable(this._runtimeModule);
-
-  @override
-  js_ast.Statement _dischargeType(DartType t) {
-    var name = _names.remove(t);
-    if (name != null) {
-      var init = _defs.remove(t);
-      assert(init != null);
-      // TODO(vsm): Change back to `let`.
-      // See https://github.com/dart-lang/sdk/issues/40380.
-      return js.statement('var # = () => ((# = #.constFn(#))());',
-          [name, name, _runtimeModule, init]);
+  if (type is FunctionType) {
+    if (flat) return 'Fn';
+    var rType = _typeString(type.returnType, flat: true);
+    var params = type.positionalParameters
+        .take(3)
+        .map((p) => _typeString(p, flat: true));
+    var paramList = params.join('And');
+    var count = type.positionalParameters.length;
+    if (count > 3 || type.namedParameters.isNotEmpty) {
+      paramList = '${paramList}__';
+    } else if (count == 0) {
+      paramList = 'Void';
     }
-    return null;
+    return '${paramList}To$nullability$rType';
   }
-
-  /// If [type] does not already have a generator name chosen for it,
-  /// assign it one, using [typeRep] as the initializer for it.
-  /// Emit the generator name.
-  js_ast.TemporaryId _nameType(DartType type, js_ast.Expression typeRep) {
-    var temp = _names[type];
-    if (temp == null) {
-      _names[type] = temp = chooseTypeName(type);
-      _defs[type] = typeRep;
-    }
-    return temp;
-  }
+  if (type is TypeParameterType) return '${type.parameter.name}$nullability';
+  if (type is DynamicType) return 'dynamic';
+  if (type is VoidType) return 'void';
+  if (type is NeverType) return 'Never$nullability';
+  if (type is BottomType) return 'bottom';
+  if (type is NullType) return 'Null';
+  return 'invalid';
 }
 
 class TypeTable {
-  /// Generator variable names for hoisted types.
-  final _GeneratorTable _generators;
-
   /// Mapping from type parameters to the types which must have their
   /// cache/generator variables discharged at the binding site for the
   /// type variable since the type definition depends on the type
   /// parameter.
   final _scopeDependencies = <TypeParameter, List<DartType>>{};
 
-  TypeTable(js_ast.Identifier runtime) : _generators = _GeneratorTable(runtime);
+  /// Contains types with any free type parameters and maps them to a unique
+  /// JS identifier.
+  ///
+  /// Used to reference types hoisted to the top of a generic class or generic
+  /// function (as opposed to the top of the entire module).
+  final _unboundTypeIds = HashMap<DartType, js_ast.Identifier>();
 
-  /// Emit a list of statements declaring the cache variables and generator
-  /// definitions tracked by the table.  If [formals] is present, only
-  /// emit the definitions which depend on the formals.
-  List<js_ast.Statement> discharge([List<TypeParameter> formals]) {
-    var filter = formals?.expand((p) => _scopeDependencies[p] ?? <DartType>[]);
-    var stmts = _generators.discharge(filter);
-    formals?.forEach(_scopeDependencies.remove);
-    return stmts;
+  /// Holds JS type generators keyed by their underlying DartType.
+  final typeContainer = ModuleItemContainer<DartType>.asObject('T',
+      keyToString: (DartType t) => escapeIdentifier(_typeString(t)));
+
+  final js_ast.Identifier _runtimeModule;
+
+  TypeTable(this._runtimeModule);
+
+  /// Returns true if [type] is already recorded in the table.
+  bool _isNamed(DartType type) =>
+      typeContainer.contains(type) || _unboundTypeIds.containsKey(type);
+
+  /// Emit the initializer statements for the type container, which contains
+  /// all named types with fully bound type parameters.
+  List<js_ast.Statement> dischargeBoundTypes() {
+    for (var t in typeContainer.keys) {
+      typeContainer[t] = js.call('() => ((# = #.constFn(#))())',
+          [typeContainer.access(t), _runtimeModule, typeContainer[t]]);
+    }
+    return typeContainer.emit();
   }
 
-  /// Record the dependencies of the type on its free variables
+  js_ast.Statement _dischargeFreeType(DartType type) {
+    typeContainer.setNoEmit(type);
+    var init = typeContainer[type];
+    var id = _unboundTypeIds[type];
+    // TODO(vsm): Change back to `let`.
+    // See https://github.com/dart-lang/sdk/issues/40380.
+    return js.statement('var # = () => ((# = #.constFn(#))());',
+        [id, id, _runtimeModule, init]);
+  }
+
+  /// Emit a list of statements declaring the cache variables and generator
+  /// definitions tracked by the table so far.
+  ///
+  /// If [formals] is present, only emit the definitions which depend on the
+  /// formals.
+  List<js_ast.Statement> dischargeFreeTypes([Iterable<TypeParameter> formals]) {
+    var decls = <js_ast.Statement>[];
+    var types = formals == null
+        ? typeContainer.keys.where((p) => freeTypeParameters(p).isNotEmpty)
+        : formals.expand((p) => _scopeDependencies[p] ?? <DartType>[]).toSet();
+
+    for (var t in types) {
+      var stmt = _dischargeFreeType(t);
+      if (stmt != null) decls.add(stmt);
+    }
+    return decls;
+  }
+
+  /// Emit a JS expression that evaluates to the generator for [type].
+  ///
+  /// If [type] does not already have a generator name chosen for it,
+  /// assign it one, using [typeRep] as its initializer.
+  js_ast.Expression _nameType(DartType type, js_ast.Expression typeRep) {
+    if (!typeContainer.contains(type)) {
+      typeContainer[type] = typeRep;
+    }
+    return _unboundTypeIds[type] ?? typeContainer.access(type);
+  }
+
+  /// Record the dependencies of the type on its free variables.
+  ///
+  /// Returns true if [type] is a free type parameter (but not a bound) and so
+  /// is not locally hoisted.
   bool recordScopeDependencies(DartType type) {
+    if (_isNamed(type)) {
+      return false;
+    }
+
     var freeVariables = freeTypeParameters(type);
     // TODO(leafp): This is a hack to avoid trying to hoist out of
     // generic functions and generic function types.  This often degrades
@@ -200,6 +188,17 @@
       return true;
     }
 
+    // This is only reached when [type] is itself a bound that depends on a
+    // free type parameter.
+    // TODO(markzipan): Bounds are locally hoisted to their own JS identifiers,
+    // but we don't do this this for other types that depend on free variables,
+    // resulting in some duplicated runtime code. We may get some performance
+    // wins if we just locally hoist everything.
+    if (freeVariables.isNotEmpty) {
+      _unboundTypeIds[type] =
+          js_ast.TemporaryId(escapeIdentifier(_typeString(type)));
+    }
+
     for (var free in freeVariables) {
       // If `free` is a promoted type parameter, get the original one so we can
       // find it in our map.
@@ -212,10 +211,10 @@
   /// add the type and its representation to the table, returning an
   /// expression which implements the type (but which caches the value).
   js_ast.Expression nameType(DartType type, js_ast.Expression typeRep) {
-    if (!_generators.isNamed(type) && recordScopeDependencies(type)) {
+    if (recordScopeDependencies(type)) {
       return typeRep;
     }
-    var name = _generators._nameType(type, typeRep);
+    var name = _nameType(type, typeRep);
     return js.call('#()', [name]);
   }
 
@@ -228,10 +227,10 @@
   js_ast.Expression nameFunctionType(
       FunctionType type, js_ast.Expression typeRep,
       {bool lazy = false}) {
-    if (!_generators.isNamed(type) && recordScopeDependencies(type)) {
+    if (recordScopeDependencies(type)) {
       return lazy ? js_ast.ArrowFun([], typeRep) : typeRep;
     }
-    var name = _generators._nameType(type, typeRep);
+    var name = _nameType(type, typeRep);
     return lazy ? name : js.call('#()', [name]);
   }
 }