Skip to content

Commit

Permalink
Towards #175
Browse files Browse the repository at this point in the history
Previously, scip-typescript didn't cache anything at all between TypeScript projects. This commit implements an optimization so that we now cache the results of loading source files and parsing options. Benchmarks against the sourcegraph/sourcegraph repo indicate this optimization consistently speeds up the `index` command in all three multi-project repositories that I tested it with.

- sourcegraph/sourcegraph: ~30% from ~100s to ~70s
- nextautjs/next-auth: ~40% from 6.5s to 3.9
- xtermjs/xterm.js: ~45% from 7.3s to 4.1s

For every repo, I additionally validated that the resulting index.scip has identical checksum before and after applying this optimization. Given these promising results, this new optimization is enabled by default, but can be disabled with the option `--no-global-cache`.

*Test plan*

Manually tested by running `scip-typescript index tsconfig.all.json` in the sourcegraph/sourcegraph repository. To benchmark the difference for this PR:

- Checkout the code
- Run `yarn tsc -b`
- Go to the directory of your project
- Run `node PATH_TO_SCIP_TYPESCRIPT/dist/src/main.js`
- Copy the "optimized" index.scip with `cp index.scip index-withcache.scip`
- Run `node PATH_TO_SCIP_TYPESCRIPT/dist/src/main.js --no-global-caches`
- Validate the checksum is identical from the optimized output `shasum -a 256 *.scip`
  • Loading branch information
olafurpg committed Oct 17, 2022
1 parent 5c08bb9 commit 617f886
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 42 deletions.
12 changes: 11 additions & 1 deletion src/CommandLineOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Command } from 'commander'
// eslint-disable-next-line id-length
import ts from 'typescript'

import packageJson from '../package.json'

