Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write path normalization without array allocations #60812

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Dec 18, 2024

Related: #60633
Alternative to #60755

Despite the good benchmarks, I didn’t see any measurable impact in real-world updateGraphWorker duration on my M2 Mac Mini. However, since that was a GC hot spot for @dbaeumer and #60755 improved things some, this may do even better for him.

bench.js
const ts = require("./built/local/typescript");
const ts_old = require("./node_modules/typescript");
const { Suite } = require("bench-node");

const suite = new Suite();

suite.add("normalizePath_old", () => {
  runScenarios(ts_old.normalizePath);
});

suite.add("normalizePath_new", () => {
  runScenarios(ts.normalizePath);
});

suite.add("getNormalizedAbsolutePath_old", () => {
  runScenarios(ts_old.getNormalizedAbsolutePath);
});

suite.add("getNormalizedAbsolutePath_new", () => {
  runScenarios(ts.getNormalizedAbsolutePath);
});

suite.run();

function runScenarios(normalize) {
  normalize("/", "");
  normalize("/.", "");
  normalize("/./", "");
  normalize("/../", "");
  normalize("/a", "");
  normalize("/a/", "");
  normalize("/a/.", "");
  normalize("/a/foo.", "");
  normalize("/a/./", "");
  normalize("/a/./b", "");
  normalize("/a/./b/", "");
  normalize("/a/..", "");
  normalize("/a/../", "");
  normalize("/a/../", "");
  normalize("/a/../b", "");
  normalize("/a/../b/", "");
  normalize("/a/..", "");
  normalize("/a/..", "/");
  normalize("/a/..", "b/");
  normalize("/a/..", "/b");
  normalize("/a/.", "b");
  normalize("/a/.", ".");

  // Tests as above, but with backslashes.
  normalize("\\", "");
  normalize("\\.", "");
  normalize("\\.\\", "");
  normalize("\\..\\", "");
  normalize("\\a\\.\\", "");
  normalize("\\a\\.\\b", "");
  normalize("\\a\\.\\b\\", "");
  normalize("\\a\\..", "");
  normalize("\\a\\..\\", "");
  normalize("\\a\\..\\", "");
  normalize("\\a\\..\\b", "");
  normalize("\\a\\..\\b\\", "");
  normalize("\\a\\..", "");
  normalize("\\a\\..", "\\");
  normalize("\\a\\..", "b\\");
  normalize("\\a\\..", "\\b");
  normalize("\\a\\.", "b");
  normalize("\\a\\.", ".");

  // Relative paths on an empty currentDirectory.
  normalize("", "");
  normalize(".", "");
  normalize("./", "");
  normalize("./a", "");
  // Strangely, these do not normalize to the empty string.
  normalize("..", "");
  normalize("../", "");
  normalize("../..", "");
  normalize("../../", "");
  normalize("./..", "");
  normalize("../../a/..", "");

  // Interaction between relative paths and currentDirectory.
  normalize("", "/home");
  normalize(".", "/home");
  normalize("./", "/home");
  normalize("..", "/home");
  normalize("../", "/home");
  normalize("a", "b");
  normalize("a", "b/c");

  // Base names starting or ending with a dot do not affect normalization.
  normalize(".a", "");
  normalize("..a", "");
  normalize("a.", "");
  normalize("a..", "");

  normalize("/base/./.a", "");
  normalize("/base/../.a", "");
  normalize("/base/./..a", "");
  normalize("/base/../..a", "");
  normalize("/base/./..a/b", "");
  normalize("/base/../..a/b", "");

  normalize("/base/./a.", "");
  normalize("/base/../a.", "");
  normalize("/base/./a..", "");
  normalize("/base/../a..", "");
  normalize("/base/./a../b", "");
  normalize("/base/../a../b", "");

  // Consecutive intermediate slashes are normalized to a single slash.
  normalize("a//b", "");
  normalize("a///b", "");
  normalize("a/b//c", "");
  normalize("/a/b//c", "");
  normalize("//a/b//c", "");

  // Backslashes are converted to slashes,
  // and then consecutive intermediate slashes are normalized to a single slash
  normalize("a\\\\b", "");
  normalize("a\\\\\\b", "");
  normalize("a\\b\\\\c", "");
  normalize("\\a\\b\\\\c", "");
  normalize("\\\\a\\b\\\\c", "");

  // The same occurs for mixed slashes.
  normalize("a/\\b", "");
  normalize("a\\/b", "");
  normalize("a\\/\\b", "");
  normalize("a\\b//c", "");
  normalize("\\a\\b\\\\c", "");
  normalize("\\\\a\\b\\\\c", "");
}
npm i --no-save bench-node
npm run build
node --allow-natives-syntax bench.js   
                                              
