Make file cache size configurable via a tsconfig argument.

PiperOrigin-RevId: 182509779
diff --git a/internal/common/tsconfig.bzl b/internal/common/tsconfig.bzl
index 407a4a4..a663ad6 100644
--- a/internal/common/tsconfig.bzl
+++ b/internal/common/tsconfig.bzl
@@ -118,6 +118,12 @@
   else:
     bazel_options["allowedStrictDeps"] = [f.path for f in allowed_deps]
 
+  if "TYPESCRIPT_WORKER_CACHE_SIZE_MB" in ctx.var:
+    max_cache_size_mb = int(ctx.var["TYPESCRIPT_WORKER_CACHE_SIZE_MB"])
+    if max_cache_size_mb < 0:
+      fail("TYPESCRIPT_WORKER_CACHE_SIZE_MB set to a negative value (%d)." % max_cache_size_mb)
+    bazel_options["maxCacheSizeMb"] = max_cache_size_mb
+
   # Keep these options in sync with those in playground/playground.ts.
   compiler_options = {
       # De-sugar to this language level
diff --git a/internal/tsc_wrapped/file_cache.ts b/internal/tsc_wrapped/file_cache.ts
index 176468d..a335570 100644
--- a/internal/tsc_wrapped/file_cache.ts
+++ b/internal/tsc_wrapped/file_cache.ts
@@ -31,10 +31,11 @@
   inCache(key: string): boolean;
 }
 
-// Cache at most to this amount of memory use.  It appears that
-// without the cache involved, our steady state size after parsing is
-// in the ~150mb range.
-const MAX_CACHE_SIZE = 300 * (1 << 20 /* 1 MB */);
+/**
+ * Default cache size. Without the cache involved, our steady state size
+ * after parsing is in the ~150mb range.
+ */
+const DEFAULT_MAX_CACHE_SIZE = 300 * (1 << 20 /* 1 MB */);
 
 /**
  * FileCache is a trivial LRU cache for bazel outputs.
@@ -53,13 +54,29 @@
    * digest to assign to a newly loaded file.
    */
   private lastDigests: {[filePath: string]: string} = {};
