Trying out different setups for type checking from commandline. This is with the assumption that the build setup will not do any form of type checking, and it needs separate handling.
Looking for a couple of things:
- Clean state full project lint time
- Incremental lint time for some number of packages changed
- Watch mode startup speed
Some constraints:
- Needs to output declaration files that can be published, preferably without any further processing. We might end up using some tool for merging into a .d.ts file eventually, but it'd be good if the files end up in a good location for now.
- Preferably support project-wide watch-mode. The intent is that type check feedback during development would come from the Editor, but not everyone has that set up, so it'd be good to provide watch mode for that.
Starting out with a somewhat small project and all available configuration options.
createProjectMatrix({
baseConfig: presets.baseConfig({
packages: Array(5).fill(presets.packages.balanced(20)),
}),
dimensions: [
{
lintAll: {
lintStrategy: 'all',
},
lintTop: {
lintStrategy: 'top',
},
lintTopRefs: {
lintStrategy: 'top-references',
},
},
{
refs: {
projectReferences: 'enabled',
},
refsSpread: {
projectReferences: 'spread-composite',
},
noRefs: {
projectReferences: 'none',
},
},
],
});
First lint, n = 1
lintAll
refs | avg=31414 stdev=0
refsSpread | avg=30787 stdev=0
noRefs | avg=35778 stdev=0
lintTop
refs | avg=13790 stdev=0
refsSpread | avg=13322 stdev=0
noRefs | avg=12616 stdev=0
lintTopRefs
refs | avg=41878 stdev=0
refsSpread | avg=38713 stdev=0
noRefs | avg=1175 stdev=0
Dimension 0 diff vs lintAll
lintTop avg=0.408
refs < 0.439
refsSpread < 0.433
noRefs < 0.353
lintTopRefs avg=0.874
refs > 1.333
refsSpread > 1.257
noRefs < 0.033
Dimension 1 diff vs refs
refsSpread avg=0.957
lintAll ~ 0.980
lintTop ~ 0.966
lintTopRefs ~ 0.924
noRefs avg=0.694
lintAll > 1.139
lintTop ~ 0.915
lintTopRefs < 0.028
Followup lint, n = 1
lintAll
refs | avg=6876 stdev=0
refsSpread | avg=7326 stdev=0
noRefs | avg=34919 stdev=0
lintTop
refs | avg=3559 stdev=0
refsSpread | avg=3125 stdev=0
noRefs | avg=3070 stdev=0
lintTopRefs
refs | avg=773 stdev=0
refsSpread | avg=747 stdev=0
noRefs | avg=580 stdev=0
Dimension 0 diff vs lintAll
lintTop avg=0.344
refs < 0.518
refsSpread < 0.427
noRefs < 0.088
lintTopRefs avg=0.077
refs < 0.112
refsSpread < 0.102
noRefs < 0.017
Dimension 1 diff vs refs
refsSpread avg=0.970
lintAll ~ 1.065
lintTop < 0.878
lintTopRefs ~ 0.966
noRefs avg=2.230
lintAll > 5.078
lintTop < 0.863
lintTopRefs < 0.750
- A top-level lint with a global type cache directory is by far the best option from a performance standpoint. The incremental lint is quick enough and initial build is much quicker. It also doesn't matter if project references are used or not, which means we don't need to manage those. The downside of using top-level lint is tsconfigs in each package are ignored, the types are output to a separate dir and need further processing to be placed in the correct dist directory for publishing.
Using the same config but skipping the top-level lint config, since that seems to be the best option but with known downsides.
Also removing the spread composite option, since that didn't seem to have any impact.
New in this run is incemental option for non-project reference builds.
createProjectMatrix({
baseConfig: presets.baseConfig({
packages: Array(5).fill(presets.packages.balanced(20)),
}),
dimensions: [
{
lintAll: {
lintStrategy: 'all',
},
lintTopRefs: {
lintStrategy: 'top-references',
},
},
{
refs: {
projectReferences: 'enabled',
},
noRefsInc: {
projectReferences: 'incremental',
},
noRefs: {
projectReferences: 'none',
},
},
],
});
First lint, n = 1
lintAll
refs | avg=29083 stdev=0
noRefsInc | avg=35475 stdev=0
noRefs | avg=33086 stdev=0
lintTopRefs
refs | avg=39393 stdev=0
noRefsInc | avg=611 stdev=0
noRefs | avg=487 stdev=0
Dimension 0 diff vs lintAll
lintTopRefs avg=0.462
refs > 1.355
noRefsInc < 0.017
noRefs < 0.015
Dimension 1 diff vs refs
noRefsInc avg=0.618
lintAll > 1.220
lintTopRefs < 0.016
noRefs avg=0.575
lintAll > 1.138
lintTopRefs < 0.012
Second lint, n = 5
lintAll
refs | avg=6331 stdev=358
noRefsInc | avg=8168 stdev=321
noRefs | avg=32705 stdev=1837
lintTopRefs
refs | avg=595 stdev=9
noRefsInc | avg=465 stdev=18
noRefs | avg=465 stdev=21
Dimension 0 diff vs lintAll
lintTopRefs avg=0.055
refs < 0.094
noRefsInc < 0.057
noRefs < 0.014
Dimension 1 diff vs refs
noRefsInc avg=1.036
lintAll > 1.290
lintTopRefs < 0.782
noRefs avg=2.974
lintAll > 5.166
lintTopRefs < 0.782
lintTopRefs with noRefs is a noop, ignore those values
- Incremental tsc is likely a given, remains to be seen if it's something we want to run in CI/CD or not. Skipping non-incremental for further tests.
Focusing in on incremental with just 1 3-part dimension:
{
allInc: {
lintStrategy: 'all',
projectReferences: 'incremental',
},
allRefs: {
lintStrategy: 'all',
projectReferences: 'enabled',
},
topRefs: {
lintStrategy: 'top-references',
projectReferences: 'enabled',
},
}
First lint, n = 1
allInc | avg=31707 stdev=0
allRefs | avg=26146 stdev=0
topRefs | avg=43529 stdev=0
Dimension 0 diff vs allInc
allRefs avg=0.825
< 0.825
topRefs avg=1.373
> 1.373
Second lint, n = 5
allInc | avg=7793 stdev=486
allRefs | avg=7000 stdev=88
topRefs | avg=612 stdev=36
Dimension 0 diff vs allInc
allRefs avg=0.898
< 0.898
topRefs avg=0.078
< 0.078
- Initial result is not reliable enough, need to figure out how to run that in a loop.
Multiple runs of clean lean, also fixed main not being included in the project references version.
First lint, n = 5
allInc | avg=30141 stdev=1399
allRefs | avg=25783 stdev=1269
topRefs | avg=45644 stdev=717
Dimension 0 diff vs allInc
allRefs avg=0.855
< 0.855
topRefs avg=1.514
> 1.514
- Using project references seems to speed up clean lerna lints too.
- Using tsc build mode seems very slow, so we likely don't want to rely on this for the clean build. It might come in handy to have this in watch mode though.
Same as previous config, but running full test including new benchmark of linting changed files.
The changed lint test is double the actual expected lint time, as it modifies a file and expects an error, then changes it back and expects a successful lint run.
First lint, n = 5
allInc | avg=29451 stdev=369
allRefs | avg=24719 stdev=743
topRefs | avg=43392 stdev=536
Dimension 0 diff vs allInc
allRefs avg=0.839
< 0.839
topRefs avg=1.473
> 1.473
Second lint, n = 5
allInc | avg=7460 stdev=510
allRefs | avg=7110 stdev=249
topRefs | avg=623 stdev=8
Dimension 0 diff vs allInc
allRefs avg=0.953
~ 0.953
topRefs avg=0.084
< 0.084
Changed lint, n = 5
allInc | avg=15818 stdev=366
allRefs | avg=15298 stdev=380
topRefs | avg=6170 stdev=142
Dimension 0 diff vs allInc
allRefs avg=0.967
~ 0.967
topRefs avg=0.390
< 0.390
tsc --build
is much faster at detecting and linting only changed packages, while still much slower than running lint through lerna for the clean build.- Need to benchmark the combination of lerna lint for full linting, and if the result of that is fine to use later with
tsc --build
.
Trying out same setup as previous test, but always using tsc --build
for the changed lint, and skipping the setup without project references.
Change lint, n = 5
allRefs | avg=6154 stdev=293
topRefs | avg=6100 stdev=94
Dimension 0 diff vs allRefs
topRefs avg=0.991
~ 0.991
Needing to dig into the first lint time after a full build:
Followup lint, changed, n = 1
allRefs | avg=6704 stdev=0
topRefs | avg=5933 stdev=0
Followup lint, unchanged, n = 1
allRefs | avg=745 stdev=0
topRefs | avg=612 stdev=0
- It seems like running a lerna lint followed by a
tsc --build
works just fine, it's possible that it's a bit slower but at most some 10%. - Time to try larger projects.
Same as previous config, but much larger project:
createProjectMatrix({
baseConfig: presets.baseConfig({
packages: Array(50).fill(presets.packages.balanced(20)),
}),
dimensions: [
{
allInc: {
lintStrategy: 'all',
projectReferences: 'incremental',
},
allRefs: {
lintStrategy: 'all',
projectReferences: 'enabled',
},
topRefs: {
lintStrategy: 'top-references',
projectReferences: 'enabled',
},
},
],
});
First lint, n = 3
allInc | avg=231204 stdev=14297
allRefs | avg=225363 stdev=1714
topRefs | avg=375197 stdev=9138
Dimension 0 diff vs allInc
allRefs avg=0.975
~ 0.975
topRefs avg=1.623
> 1.623
Followup project references lint, n = 1
allInc is a noop
allInc | avg=795 stdev=0
allRefs | avg=1499 stdev=0
topRefs | avg=1454 stdev=0
Dimension 0 diff vs allInc
allRefs avg=1.886
> 1.886
topRefs avg=1.829
> 1.829
Second lint, n = 3
allInc | avg=55846 stdev=6270
allRefs | avg=54423 stdev=1899
topRefs | avg=1390 stdev=43
Dimension 0 diff vs allInc
allRefs avg=0.975
~ 0.975
topRefs avg=0.025
< 0.025
Changed with project lint, n = 3
allInc is a noop
allInc | avg=939 stdev=51
allRefs | avg=7037 stdev=61
topRefs | avg=7012 stdev=213
Dimension 0 diff vs allInc
allRefs avg=7.491
> 7.491
topRefs avg=7.465
> 7.465
- Linting each package separately is prohibitively slow in a large project. Incremental linting is alright, but the clean lint takes minutes.
- Using lerna for incremental linting doesn't make sense, project references need to be used.
Same project size as previous test, but using a single top-level lint strategy:
({
top: {
lintStrategy: 'top',
projectReferences: 'none',
},
topInc: {
lintStrategy: 'top',
projectReferences: 'incremental',
},
});
First lint, n = 5
top | avg=32758 stdev=1111
topInc | avg=39714 stdev=1064
Dimension 0 diff vs top
topInc avg=1.212
> 1.212
Second lint, n = 5
top | avg=29937 stdev=1717
topInc | avg=6230 stdev=28
Dimension 0 diff vs top
topInc avg=0.208
< 0.208
Changed lint x2, n = 5
top | avg=61756 stdev=3562
topInc | avg=17600 stdev=558
Dimension 0 diff vs top
topInc avg=0.285
< 0.285
- Initial checking of the large 50x20 project is about 6x faster with incremental linting, and even faster without.
- Incremental checks are also quite quick, but not as fast as with project references.
- How do project references compare to incremental top-level checks in watch mode?
- Will a more complex dependency graph change the above conclusions?
Measuring impact of adding a common dependency:
createProjectMatrix({
baseConfig: presets.baseConfig(),
dimensions: [
{
topInc: {
lintStrategy: 'top',
projectReferences: 'incremental',
},
allRefs: {
lintStrategy: 'all',
projectReferences: 'enabled',
},
topRefs: {
lintStrategy: 'top-references',
projectReferences: 'enabled',
},
},
{
commonDep: {
packages: Array(4).fill(presets.packages.balanced(20)),
commonDep: presets.packages.balanced(20),
},
noCommon: {
packages: Array(5).fill(presets.packages.balanced(20)),
commonDep: null,
},
},
],
});
First lint, n = 5
topInc
commonDep | avg=13244 stdev=632
noCommon | avg=13675 stdev=1811
allRefs
commonDep | avg=37719 stdev=2988
noCommon | avg=30204 stdev=1698
topRefs
commonDep | avg=46720 stdev=1059
noCommon | avg=44387 stdev=2521
Dimension 0 diff vs topInc
allRefs avg=2.528
commonDep > 2.848
noCommon > 2.209
topRefs avg=3.387
commonDep > 3.528
noCommon > 3.246
Dimension 1 diff vs commonDep
noCommon avg=0.928
topInc ~ 1.033
allRefs < 0.801
topRefs ~ 0.950
Second lint, n = 5
topInc
commonDep | avg=2917 stdev=176
noCommon | avg=2601 stdev=30
allRefs
commonDep | avg=8828 stdev=479
noCommon | avg=8285 stdev=444
topRefs
commonDep | avg=789 stdev=23
noCommon | avg=724 stdev=12
Dimension 0 diff vs topInc
allRefs avg=3.106
commonDep > 3.027
noCommon > 3.185
topRefs avg=0.274
commonDep < 0.270
noCommon < 0.278
Dimension 1 diff vs commonDep
noCommon avg=0.916
topInc < 0.892
allRefs ~ 0.938
topRefs ~ 0.917
Changed lint, n = 5
topInc
commonDep | avg=7780 stdev=354
noCommon | avg=7849 stdev=234
allRefs
commonDep | avg=17178 stdev=563
noCommon | avg=15429 stdev=757
topRefs
commonDep | avg=6779 stdev=481
noCommon | avg=6098 stdev=495
Dimension 0 diff vs topInc
allRefs avg=2.087
commonDep > 2.208
noCommon > 1.966
topRefs avg=0.824
commonDep < 0.871
noCommon < 0.777
Dimension 1 diff vs commonDep
noCommon avg=0.936
topInc ~ 1.009
allRefs < 0.898
topRefs < 0.900
- A more complex dependency graph doesn't seem to have a big impact on lint speed, except that lerna will be able to parallelize less.
Manual watch mode tests of topInc and topRefs with 100x20. Also doing another check in VSCode to make sure initial results still hold.
VSCode seems the same as before, it takes 2.5-3s for updates to happen.
Watch mode is tricky, the top-level single package watch mode seems way better than top-level watch mode with project references.
Project references start up quicker if there are no changes, but it seems like it needs a full check when changes happen. Meaning it takes around 20s for a simple change in a package to appear in main. The same change takes about 3-4s when using the top-level watch mode.
A benefit of project references is that it is possible to lint a single package in isolation. This provides a huge speed boost and linting is almost instant for local changes. The convenience of being able to spin up a single root task might outweight the benefit of this though.