Skip to content

Commit

Permalink
Optimization: cache results between TypeScript projects
Browse files Browse the repository at this point in the history
Towards #175

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 speeds up the index time by ~30% from ~100s to ~70s.
The resulting index.scip has identical checksum before and after
applying this optimization.

This new optimization is enabled by default, but can be disabled with
the option `--no-global-cache`.
  • Loading branch information
olafurpg committed Oct 17, 2022
1 parent 5c08bb9 commit 61fba8b
Show file tree
Hide file tree
Showing 3 changed files with 95 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)
},
})
}
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 61fba8b

Please sign in to comment.