Expand All @@ -10,6 +12,7 @@ export interface MultiProjectOptions {
progressBar: boolean
yarnWorkspaces: boolean
yarnBerryWorkspaces: boolean
globalCaches: boolean
cwd: string
output: string
indexedProjects: Set<string>
Expand All @@ -22,6 +25,12 @@ export interface ProjectOptions extends MultiProjectOptions {
writeIndex: (index: lsif.lib.codeintel.lsiftyped.Index) => void
}

/** Cached values */
export interface GlobalCache {
sources: Map<string, ts.SourceFile | undefined>
parsedCommandLines: Map<string, ts.ParsedCommandLine>
}

export function mainCommand(
indexAction: (projects: string[], otpions: MultiProjectOptions) => void
): Command {
Expand All @@ -47,7 +56,8 @@ export function mainCommand(
false
)
.option('--output <path>', 'path to the output file', 'index.scip')
.option('--no-progress-bar', 'whether to disable the progress bar')
.option('--progress-bar', 'whether to enable a rich progress bar')
.option('--no-global-caches', 'whether to disable global caches between TypeScript projects')
.argument('[projects...]')
.action((parsedProjects, parsedOptions) => {
indexAction(
Expand Down
86 changes: 58 additions & 28 deletions src/ProjectIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
import * as path from 'path'
import { Writable as WritableStream } from 'stream'

import prettyMilliseconds from 'pretty-ms'
import ProgressBar from 'progress'
import * as ts from 'typescript'

import { ProjectOptions } from './CommandLineOptions'
import { GlobalCache, ProjectOptions } from './CommandLineOptions'
import { FileIndexer } from './FileIndexer'
import { Input } from './Input'
import * as lsif from './lsif'
import { LsifSymbol } from './LsifSymbol'
import { Packages } from './Packages'

function createCompilerHost(
cache: GlobalCache,
compilerOptions: ts.CompilerOptions,
projectOptions: ProjectOptions
): ts.CompilerHost {
const host = ts.createCompilerHost(compilerOptions)
if (!projectOptions.globalCaches) {
return host
}
const hostCopy = { ...host }
host.getParsedCommandLine = (fileName: string) => {
if (!hostCopy.getParsedCommandLine) {
return undefined
}
if (cache.parsedCommandLines.has(fileName)) {
return cache.parsedCommandLines.get(fileName)
}
const result = hostCopy.getParsedCommandLine(fileName)
if (result !== undefined) {
cache.parsedCommandLines.set(fileName, result)
}
return result
}
host.getSourceFile = (fileName, languageVersion) => {
const fromCache = cache.sources.get(fileName)
if (fromCache !== undefined) {
return fromCache
}
const result = hostCopy.getSourceFile(fileName, languageVersion)
if (result !== undefined) {
cache.sources.set(fileName, result)
}
return result
}
return host
}

export class ProjectIndexer {
private options: ProjectOptions
private program: ts.Program
Expand All @@ -20,10 +56,12 @@ export class ProjectIndexer {
private packages: Packages
constructor(
public readonly config: ts.ParsedCommandLine,
options: ProjectOptions
options: ProjectOptions,
cache: GlobalCache
) {
this.options = options
this.program = ts.createProgram(config.fileNames, config.options)
const host = createCompilerHost(cache, config.options, options)
this.program = ts.createProgram(config.fileNames, config.options, host)
this.checker = this.program.getTypeChecker()
this.packages = new Packages(options.projectRoot)
}
Expand All @@ -47,24 +85,24 @@ export class ProjectIndexer {
)
}

const jobs = new ProgressBar(
` ${this.options.projectDisplayName} [:bar] :current/:total :title`,
{
total: filesToIndex.length,
renderThrottle: 100,
incomplete: '_',
complete: '#',
width: 20,
clear: true,
stream: this.options.progressBar
? process.stderr
: writableNoopStream(),
}
)
const jobs: ProgressBar | undefined = !this.options.progressBar
? undefined
: new ProgressBar(
` ${this.options.projectDisplayName} [:bar] :current/:total :title`,
{
total: filesToIndex.length,
renderThrottle: 100,
incomplete: '_',
complete: '#',
width: 20,
clear: true,
stream: process.stderr,
}
)
let lastWrite = startTimestamp
for (const [index, sourceFile] of filesToIndex.entries()) {
const title = path.relative(this.options.cwd, sourceFile.fileName)
jobs.tick({ title })
jobs?.tick({ title })
if (!this.options.progressBar) {
const now = Date.now()
const elapsed = now - lastWrite
Expand Down Expand Up @@ -102,7 +140,7 @@ export class ProjectIndexer {
)
}
}
jobs.terminate()
jobs?.terminate()
const elapsed = Date.now() - startTimestamp
if (!this.options.progressBar && lastWrite > startTimestamp) {
process.stdout.write('\n')
Expand All @@ -112,11 +150,3 @@ export class ProjectIndexer {
)
}
}

function writableNoopStream(): WritableStream {
return new WritableStream({
write(_unused1, _unused2, callback) {
setImmediate(callback)
},
})
}
1 change: 1 addition & 0 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ for (const snapshotDirectory of snapshotDirectories) {
yarnBerryWorkspaces: false,
progressBar: false,
indexedProjects: new Set(),
globalCaches: true
})
if (inferTsconfig) {
fs.rmSync(tsconfigJsonPath)
Expand Down
39 changes: 26 additions & 13 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as ts from 'typescript'
import packageJson from '../package.json'

import {
GlobalCache,
mainCommand,
MultiProjectOptions,
ProjectOptions,
Expand Down Expand Up @@ -48,6 +49,11 @@ export function indexCommand(
documentCount += index.documents.length
fs.writeSync(output, index.serializeBinary())
}

const cache: GlobalCache = {
sources: new Map(),
parsedCommandLines: new Map(),
}
try {
writeIndex(
new lsiftyped.Index({
Expand All @@ -67,12 +73,15 @@ export function indexCommand(
// they can have dependencies.
for (const projectRoot of projects) {
const projectDisplayName = projectRoot === '.' ? options.cwd : projectRoot
indexSingleProject({
...options,
projectRoot,
projectDisplayName,
writeIndex,
})
indexSingleProject(
{
...options,
projectRoot,
projectDisplayName,
writeIndex,
},
cache
)
}
} finally {
fs.close(output)
Expand All @@ -96,10 +105,11 @@ function makeAbsolutePath(cwd: string, relativeOrAbsolutePath: string): string {
return path.resolve(cwd, relativeOrAbsolutePath)
}

function indexSingleProject(options: ProjectOptions): void {
function indexSingleProject(options: ProjectOptions, cache: GlobalCache): void {
if (options.indexedProjects.has(options.projectRoot)) {
return
}

options.indexedProjects.add(options.projectRoot)
let config = ts.parseCommandLine(
['-p', options.projectRoot],
Expand All @@ -125,15 +135,18 @@ function indexSingleProject(options: ProjectOptions): void {
}

for (const projectReference of config.projectReferences || []) {
indexSingleProject({
...options,
projectRoot: projectReference.path,
projectDisplayName: projectReference.path,
})
indexSingleProject(
{
...options,
projectRoot: projectReference.path,
projectDisplayName: projectReference.path,
},
cache
)
}

if (config.fileNames.length > 0) {
new ProjectIndexer(config, options).index()
new ProjectIndexer(config, options, cache).index()
}
}

Expand Down

0 comments on commit 617f886

Please sign in to comment.