-  public cacheStats = {
+
+  cacheStats = {
     hits: 0,
     reads: 0,
     readTimeMs: 0,
   };
 
-  constructor(private debug: (...msg: any[]) => void) {}
+  private maxCacheSize = DEFAULT_MAX_CACHE_SIZE;
+
+  constructor(private debug: (...msg: Array<{}>) => void) {}
+
+  setMaxCacheSize(maxCacheSize: number) {
+    if (maxCacheSize < 0) {
+      throw new Error(`FileCache max size is negative: ${maxCacheSize}`);
+    }
+    this.debug('FileCache max size is', maxCacheSize >> 20, 'MB');
+    this.maxCacheSize = maxCacheSize;
+    this.maybeFreeMemory();
+  }
+
+  resetMaxCacheSize() {
+    this.setMaxCacheSize(DEFAULT_MAX_CACHE_SIZE);
+  }
 
   /**
    * Updates the cache with the given digests.
@@ -112,18 +129,7 @@
   putCache(filePath: string, entry: CacheEntry<CachedType>): void {
     const readStart = Date.now();
     this.cacheStats.readTimeMs += Date.now() - readStart;
-
-    let dropped = 0;
-    if (this.shouldFreeMemory()) {
-      // Drop half the cache, the least recently used entry == the first
-      // entry.
-      this.debug('Evicting from the cache');
-      const keys = Object.keys(this.fileCache);
-      dropped = Math.round(keys.length / 2);
-      for (let i = 0; i < dropped; i++) {
-        delete this.fileCache[keys[i]];
-      }
-    }
+    const dropped = this.maybeFreeMemory();
     this.fileCache[filePath] = entry;
     this.debug('Loaded', filePath, 'dropped', dropped, 'cache entries');
   }
@@ -178,8 +184,25 @@
    * Defined as a property so it can be overridden in tests.
    */
   shouldFreeMemory: () => boolean = () => {
-    return process.memoryUsage().heapUsed > MAX_CACHE_SIZE;
+    return process.memoryUsage().heapUsed > this.maxCacheSize;
   };
+
+  /**
+   * Frees memory if required. Returns the number of dropped entries.
+   */
+  private maybeFreeMemory() {
+    if (!this.shouldFreeMemory()) {
+      return 0;
+    }
+    // Drop half the cache, the least recently used entry == the first entry.
+    this.debug('Evicting from the cache');
+    const keys = Object.keys(this.fileCache);
+    const dropped = Math.round(keys.length / 2);
+    for (let i = 0; i < dropped; i++) {
+      delete this.fileCache[keys[i]];
+    }
+    return dropped;
+  }
 }
 
 /**
diff --git a/internal/tsc_wrapped/file_cache_test.ts b/internal/tsc_wrapped/file_cache_test.ts
index 2f5d692..1c00f39 100644
--- a/internal/tsc_wrapped/file_cache_test.ts
+++ b/internal/tsc_wrapped/file_cache_test.ts
@@ -50,11 +50,12 @@
   });
 
   it('caches in LRU order', () => {
-    let free = false;
     const fileCache = new FileCache<ts.SourceFile>(fauxDebug);
-    const fileLoader = new CachedFileLoader(fileCache, true);
+    let free = false;
     fileCache.shouldFreeMemory = () => free;
 
+    const fileLoader = new CachedFileLoader(fileCache, true);
+
     function load(name: string, fn: string) {
       return fileLoader.loadFile(name, fn, ts.ScriptTarget.ES5);
     }
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 27451ab..88a40e4 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -39,11 +39,33 @@
  */
 function runOneBuild(
     args: string[], inputs?: {[path: string]: string}): boolean {
+  // Strip leading at-signs, used in build_defs.bzl to indicate a params file
+  const tsconfigFile = args[0].replace(/^@+/, '');
+
+  const [parsed, errors, {target}] = parseTsconfig(tsconfigFile);
+  if (errors) {
+    console.error(diagnostics.format(target, errors));
+    return false;
+  }
+  if (!parsed) {
+    throw new Error(
+        'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
+  }
+  const {options, bazelOpts, files} = parsed;
+
   // Reset cache stats.
   fileCache.resetStats();
   fileCache.traceStats();
+  if (bazelOpts.maxCacheSizeMb !== undefined) {
+    const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * 1 << 20;
+    fileCache.setMaxCacheSize(maxCacheSizeBytes);
+  } else {
+    fileCache.resetMaxCacheSize();
+  }
+
   let fileLoader: FileLoader;
   const allowNonHermeticReads = true;
+
   if (inputs) {
     fileLoader = new CachedFileLoader(fileCache, allowNonHermeticReads);
     // Resolve the inputs to absolute paths to match TypeScript internals
@@ -60,19 +82,7 @@
     console.error('Expected one argument: path to tsconfig.json');
     return false;
   }
-  // Strip leading at-signs, used in build_defs.bzl to indicate a params file
-  const tsconfigFile = args[0].replace(/^@+/, '');
 
-  const [parsed, errors, {target}] = parseTsconfig(tsconfigFile);
-  if (errors) {
-    console.error(diagnostics.format(target, errors));
-    return false;
-  }
-  if (!parsed) {
-    throw new Error(
-        'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
-  }
-  const {options, bazelOpts, files} = parsed;
   const compilerHostDelegate =
       ts.createCompilerHost({target: ts.ScriptTarget.ES5});
 
diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts
index 9a10023..8475b36 100644
--- a/internal/tsc_wrapped/tsconfig.ts
+++ b/internal/tsc_wrapped/tsconfig.ts
@@ -111,6 +111,11 @@
    * ಠ_ಠ.clutz namespace.
    */
   addDtsClutzAliases: true;
+
+  /**
+   * The maximum cache size for bazel outputs, in megabytes.
+   */
+  maxCacheSizeMb?: number;
 }
 
 export interface ParsedTsConfig {