normalizePath_old                             x 49,990 ops/sec (13 runs sampled) v8-never-optimize=true min..max=(19.15us...20.48us)
normalizePath_new                             x 125,800 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(7.80us...8.27us)
getNormalizedAbsolutePath_old                 x 55,493 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(17.17us...18.45us)
getNormalizedAbsolutePath_new                 x 125,567 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(7.90us...8.03us)

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Dec 18, 2024
@andrewbranch
Copy link
Member Author

@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 18, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
perf test this ✅ Started 👀 Results

@@ -624,27 +624,103 @@ export function getNormalizedPathComponents(path: string, currentDirectory: stri
}

/** @internal */
export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined): string {
return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
export function getNormalizedAbsolutePath(path: string, currentDirectory: string | undefined): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if currentDirectory is almost always normalized.

@typescript-bot
Copy link
Collaborator

@andrewbranch
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 34 34 ~ ~ ~ p=1.000 n=6
Symbols 62,363 62,363 ~ ~ ~ p=1.000 n=6
Types 50,395 50,395 ~ ~ ~ p=1.000 n=6
Memory used 195,362k (± 1.01%) 196,140k (± 0.73%) ~ 193,230k 196,885k p=0.230 n=6
Parse Time 1.59s (± 1.19%) 1.61s (± 1.01%) +0.02s (+ 1.47%) 1.60s 1.64s p=0.048 n=6
Bind Time 0.87s (± 1.62%) 0.86s (± 1.47%) ~ 0.85s 0.88s p=0.249 n=6
Check Time 11.73s (± 0.48%) 11.79s (± 0.56%) ~ 11.71s 11.87s p=0.261 n=6
Emit Time 3.46s (± 3.38%) 3.33s (± 2.58%) ~ 3.26s 3.50s p=0.063 n=6
Total Time 17.65s (± 0.66%) 17.60s (± 0.75%) ~ 17.45s 17.77s p=0.471 n=6
angular-1 - node (v18.15.0, x64)
Errors 37 37 ~ ~ ~ p=1.000 n=6
Symbols 947,936 947,936 ~ ~ ~ p=1.000 n=6
Types 410,955 410,955 ~ ~ ~ p=1.000 n=6
Memory used 1,226,014k (± 0.00%) 1,225,733k (± 0.00%) -281k (- 0.02%) 1,225,632k 1,225,785k p=0.005 n=6
Parse Time 8.01s (± 0.80%) 8.21s (± 0.87%) +0.19s (+ 2.41%) 8.11s 8.27s p=0.006 n=6
Bind Time 2.29s (± 1.01%) 2.30s (± 0.45%) ~ 2.28s 2.31s p=0.220 n=6
Check Time 38.28s (± 0.44%) 38.60s (± 0.41%) +0.32s (+ 0.84%) 38.38s 38.82s p=0.013 n=6
Emit Time 18.34s (± 0.77%) 19.85s (± 0.82%) 🔻+1.51s (+ 8.24%) 19.70s 20.14s p=0.005 n=6
Total Time 66.92s (± 0.44%) 68.96s (± 0.29%) +2.04s (+ 3.05%) 68.72s 69.22s p=0.005 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,449,172 2,449,172 ~ ~ ~ p=1.000 n=6
Types 896,410 896,410 ~ ~ ~ p=1.000 n=6
Memory used 2,321,055k (± 0.00%) 2,320,238k (± 0.01%) -817k (- 0.04%) 2,319,979k 2,320,359k p=0.005 n=6
Parse Time 11.39s (± 0.39%) 11.77s (± 1.44%) +0.38s (+ 3.31%) 11.66s 12.11s p=0.005 n=6
Bind Time 2.68s (± 0.28%) 2.66s (± 0.97%) ~ 2.62s 2.70s p=0.118 n=6
Check Time 88.08s (± 0.61%) 90.77s (± 1.75%) +2.69s (+ 3.06%) 88.98s 93.25s p=0.005 n=6
Emit Time 0.35s (± 4.04%) 0.36s (± 2.84%) ~ 0.35s 0.38s p=0.118 n=6
Total Time 102.50s (± 0.51%) 105.56s (± 1.45%) +3.06s (+ 2.99%) 103.84s 108.01s p=0.005 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,880 1,226,894 +14 (+ 0.00%) ~ ~ p=0.001 n=6
Types 266,745 266,772 +27 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 2,846,394k (±13.21%) 2,847,042k (±13.14%) ~ 2,363,411k 3,089,147k p=0.689 n=6
Parse Time 6.75s (± 2.20%) 6.76s (± 1.67%) ~ 6.61s 6.89s p=1.000 n=6
Bind Time 2.16s (± 2.08%) 2.18s (± 1.77%) ~ 2.12s 2.23s p=0.810 n=6
Check Time 42.75s (± 0.52%) 42.96s (± 0.53%) ~ 42.64s 43.31s p=0.230 n=6
Emit Time 3.49s (± 3.25%) 3.46s (± 3.04%) ~ 3.31s 3.64s p=0.470 n=6
Total Time 55.15s (± 0.57%) 55.35s (± 0.58%) ~ 54.85s 55.82s p=0.297 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,880 1,226,894 +14 (+ 0.00%) ~ ~ p=0.001 n=6
Types 266,745 266,772 +27 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 3,032,595k (± 9.77%) 3,033,234k (± 9.76%) ~ 2,428,371k 3,155,346k p=0.378 n=6
Parse Time 8.65s (± 1.54%) 8.71s (± 1.29%) ~ 8.49s 8.79s p=0.093 n=6
Bind Time 2.65s (± 1.51%) 2.64s (± 1.19%) ~ 2.59s 2.68s p=0.810 n=6
Check Time 53.24s (± 0.37%) 53.40s (± 0.13%) ~ 53.31s 53.47s p=0.128 n=6
Emit Time 4.39s (± 2.24%) 4.32s (± 2.25%) ~ 4.20s 4.44s p=0.378 n=6
Total Time 68.93s (± 0.28%) 69.07s (± 0.14%) ~ 68.94s 69.21s p=0.172 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,439 262,453 +14 (+ 0.01%) ~ ~ p=0.001 n=6
Types 106,628 106,629 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 440,184k (± 0.01%) 440,500k (± 0.01%) +316k (+ 0.07%) 440,436k 440,550k p=0.005 n=6
Parse Time 3.52s (± 1.10%) 3.58s (± 0.82%) +0.06s (+ 1.66%) 3.55s 3.63s p=0.028 n=6
Bind Time 1.32s (± 0.83%) 1.32s (± 1.75%) ~ 1.29s 1.35s p=0.870 n=6
Check Time 18.90s (± 0.44%) 19.06s (± 0.27%) +0.15s (+ 0.82%) 18.97s 19.11s p=0.010 n=6
Emit Time 1.52s (± 0.69%) 1.53s (± 0.34%) ~ 1.53s 1.54s p=0.142 n=6
Total Time 25.26s (± 0.42%) 25.49s (± 0.24%) +0.22s (+ 0.89%) 25.38s 25.55s p=0.006 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 70 70 ~ ~ ~ p=1.000 n=6
Symbols 226,062 226,062 ~ ~ ~ p=1.000 n=6
Types 94,488 94,488 ~ ~ ~ p=1.000 n=6
Memory used 371,576k (± 0.02%) 371,654k (± 0.03%) ~ 371,558k 371,896k p=0.261 n=6
Parse Time 2.91s (± 0.77%) 2.91s (± 1.20%) ~ 2.86s 2.95s p=0.517 n=6
Bind Time 1.58s (± 0.84%) 1.59s (± 1.92%) ~ 1.55s 1.63s p=0.464 n=6
Check Time 16.54s (± 0.54%) 16.53s (± 0.32%) ~ 16.45s 16.61s p=0.936 n=6
Emit Time 0.00s (±244.70%) 0.00s ~ ~ ~ p=0.405 n=6
Total Time 21.03s (± 0.38%) 21.04s (± 0.41%) ~ 20.91s 21.16s p=1.000 n=6
vscode - node (v18.15.0, x64)
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 3,222,386 3,222,386 ~ ~ ~ p=1.000 n=6
Types 1,108,493 1,108,493 ~ ~ ~ p=1.000 n=6
Memory used 3,288,091k (± 0.01%) 3,287,870k (± 0.01%) ~ 3,287,518k 3,288,482k p=0.298 n=6
Parse Time 14.21s (± 0.33%) 14.80s (± 2.89%) 🔻+0.60s (+ 4.21%) 14.49s 15.66s p=0.005 n=6
Bind Time 4.53s (± 0.57%) 4.62s (± 2.79%) ~ 4.52s 4.81s p=0.225 n=6
Check Time 88.99s (± 3.43%) 90.52s (± 2.98%) ~ 89.09s 96.01s p=0.230 n=6
Emit Time 27.20s (± 7.39%) 31.85s (± 6.12%) 🔻+4.65s (+17.09%) 27.93s 33.04s p=0.013 n=6
Total Time 134.93s (± 2.20%) 141.81s (± 0.69%) 🔻+6.88s (+ 5.10%) 140.77s 143.06s p=0.005 n=6
webpack - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 291,468 291,468 ~ ~ ~ p=1.000 n=6
Types 118,921 118,921 ~ ~ ~ p=1.000 n=6
Memory used 445,188k (± 0.02%) 445,209k (± 0.03%) ~ 445,042k 445,409k p=0.810 n=6
Parse Time 4.04s (± 0.72%) 4.09s (± 1.05%) ~ 4.02s 4.14s p=0.091 n=6
Bind Time 1.76s (± 0.78%) 1.76s (± 0.86%) ~ 1.75s 1.79s p=1.000 n=6
Check Time 18.74s (± 0.48%) 18.76s (± 0.51%) ~ 18.66s 18.89s p=0.936 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.54s (± 0.41%) 24.62s (± 0.44%) ~ 24.52s 24.78s p=0.378 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 552,233 552,233 ~ ~ ~ p=1.000 n=6
Types 184,971 184,971 ~ ~ ~ p=1.000 n=6
Memory used 492,315k (± 0.02%) 492,202k (± 0.01%) -113k (- 0.02%) 492,163k 492,254k p=0.031 n=6
Parse Time 2.76s (± 0.15%) 2.80s (± 0.27%) +0.04s (+ 1.33%) 2.79s 2.81s p=0.003 n=6
Bind Time 0.96s 0.96s ~ ~ ~ p=1.000 n=6
Check Time 16.16s (± 0.27%) 16.20s (± 0.28%) ~ 16.16s 16.27s p=0.261 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 19.88s (± 0.21%) 19.97s (± 0.21%) +0.09s (+ 0.44%) 19.93s 20.03s p=0.006 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,312ms (± 0.59%) 2,342ms (± 0.35%) +30ms (+ 1.30%) 2,327ms 2,349ms p=0.005 n=6
Req 2 - geterr 5,287ms (± 0.30%) 5,332ms (± 0.22%) +45ms (+ 0.85%) 5,320ms 5,347ms p=0.005 n=6
Req 3 - references 263ms (± 1.58%) 267ms (± 1.56%) ~ 261ms 270ms p=0.076 n=6
Req 4 - navto 227ms (± 0.53%) 227ms (± 0.54%) ~ 226ms 229ms p=0.788 n=6
Req 5 - completionInfo count 1,357 1,357 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 77ms (± 3.88%) 81ms (± 9.55%) ~ 76ms 91ms p=0.208 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,456ms (± 1.59%) 2,464ms (± 1.27%) ~ 2,420ms 2,499ms p=0.689 n=6
Req 2 - geterr 4,056ms (± 0.50%) 4,071ms (± 0.44%) ~ 4,053ms 4,093ms p=0.173 n=6
Req 3 - references 284ms (± 0.48%) 284ms (± 0.29%) ~ 283ms 285ms p=0.934 n=6
Req 4 - navto 228ms (± 0.18%) 228ms ~ ~ ~ p=0.405 n=6
Req 5 - completionInfo count 1,519 1,519 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 85ms (± 3.28%) 87ms (± 1.19%) ~ 86ms 88ms p=0.542 n=6
xstate-main-1-tsserver - node (v18.15.0, x64)
Req 1 - updateOpen 5,311ms (± 0.26%) 5,367ms (± 0.25%) +56ms (+ 1.06%) 5,350ms 5,384ms p=0.005 n=6
Req 2 - geterr 1,149ms (± 0.80%) 1,168ms (± 0.97%) +19ms (+ 1.67%) 1,155ms 1,183ms p=0.020 n=6
Req 3 - references 83ms 78ms (± 0.66%) 🟩-5ms (- 6.43%) 77ms 78ms p=0.002 n=6
Req 4 - navto 450ms (± 0.27%) 454ms (± 0.62%) +3ms (+ 0.74%) 451ms 458ms p=0.027 n=6
Req 5 - completionInfo count 3,450 3,450 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 853ms (± 1.86%) 838ms (± 1.19%) ~ 821ms 850ms p=0.199 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstate-main-1-tsserver - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 161.64ms (± 0.22%) 161.69ms (± 0.18%) +0.06ms (+ 0.04%) 160.53ms 164.00ms p=0.005 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 233.15ms (± 0.15%) 235.26ms (± 0.85%) +2.12ms (+ 0.91%) 231.65ms 267.00ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 232.11ms (± 0.15%) 231.93ms (± 0.16%) -0.18ms (- 0.08%) 230.45ms 238.24ms p=0.000 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 227.99ms (± 0.19%) 227.82ms (± 0.16%) -0.17ms (- 0.08%) 226.47ms 232.88ms p=0.000 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

Comment on lines +633 to +636
const root = path.substring(0, rootLength);
const normalizedRoot = root && normalizeSlashes(root);
// `normalized` is only initialized once `path` is determined to be non-normalized
let normalized = normalizedRoot === root ? undefined : normalizedRoot;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just use lastIndexOf to see if altDirectorySeparator occurs. That way you can avoid creating a substring for root at all.

@andrewbranch
Copy link
Member Author

I’m puzzled by the perf results

@andrewbranch
Copy link
Member Author

Changing the scenarios to have long paths that are already normalized shows the old normalizePath regex fast path to be the winner. The scenarios being benchmarked are the tests, which skew heavily toward needing normalization, whereas real-world usage usually results in a no-op. Probably using the regex to test, new function to modify, is going to be the best combination.

@andrewbranch andrewbranch changed the title Write path normalization without array allocations or regexes Write path normalization without array allocations Dec 19, 2024
@andrewbranch
Copy link
Member Author

@typescript-bot perf test

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 19, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
perf test ✅ Started 👀 Results

Comment on lines +661 to +662
const sepIndex = path.indexOf(directorySeparator, index + 1);
const altSepIndex = path.indexOf(altDirectorySeparator, index + 1);
Copy link
Member

@DanielRosenwasser DanielRosenwasser Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quadratic because you have to find at least one of the other kinds of slashes over and over. You should just tight-loop on !isAnyDirectorySeparator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or use findIndex(path, isAnyDirectorySeparator, index + 1)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern with findIndex is that it might not optimize well it passed in both arrays and strings. Maybe I'm superstitious.

// At beginning of segment
segmentStart = index;
let ch = path.charCodeAt(index);
while (isAnyDirectorySeparator(ch) && index + 1 < path.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took me a bit to see why this works, but it's because index starts immediately after the root, or after the first separator following the prior segment. Figuring out if one of those is a backslash is handled below.

normalizedUpTo = index + 2;
}
}
else if (normalized === undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in this branch it is technically possible for you to avoid extra substrings by adjusting normalizedUpTo instead of initializing normalized.

@typescript-bot
Copy link
Collaborator

@andrewbranch
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 34 34 ~ ~ ~ p=1.000 n=6
Symbols 62,363 62,363 ~ ~ ~ p=1.000 n=6
Types 50,395 50,395 ~ ~ ~ p=1.000 n=6
Memory used 195,042k (± 1.01%) 196,764k (± 0.09%) ~ 196,453k 196,962k p=0.298 n=6
Parse Time 1.61s (± 1.30%) 1.60s (± 1.84%) ~ 1.56s 1.64s p=0.373 n=6
Bind Time 0.88s (± 2.14%) 0.87s (± 0.60%) ~ 0.86s 0.87s p=0.452 n=6
Check Time 11.74s (± 0.14%) 11.76s (± 0.66%) ~ 11.67s 11.90s p=1.000 n=6
Emit Time 3.28s (± 1.20%) 3.31s (± 3.47%) ~ 3.24s 3.54s p=0.936 n=6
Total Time 17.50s (± 0.42%) 17.53s (± 0.61%) ~ 17.40s 17.66s p=0.687 n=6
angular-1 - node (v18.15.0, x64)
Errors 37 37 ~ ~ ~ p=1.000 n=6
Symbols 947,936 947,936 ~ ~ ~ p=1.000 n=6
Types 410,955 410,955 ~ ~ ~ p=1.000 n=6
Memory used 1,226,032k (± 0.00%) 1,225,726k (± 0.01%) -306k (- 0.02%) 1,225,636k 1,225,853k p=0.005 n=6
Parse Time 8.10s (± 0.81%) 8.05s (± 0.49%) ~ 8.02s 8.11s p=0.198 n=6
Bind Time 2.28s (± 0.95%) 2.29s (± 0.24%) ~ 2.29s 2.30s p=0.101 n=6
Check Time 38.17s (± 0.32%) 38.11s (± 0.36%) ~ 37.94s 38.31s p=0.423 n=6
Emit Time 18.36s (± 0.39%) 18.21s (± 0.22%) -0.15s (- 0.81%) 18.17s 18.29s p=0.008 n=6
Total Time 66.91s (± 0.24%) 66.67s (± 0.22%) ~ 66.48s 66.87s p=0.066 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,449,172 2,449,172 ~ ~ ~ p=1.000 n=6
Types 896,410 896,410 ~ ~ ~ p=1.000 n=6
Memory used 2,321,084k (± 0.00%) 2,320,392k (± 0.01%) -693k (- 0.03%) 2,320,197k 2,320,555k p=0.005 n=6
Parse Time 14.06s (± 0.95%) 13.92s (± 0.61%) -0.14s (- 1.00%) 13.76s 14.00s p=0.031 n=6
Bind Time 3.29s (± 0.51%) 3.29s (± 0.45%) ~ 3.27s 3.31s p=0.934 n=6
Check Time 108.54s (± 2.20%) 108.26s (± 1.40%) ~ 107.06s 110.72s p=0.688 n=6
Emit Time 0.43s (± 1.97%) 0.51s (±41.02%) ~ 0.41s 0.94s p=0.548 n=6
Total Time 126.31s (± 1.99%) 125.98s (± 1.24%) ~ 124.69s 128.19s p=0.575 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,880 1,226,898 +18 (+ 0.00%) ~ ~ p=0.001 n=6
Types 266,745 266,773 +28 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 2,967,103k (±10.01%) 2,725,331k (±14.65%) ~ 2,360,476k 3,091,267k p=0.689 n=6
Parse Time 6.81s (± 1.59%) 6.68s (± 1.40%) -0.12s (- 1.79%) 6.58s 6.79s p=0.031 n=6
Bind Time 2.16s (± 1.60%) 2.16s (± 1.70%) ~ 2.11s 2.19s p=0.872 n=6
Check Time 42.90s (± 0.20%) 42.78s (± 0.30%) ~ 42.53s 42.87s p=0.093 n=6
Emit Time 3.49s (± 2.72%) 3.48s (± 3.20%) ~ 3.37s 3.63s p=0.936 n=6
Total Time 55.35s (± 0.24%) 55.11s (± 0.42%) ~ 54.73s 55.39s p=0.066 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,226,880 1,226,898 +18 (+ 0.00%) ~ ~ p=0.001 n=6
Types 266,745 266,773 +28 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 3,032,854k (± 9.77%) 3,033,179k (± 9.74%) ~ 2,429,601k 3,154,501k p=1.000 n=6
Parse Time 6.98s (± 1.03%) 6.95s (± 0.81%) ~ 6.85s 7.00s p=0.297 n=6
Bind Time 2.17s (± 1.22%) 2.16s (± 1.21%) ~ 2.13s 2.19s p=0.517 n=6
Check Time 42.93s (± 0.45%) 42.77s (± 0.38%) ~ 42.52s 42.93s p=0.092 n=6
Emit Time 3.53s (± 6.37%) 3.52s (± 2.33%) ~ 3.39s 3.60s p=0.378 n=6
Total Time 55.61s (± 0.23%) 55.39s (± 0.46%) ~ 54.92s 55.63s p=0.065 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 262,439 262,457 +18 (+ 0.01%) ~ ~ p=0.001 n=6
Types 106,628 106,630 +2 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 440,209k (± 0.01%) 440,509k (± 0.02%) +300k (+ 0.07%) 440,407k 440,692k p=0.005 n=6
Parse Time 3.56s (± 0.61%) 3.54s (± 1.17%) ~ 3.51s 3.61s p=0.255 n=6
Bind Time 1.32s (± 0.92%) 1.32s (± 1.05%) ~ 1.30s 1.34s p=0.802 n=6
Check Time 18.90s (± 0.11%) 19.09s (± 0.92%) +0.19s (+ 0.98%) 18.95s 19.44s p=0.005 n=6
Emit Time 1.53s (± 0.76%) 1.54s (± 0.82%) ~ 1.53s 1.56s p=0.278 n=6
Total Time 25.31s (± 0.13%) 25.48s (± 0.84%) +0.17s (+ 0.68%) 25.28s 25.88s p=0.037 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 70 70 ~ ~ ~ p=1.000 n=6
Symbols 226,062 226,062 ~ ~ ~ p=1.000 n=6
Types 94,488 94,488 ~ ~ ~ p=1.000 n=6
Memory used 371,665k (± 0.03%) 371,718k (± 0.05%) ~ 371,533k 372,071k p=0.689 n=6
Parse Time 2.90s (± 0.72%) 2.88s (± 1.23%) ~ 2.83s 2.92s p=0.460 n=6
Bind Time 1.60s (± 1.25%) 1.58s (± 0.53%) ~ 1.57s 1.59s p=0.276 n=6
Check Time 16.47s (± 0.33%) 16.49s (± 0.27%) ~ 16.43s 16.56s p=0.423 n=6
Emit Time 0.00s (±244.70%) 0.00s ~ ~ ~ p=0.405 n=6
Total Time 20.97s (± 0.43%) 20.95s (± 0.33%) ~ 20.85s 21.05s p=0.936 n=6
vscode - node (v18.15.0, x64)
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 3,222,815 3,222,815 ~ ~ ~ p=1.000 n=6
Types 1,108,656 1,108,656 ~ ~ ~ p=1.000 n=6
Memory used 3,288,263k (± 0.01%) 3,288,265k (± 0.01%) ~ 3,287,962k 3,288,511k p=0.810 n=6
Parse Time 11.59s (± 0.17%) 11.52s (± 0.17%) -0.07s (- 0.62%) 11.50s 11.55s p=0.005 n=6
Bind Time 3.78s (± 0.72%) 3.85s (± 2.67%) ~ 3.75s 3.99s p=0.139 n=6
Check Time 73.69s (± 0.27%) 73.34s (± 0.14%) -0.35s (- 0.47%) 73.23s 73.50s p=0.016 n=6
Emit Time 23.75s (± 1.18%) 23.27s (± 0.63%) -0.47s (- 1.99%) 23.09s 23.44s p=0.005 n=6
Total Time 112.80s (± 0.39%) 111.98s (± 0.20%) -0.82s (- 0.73%) 111.70s 112.28s p=0.008 n=6
webpack - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 291,468 291,468 ~ ~ ~ p=1.000 n=6
Types 118,921 118,921 ~ ~ ~ p=1.000 n=6
Memory used 445,276k (± 0.03%) 445,259k (± 0.04%) ~ 444,963k 445,471k p=0.873 n=6
Parse Time 4.11s (± 0.40%) 4.10s (± 1.13%) ~ 4.04s 4.15s p=0.747 n=6
Bind Time 1.77s (± 1.36%) 1.77s (± 0.97%) ~ 1.74s 1.79s p=0.625 n=6
Check Time 18.78s (± 0.51%) 18.83s (± 0.60%) ~ 18.69s 18.97s p=0.470 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.68s (± 0.38%) 24.69s (± 0.52%) ~ 24.56s 24.84s p=1.000 n=6
xstate-main - node (v18.15.0, x64)
Errors 5 5 ~ ~ ~ p=1.000 n=6
Symbols 552,233 552,233 ~ ~ ~ p=1.000 n=6
Types 184,971 184,971 ~ ~ ~ p=1.000 n=6
Memory used 492,390k (± 0.00%) 492,333k (± 0.01%) ~ 492,252k 492,398k p=0.128 n=6
Parse Time 3.40s (± 0.31%) 3.42s (± 0.57%) ~ 3.40s 3.45s p=0.119 n=6
Bind Time 1.17s (± 0.64%) 1.19s (± 1.89%) ~ 1.16s 1.21s p=0.216 n=6
Check Time 19.41s (± 0.26%) 19.44s (± 0.30%) ~ 19.38s 19.54s p=0.378 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 23.98s (± 0.23%) 24.05s (± 0.29%) ~ 23.97s 24.17s p=0.109 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,326ms (± 0.58%) 2,333ms (± 0.34%) ~ 2,326ms 2,345ms p=0.468 n=6
Req 2 - geterr 5,321ms (± 0.57%) 5,349ms (± 0.39%) ~ 5,320ms 5,383ms p=0.173 n=6
Req 3 - references 265ms (± 1.53%) 267ms (± 1.44%) ~ 263ms 270ms p=0.666 n=6
Req 4 - navto 227ms (± 0.46%) 228ms (± 0.58%) ~ 226ms 229ms p=0.116 n=6
Req 5 - completionInfo count 1,357 1,357 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 79ms (± 8.26%) 81ms (±10.15%) ~ 76ms 92ms p=0.652 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,437ms (± 1.50%) 2,443ms (± 1.02%) ~ 2,399ms 2,467ms p=0.873 n=6
Req 2 - geterr 4,004ms (± 0.20%) 3,996ms (± 0.37%) ~ 3,976ms 4,017ms p=0.258 n=6
Req 3 - references 280ms (± 1.23%) 280ms (± 1.26%) ~ 273ms 282ms p=0.623 n=6
Req 4 - navto 227ms (± 0.18%) 227ms (± 0.18%) ~ 226ms 227ms p=1.000 n=6
Req 5 - completionInfo count 1,519 1,519 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 83ms (± 5.12%) 83ms (± 5.08%) ~ 75ms 86ms p=0.517 n=6
xstate-main-1-tsserver - node (v18.15.0, x64)
Req 1 - updateOpen 5,314ms (± 0.18%) 5,319ms (± 0.79%) ~ 5,277ms 5,400ms p=0.630 n=6
Req 2 - geterr 1,150ms (± 0.81%) 1,156ms (± 0.64%) ~ 1,146ms 1,167ms p=0.295 n=6
Req 3 - references 83ms 76ms 🟩-7ms (- 8.43%) ~ ~ p=0.001 n=6
Req 4 - navto 452ms (± 1.07%) 450ms (± 0.33%) ~ 448ms 452ms p=0.568 n=6
Req 5 - completionInfo count 3,450 3,450 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 855ms (± 1.39%) 849ms (± 1.08%) ~ 834ms 861ms p=0.421 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstate-main-1-tsserver - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 158.93ms (± 0.21%) 158.98ms (± 0.20%) ~ 157.74ms 162.13ms p=0.086 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 236.48ms (± 0.15%) 236.51ms (± 0.17%) ~ 234.87ms 243.71ms p=0.586 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 232.29ms (± 0.16%) 232.22ms (± 0.16%) ~ 230.84ms 237.70ms p=0.058 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 228.14ms (± 0.17%) 228.22ms (± 0.16%) +0.08ms (+ 0.03%) 226.88ms 230.59ms p=0.035 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

// the root is `file://Users/`
assert.strictEqual(ts.getNormalizedAbsolutePath("file://Users/matb/projects/san/../../../../../../typings/@epic/Core.d.ts", ""), "file://Users/typings/@epic/Core.d.ts");
// this is real
assert.strictEqual(ts.getNormalizedAbsolutePath("file:///Users/matb/projects/san/../../../../../../typings/@epic/Core.d.ts", ""), "file:///typings/@epic/Core.d.ts");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if we want to preserve specific examples in these, but that might be fine.

Maybe we should have tests on untitled:?

return path;
}
// Some paths only require cleanup of `/./` or leading `./`
const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're already here, I'd also recommend

