Introduce helper gatherDiagnostics in tsc_wrapped

PiperOrigin-RevId: 221529157
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 747be13..d98ec95 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -6,12 +6,15 @@
 
 import {CachedFileLoader, FileLoader, ProgramAndFileCache, UncachedFileLoader} from './cache';
 import {CompilerHost} from './compiler_host';
-import * as diagnostics from './diagnostics';
-import {wrap} from './perf_trace';
+import * as bazelDiagnostics from './diagnostics';
+import * as perfTrace from './perf_trace';
 import {PLUGIN as strictDepsPlugin} from './strict_deps';
-import {parseTsconfig, resolveNormalizedPath} from './tsconfig';
+import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
 import {debug, log, runAsWorker, runWorkerLoop} from './worker';
 
+/**
+ * Top-level entry point for tsc_wrapped.
+ */
 export function main(args: string[]) {
   if (runAsWorker(args)) {
     log('Starting TypeScript compiler persistent worker...');
@@ -28,76 +31,21 @@
   return 0;
 }
 
-// The one ProgramAndFileCache instance used in this process.
+/** The one ProgramAndFileCache instance used in this process. */
 const cache = new ProgramAndFileCache(debug);
 
+function isCompilationTarget(
+    bazelOpts: BazelOptions, sf: ts.SourceFile): boolean {
+  return (bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1);
+}
+
 /**
- * Runs a single build, returning false on failure.  This is potentially called
- * multiple times (once per bazel request) when running as a bazel worker.
- * Any encountered errors are written to stderr.
+ * Gather diagnostics from TypeScript's type-checker as well as other plugins we
+ * install such as strict dependency checking.
  */
-function runOneBuild(
-    args: string[], inputs?: {[path: string]: string}): boolean {
-  if (args.length !== 1) {
-    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, disabledTsetseRules} = parsed;
-
-  // Reset cache stats.
-  cache.resetStats();
-  cache.traceStats();
-  if (bazelOpts.maxCacheSizeMb !== undefined) {
-    const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * 1 << 20;
-    cache.setMaxCacheSize(maxCacheSizeBytes);
-  } else {
-    cache.resetMaxCacheSize();
-  }
-
-  let fileLoader: FileLoader;
-  const allowActionInputReads = true;
-
-  if (inputs) {
-    fileLoader = new CachedFileLoader(cache);
-    // Resolve the inputs to absolute paths to match TypeScript internals
-    const resolvedInputs = new Map<string, string>();
-    for (const key of Object.keys(inputs)) {
-      resolvedInputs.set(resolveNormalizedPath(key), inputs[key]);
-    }
-    cache.updateCache(resolvedInputs);
-  } else {
-    fileLoader = new UncachedFileLoader();
-  }
-
-  const compilerHostDelegate =
-      ts.createCompilerHost({target: ts.ScriptTarget.ES5});
-
-  const compilerHost = new CompilerHost(
-      files, options, bazelOpts, compilerHostDelegate, fileLoader,
-      allowActionInputReads);
-
-  let oldProgram = cache.getProgram(bazelOpts.target);
-  let program = ts.createProgram(files, options, compilerHost, oldProgram);
-  cache.putProgram(bazelOpts.target, program);
-
-  cache.traceStats();
-
-  function isCompilationTarget(sf: ts.SourceFile): boolean {
-    return (bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1);
-  }
-  let diags: ts.Diagnostic[] = [];
+export function gatherDiagnostics(
+    options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program,
+    disabledTsetseRules: string[]): ts.Diagnostic[] {
   // Install extra diagnostic plugins
   if (!bazelOpts.disableStrictDeps) {
     const ignoredFilesPrefixes: string[] = [];
@@ -119,39 +67,144 @@
       ignoredFilesPrefixes,
     });
   }
-  program = tsetsePlugin.wrap(program, disabledTsetseRules);
-
-  // These checks mirror ts.getPreEmitDiagnostics, with the important
-  // exception that if you call program.getDeclarationDiagnostics() it somehow
-  // corrupts the emit.
-  wrap(`global diagnostics`, () => {
-    diags.push(...program.getOptionsDiagnostics());
-    diags.push(...program.getGlobalDiagnostics());
-  });
-  let sourceFilesToCheck: ReadonlyArray<ts.SourceFile>;
-  if (bazelOpts.typeCheckDependencies) {
-    sourceFilesToCheck = program.getSourceFiles();
-  } else {
-    sourceFilesToCheck = program.getSourceFiles().filter(isCompilationTarget);
+  if (!bazelOpts.isJsTranspilation) {
+    program = tsetsePlugin.wrap(program, disabledTsetseRules);
   }
-  for (const sf of sourceFilesToCheck) {
-    wrap(`check ${sf.fileName}`, () => {
-      diags.push(...program.getSyntacticDiagnostics(sf));
-      diags.push(...program.getSemanticDiagnostics(sf));
+
+  // TODO(alexeagle): support plugins registered by config
+
+  const diagnostics: ts.Diagnostic[] = [];
+  perfTrace.wrap('type checking', () => {
+    // These checks mirror ts.getPreEmitDiagnostics, with the important
+    // exception of avoiding b/30708240, which is that if you call
+    // program.getDeclarationDiagnostics() it somehow corrupts the emit.
+    perfTrace.wrap(`global diagnostics`, () => {
+      diagnostics.push(...program.getOptionsDiagnostics());
+      diagnostics.push(...program.getGlobalDiagnostics());
     });
-  }
+    let sourceFilesToCheck: ReadonlyArray<ts.SourceFile>;
+    if (bazelOpts.typeCheckDependencies) {
+      sourceFilesToCheck = program.getSourceFiles();
+    } else {
+      sourceFilesToCheck = program.getSourceFiles().filter(
+          f => isCompilationTarget(bazelOpts, f));
+    }
+    for (const sf of sourceFilesToCheck) {
+      perfTrace.wrap(`check ${sf.fileName}`, () => {
+        diagnostics.push(...program.getSyntacticDiagnostics(sf));
+        diagnostics.push(...program.getSemanticDiagnostics(sf));
+      });
+      perfTrace.snapshotMemoryUsage();
+    }
+  });
 
-  // If there are any TypeScript type errors abort now, so the error
-  // messages refer to the original source.  After any subsequent passes
-  // (decorator downleveling or tsickle) we do not type check.
-  diags = diagnostics.filterExpected(bazelOpts, diags);
-  if (diags.length > 0) {
-    console.error(diagnostics.format(bazelOpts.target, diags));
+  return diagnostics;
+}
+
+/**
+ * Runs a single build, returning false on failure.  This is potentially called
+ * multiple times (once per bazel request) when running as a bazel worker.
+ * Any encountered errors are written to stderr.
+ */
+function runOneBuild(
+    args: string[], inputs?: {[path: string]: string}): boolean {
+  if (args.length !== 1) {
+    console.error('Expected one argument: path to tsconfig.json');
     return false;
   }
-  const toEmit = program.getSourceFiles().filter(isCompilationTarget);
-  const emitResults: ts.EmitResult[] = [];
 
+  perfTrace.snapshotMemoryUsage();
+
+  // 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(bazelDiagnostics.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, disabledTsetseRules} = parsed;
+
+  if (bazelOpts.maxCacheSizeMb !== undefined) {
+    const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * (1 << 20);
+    cache.setMaxCacheSize(maxCacheSizeBytes);
+  } else {
+    cache.resetMaxCacheSize();
+  }
+
+  let fileLoader: FileLoader;
+  if (inputs) {
+    fileLoader = new CachedFileLoader(cache);
+    // Resolve the inputs to absolute paths to match TypeScript internals
+    const resolvedInputs = new Map<string, string>();
+    for (const key of Object.keys(inputs)) {
+      resolvedInputs.set(resolveNormalizedPath(key), inputs[key]);
+    }
+    cache.updateCache(resolvedInputs);
+  } else {
+    fileLoader = new UncachedFileLoader();
+  }
+
+  const perfTracePath = bazelOpts.perfTracePath;
+  if (!perfTracePath) {
+    return runFromOptions(
+        fileLoader, options, bazelOpts, files, disabledTsetseRules);
+  }
+
+  log('Writing trace to', perfTracePath);
+  const success = perfTrace.wrap(
+      'runOneBuild',
+      () => runFromOptions(
+          fileLoader, options, bazelOpts, files, disabledTsetseRules));
+  if (!success) return false;
+  // Force a garbage collection pass.  This keeps our memory usage
+  // consistent across multiple compilations, and allows the file
+  // cache to use the current memory usage as a guideline for expiring
+  // data.  Note: this is intentionally not within runFromOptions(), as
+  // we want to gc only after all its locals have gone out of scope.
+  global.gc();
+
+  perfTrace.snapshotMemoryUsage();
+  perfTrace.write(perfTracePath);
+
+  return true;
+}
+
+// We only allow our own code to use the expected_diagnostics attribute
+const expectDiagnosticsWhitelist: string[] = [
+];
+
+function runFromOptions(
+    fileLoader: FileLoader, options: ts.CompilerOptions,
+    bazelOpts: BazelOptions, files: string[],
+    disabledTsetseRules: string[]): boolean {
+  perfTrace.snapshotMemoryUsage();
+  cache.resetStats();
+  cache.traceStats();
+  const compilerHostDelegate =
+      ts.createCompilerHost({target: ts.ScriptTarget.ES5});
+
+  const allowActionInputReads = true;
+  const compilerHost = new CompilerHost(
+      files, options, bazelOpts, compilerHostDelegate, fileLoader,
+      allowActionInputReads);
+
+
+  const oldProgram = cache.getProgram(bazelOpts.target);
+  const program = perfTrace.wrap(
+      'createProgram',
+      () => ts.createProgram(
+          compilerHost.inputFiles, options, compilerHost, oldProgram));
+  cache.putProgram(bazelOpts.target, program);
+
+
+  const compilationTargets =
+      program.getSourceFiles().filter(f => isCompilationTarget(bazelOpts, f));
+  const emitResults: ts.EmitResult[] = [];
+  const diagnostics: ts.Diagnostic[] = [];
   if (bazelOpts.tsickle) {
     // The 'tsickle' import above is only used in type positions, so it won't
     // result in a runtime dependency on tsickle.
@@ -167,29 +220,36 @@
           'When setting bazelOpts { tsickle: true }, ' +
           'you must also add a devDependency on the tsickle npm package');
     }
-    for (const sf of toEmit) {
+    for (const sf of compilationTargets) {
       emitResults.push(optTsickle.emitWithTsickle(
           program, compilerHost, compilerHost, options, sf));
     }
-    diags.push(
+    diagnostics.push(
         ...optTsickle.mergeEmitResults(emitResults as tsickle.EmitResult[])
             .diagnostics);
   } else {
-    for (const sf of toEmit) {
+    for (const sf of compilationTargets) {
       emitResults.push(program.emit(sf));
     }
 
     for (const d of emitResults) {
-      diags.push(...d.diagnostics);
+      diagnostics.push(...d.diagnostics);
     }
   }
-  if (diags.length > 0) {
-    console.error(diagnostics.format(bazelOpts.target, diags));
+  if (diagnostics.length > 0) {
+    console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
     return false;
   }
+
+  cache.printStats();
   return true;
 }
 
+
 if (require.main === module) {
+  // Do not call process.exit(), as that terminates the binary before
+  // completing pending operations, such as writing to stdout or emitting the
+  // v8 performance log. Rather, set the exit code and fall off the main
+  // thread, which will cause node to terminate cleanly.
   process.exitCode = main(process.argv.slice(2));
 }