Skip to content

Commit

Permalink
feat: improve overall typescript performance (#435)
Browse files Browse the repository at this point in the history
Aside from general performance improvements, I added a `profile` option to be able to measure timings and compare them with other tools, like tsc.

BREAKING CHANGE: 馃Ж Changed default TypeScript compiler options to incremental: true and
the default mode to write-tsbuildinfo
  • Loading branch information
piotr-oles committed Jun 3, 2020
1 parent a430979 commit 944e0c9
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 34 deletions.
23 changes: 12 additions & 11 deletions README.md
Expand Up @@ -153,17 +153,18 @@ Options passed to the plugin constructor will overwrite options from the cosmico

Options for the TypeScript checker (`typescript` option object).

| Name | Type | Default value | Description |
| -------------------- | --------- | ------------------------------------------------------------------------- | ----------- |
| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. |
| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. |
| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) |
| `context` | `string` | `dirname(configuration.tsconfig)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `fork-ts-checker-webpack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `tsconfig`. |
| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. To enable `incremental` mode, set it in the `tsconfig.json` file. |
| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'readonly'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `readonly` mode to not overwrite filed emitted by `ts-loader`. |
| `compilerOptions` | `object` | `{ skipLibCheck: true, skipDefaultLibCheck: true }` | These options will overwrite compiler options from the `tsconfig.json` file. |
| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. |
| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). |
| Name | Type | Default value | Description |
| -------------------- | --------- | -------------------------------------------------------------------------------------- | ----------- |
| `enabled` | `boolean` | `true` | If `true`, it enables TypeScript checker. |
| `memoryLimit` | `number` | `2048` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. |
| `tsconfig` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) |
| `context` | `string` | `dirname(configuration.tsconfig)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `fork-ts-checker-webpack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `tsconfig`. |
| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. To enable `incremental` mode, set it in the `tsconfig.json` file. |
| `mode` | `'readonly'` or `'write-tsbuildinfo'` or `'write-references'` | `'write-tsbuildinfo'` | If you use the `babel-loader`, it's recommended to use `write-references` mode to improve initial compilation time. If you use `ts-loader`, it's recommended to use `write-tsbuildinfo` mode to not overwrite filed emitted by the `ts-loader`. |
| `compilerOptions` | `object` | `{ skipLibCheck: true, sourceMap: false, inlineSourceMap: false, incremental: true }` | These options will overwrite compiler options from the `tsconfig.json` file. |
| `diagnosticsOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. |
| `extensions` | `object` | `{}` | See [TypeScript extensions options](#typescript-extensions-options). |
| `profile` | `boolean` | `false` | Measures and prints timings related to the TypeScript performance. |

#### TypeScript extensions options

Expand Down
4 changes: 4 additions & 0 deletions src/ForkTsCheckerWebpackPluginOptions.json
Expand Up @@ -160,6 +160,10 @@
"$ref": "#/definitions/TypeScriptVueExtensionOptions"
}
}
},
"profile": {
"type": "boolean",
"description": "Measures and prints timings related to the TypeScript performance."
}
}
}
Expand Down
85 changes: 85 additions & 0 deletions src/profile/Performance.ts
@@ -0,0 +1,85 @@
import { performance } from 'perf_hooks';

interface Performance {
enable(): void;
disable(): void;
mark(name: string): void;
markStart(name: string): void;
markEnd(name: string): void;
measure(name: string, startMark?: string, endMark?: string): void;
print(): void;
}

function createPerformance(): Performance {
let enabled = false;
let timeOrigin: number;
let marks: Map<string, number>;
let measurements: Map<string, number>;

function enable() {
enabled = true;
marks = new Map();
measurements = new Map();
timeOrigin = performance.now();
}

function disable() {
enabled = false;
}

function mark(name: string) {
if (enabled) {
marks.set(name, performance.now());
}
}

function measure(name: string, startMark?: string, endMark?: string) {
if (enabled) {
const start = (startMark && marks.get(startMark)) || timeOrigin;
const end = (endMark && marks.get(endMark)) || performance.now();

measurements.set(name, (measurements.get(name) || 0) + (end - start));
}
}

function markStart(name: string) {
if (enabled) {
mark(`${name} start`);
}
}

function markEnd(name: string) {
if (enabled) {
mark(`${name} end`);
measure(name, `${name} start`, `${name} end`);
}
}

function formatName(name: string, width = 0) {
return `${name}:`.padEnd(width);
}

function formatDuration(duration: number, width = 0) {
return `${(duration / 1000).toFixed(2)} s`.padStart(width);
}

function print() {
if (enabled) {
let nameWidth = 0;
let durationWidth = 0;

measurements.forEach((duration, name) => {
nameWidth = Math.max(nameWidth, formatName(name).length);
durationWidth = Math.max(durationWidth, formatDuration(duration).length);
});

measurements.forEach((duration, name) => {
console.log(`${formatName(name, nameWidth)} ${formatDuration(duration, durationWidth)}`);
});
}
}

return { enable, disable, mark, markStart, markEnd, measure, print };
}

export { Performance, createPerformance };
21 changes: 18 additions & 3 deletions src/typescript-reporter/TypeScriptReporterConfiguration.ts
@@ -1,5 +1,7 @@
import webpack from 'webpack';
import path from 'path';
import * as ts from 'typescript';
import semver from 'semver';
import { TypeScriptDiagnosticsOptions } from './TypeScriptDiagnosticsOptions';
import { TypeScriptReporterOptions } from './TypeScriptReporterOptions';
import {
Expand All @@ -20,6 +22,7 @@ interface TypeScriptReporterConfiguration {
extensions: {
vue: TypeScriptVueExtensionConfiguration;
};
profile: boolean;
}

function createTypeScriptReporterConfiguration(
Expand All @@ -39,17 +42,29 @@ function createTypeScriptReporterConfiguration(
const optionsAsObject: Exclude<TypeScriptReporterOptions, boolean> =
typeof options === 'object' ? options : {};

const defaultCompilerOptions: Record<string, unknown> = {
skipLibCheck: true,
sourceMap: false,
inlineSourceMap: false,
};
if (semver.gte(ts.version, '2.9.0')) {
defaultCompilerOptions.declarationMap = false;
}
if (semver.gte(ts.version, '3.4.0')) {
defaultCompilerOptions.incremental = true;
}

return {
enabled: options !== false,
memoryLimit: 2048,
build: false,
mode: 'readonly',
mode: 'write-tsbuildinfo',
profile: false,
...optionsAsObject,
tsconfig: tsconfig,
context: optionsAsObject.context || path.dirname(tsconfig),
compilerOptions: {
skipDefaultLibCheck: true,
skipLibCheck: true,
...defaultCompilerOptions,
...(optionsAsObject.compilerOptions || {}),
},
extensions: {
Expand Down
1 change: 1 addition & 0 deletions src/typescript-reporter/TypeScriptReporterOptions.ts
Expand Up @@ -15,6 +15,7 @@ type TypeScriptReporterOptions =
extensions?: {
vue?: TypeScriptVueExtensionOptions;
};
profile?: boolean;
};

export { TypeScriptReporterOptions };
48 changes: 48 additions & 0 deletions src/typescript-reporter/profile/TypeScriptPerformance.ts
@@ -0,0 +1,48 @@
import * as ts from 'typescript';
import { Performance } from '../../profile/Performance';

interface TypeScriptPerformance {
enable(): void;
disable(): void;
mark(name: string): void;
measure(name: string, startMark?: string, endMark?: string): void;
}

function getTypeScriptPerformance(): TypeScriptPerformance | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ts as any).performance;
}

function connectTypeScriptPerformance(performance: Performance): Performance {
const typeScriptPerformance = getTypeScriptPerformance();

if (typeScriptPerformance) {
const { mark, measure } = typeScriptPerformance;
const { enable, disable } = performance;

typeScriptPerformance.mark = (name) => {
mark(name);
performance.mark(name);
};
typeScriptPerformance.measure = (name, startMark, endMark) => {
measure(name, startMark, endMark);
performance.measure(name, startMark, endMark);
};

return {
...performance,
enable() {
enable();
typeScriptPerformance.enable();
},
disable() {
disable();
typeScriptPerformance.disable();
},
};
} else {
return performance;
}
}

export { TypeScriptPerformance, connectTypeScriptPerformance };
30 changes: 19 additions & 11 deletions src/typescript-reporter/reporter/ControlledTypeScriptSystem.ts
Expand Up @@ -51,6 +51,9 @@ function createControlledTypeScriptSystem(
const realFileSystem = createRealFileSystem(caseSensitive);
const passiveFileSystem = createPassiveFileSystem(caseSensitive, realFileSystem);

// based on the ts.ignorePaths
const ignoredPaths = ['/node_modules/.', '/.git', '/.#'];

function createWatcher<TCallback>(
watchersMap: Map<string, TCallback[]>,
path: string,
Expand Down Expand Up @@ -88,7 +91,11 @@ function createControlledTypeScriptSystem(

function invokeDirectoryWatchers(path: string) {
const normalizedPath = realFileSystem.normalizePath(path);
let directory = dirname(normalizedPath);
const directory = dirname(normalizedPath);

if (ignoredPaths.some((ignoredPath) => forwardSlash(normalizedPath).includes(ignoredPath))) {
return;
}

const directoryWatchers = directoryWatchersMap.get(directory);
if (directoryWatchers) {
Expand All @@ -97,16 +104,17 @@ function createControlledTypeScriptSystem(
);
}

while (directory !== dirname(directory)) {
const recursiveDirectoryWatchers = recursiveDirectoryWatchersMap.get(directory);
if (recursiveDirectoryWatchers) {
recursiveDirectoryWatchersMap.forEach((recursiveDirectoryWatchers, watchedDirectory) => {
if (
watchedDirectory === directory ||
(directory.startsWith(watchedDirectory) &&
forwardSlash(directory)[watchedDirectory.length] === '/')
) {
recursiveDirectoryWatchers.forEach((recursiveDirectoryWatcher) =>
recursiveDirectoryWatcher(forwardSlash(normalizedPath))
);
}

directory = dirname(directory);
}
});
}

function getWriteFileSystem(path: string) {
Expand Down Expand Up @@ -207,10 +215,10 @@ function createControlledTypeScriptSystem(
invokeFileChanged(path: string) {
const normalizedPath = realFileSystem.normalizePath(path);

invokeDirectoryWatchers(normalizedPath);

if (deletedFiles.get(normalizedPath)) {
if (deletedFiles.get(normalizedPath) || !fileWatchersMap.has(path)) {
invokeFileWatchers(path, ts.FileWatcherEventKind.Created);
invokeDirectoryWatchers(normalizedPath);

deletedFiles.set(normalizedPath, false);
} else {
invokeFileWatchers(path, ts.FileWatcherEventKind.Changed);
Expand All @@ -220,8 +228,8 @@ function createControlledTypeScriptSystem(
const normalizedPath = realFileSystem.normalizePath(path);

if (!deletedFiles.get(normalizedPath)) {
invokeDirectoryWatchers(path);
invokeFileWatchers(path, ts.FileWatcherEventKind.Deleted);
invokeDirectoryWatchers(path);

deletedFiles.set(normalizedPath, true);
}
Expand Down
Expand Up @@ -23,7 +23,13 @@ function parseTypeScriptConfiguration(
parsedConfigFile.errors.push(...customCompilerOptionsConvertResults.errors);
}

return parsedConfigFile;
return {
...parsedConfigFile,
options: {
...parsedConfigFile.options,
configFilePath: configFileName,
},
};
}

export { parseTypeScriptConfiguration };

0 comments on commit 944e0c9

Please sign in to comment.