Suggested change
const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, "");
let simplified = path.replace(/\/\.\//g, "/");
if (simplified.startsWith("./") {
simplified = simplified.slice(2);
}

let segmentStart = index;
let normalizedUpTo = index;
let seenNonDotDotSegment = rootLength !== 0;
while (index < path.length) {
Copy link
Member

@rbuckton rbuckton Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since path.length doesn't change, it may be more efficient to store it in a local and reuse that rather than look it up each time (both here and elsewhere in the function).

normalized = path.substring(0, normalizedUpTo);
}
}
else if (segmentLength === 2 && path.charCodeAt(index) === CharacterCodes.dot && path.charCodeAt(index + 1) === CharacterCodes.dot) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: If the previous segmentLength was 1, but the segment was not ., this performs an unnecessary recheck of segmentLength === 2.

Comment on lines +654 to +657
if (normalized === undefined) {
// Seen superfluous separator
normalized = path.substring(0, segmentStart - 1);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (normalized === undefined) {
// Seen superfluous separator
normalized = path.substring(0, segmentStart - 1);
}
// Seen superfluous separator
normalized ??= path.substring(0, segmentStart - 1);

Comment on lines +667 to +670
if (segmentEnd === altSepIndex && normalized === undefined) {
// Seen backslash
normalized = path.substring(0, segmentStart);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (segmentEnd === altSepIndex && normalized === undefined) {
// Seen backslash
normalized = path.substring(0, segmentStart);
}
if (segmentEnd === altSepIndex) {
// Seen backslash
normalized ??= path.substring(0, segmentStart);
}

Comment on lines +674 to +676
if (normalized === undefined) {
normalized = path.substring(0, normalizedUpTo);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (normalized === undefined) {
normalized = path.substring(0, normalizedUpTo);
}
normalized ??= path.substring(0, normalizedUpTo);

Comment on lines +727 to +732
let normalized = simpleNormalizePath(path);
if (normalized !== undefined) {
return normalized;
}
normalized = getNormalizedAbsolutePath(path, "");
return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call to hasTrailingDirectorySeparator is redundant as that check is also performed in ensureTrailingDirectorySeparator. This could be simplified to:

Suggested change
let normalized = simpleNormalizePath(path);
if (normalized !== undefined) {
return normalized;
}
normalized = getNormalizedAbsolutePath(path, "");
return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized;
return simpleNormalizePath(path) ??
ensureTrailingDirectorySeparator(getNormalizedAbsolutePath(path, ""));

Comment on lines +661 to +662
const sepIndex = path.indexOf(directorySeparator, index + 1);
const altSepIndex = path.indexOf(altDirectorySeparator, index + 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or use findIndex(path, isAnyDirectorySeparator, index + 1)

@andrewbranch
Copy link
Member Author

andrewbranch commented Dec 20, 2024

From instrumenting the compilation of VS Code, I learned two things:

  1. The number of normalizePath calls outweighs getNormalizedAbsolutePath by ~300x.
  2. 99% of all calls to either function end up being a no-op.

Optimizing getNormalizedAbsolutePath is unlikely to make a measurable difference.

I'm going to repeat the instrumentation for a TS Server program update and see if the ratios are similar.

@andrewbranch
Copy link
Member Author

During a TS Server update, literally every call is a no-op.

{
  initialize: {
    normalizePath: {
      total: 108354,
      noop: 108026,
      slashes: 0,
      fastReplace: 0,
      full: 328
    },
    getNormalizedAbsolutePath: {
      total: 1978,
      noop: 1958
    }
  },
  update: {
    normalizePath: {
      total: 7839,
      noop: 7839,
      slashes: 0,
      fastReplace: 0,
      full: 0
    },
    getNormalizedAbsolutePath: {
      total: 467,
      noop: 467
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Status: Waiting on author
Development

Successfully merging this pull request may close these issues.

4 participants