Skip to content

Commit aed1e8a

Browse files
authoredJun 11, 2024··
Reduce language server latency (#1003)
* avoid recomputing project roots and ReScript versions as much as possible * more precomputation * changelog
1 parent 74728bf commit aed1e8a

File tree

5 files changed

+117
-63
lines changed

5 files changed

+117
-63
lines changed
 

‎CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
1313
## master
1414

15+
#### :nail_care: Polish
16+
17+
- Reduce latency of language server by caching a few project config related things. https://github.com/rescript-lang/rescript-vscode/pull/1003
18+
1519
#### :bug: Bug Fix
1620

1721
- Fix edge case in switch expr completion. https://github.com/rescript-lang/rescript-vscode/pull/1002

‎server/src/incrementalCompilation.ts

+25-28
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import config, { send } from "./config";
1010
import * as c from "./constants";
1111
import * as chokidar from "chokidar";
1212
import { fileCodeActions } from "./codeActions";
13+
import { projectsFiles } from "./projectFiles";
1314

1415
function debug() {
1516
return (
@@ -75,8 +76,6 @@ type IncrementallyCompiledFileInfo = {
7576
callArgs: Promise<Array<string> | null>;
7677
/** The location of the incremental folder for this project. */
7778
incrementalFolderPath: string;
78-
/** The ReScript version. */
79-
rescriptVersion: string;
8079
};
8180
/** Any code actions for this incremental file. */
8281
codeActions: Array<fileCodeActions>;
@@ -284,6 +283,8 @@ function getBscArgs(
284283
});
285284
} else if (buildSystem === "rewatch") {
286285
try {
286+
const project = projectsFiles.get(entry.project.rootPath);
287+
if (project?.rescriptVersion == null) return;
287288
let rewatchPath = path.resolve(
288289
entry.project.workspaceRootPath,
289290
"node_modules/@rolandpeelen/rewatch/rewatch"
@@ -292,7 +293,7 @@ function getBscArgs(
292293
cp
293294
.execFileSync(rewatchPath, [
294295
"--rescript-version",
295-
entry.project.rescriptVersion,
296+
project.rescriptVersion,
296297
"--compiler-args",
297298
entry.file.sourceFilePath,
298299
])
@@ -364,21 +365,21 @@ function triggerIncrementalCompilationOfFile(
364365
if (incrementalFileCacheEntry == null) {
365366
// New file
366367
const projectRootPath = utils.findProjectRootOfFile(filePath);
367-
const workspaceRootPath = projectRootPath
368-
? utils.findProjectRootOfFile(projectRootPath)
369-
: null;
370368
if (projectRootPath == null) {
371369
if (debug())
372370
console.log("Did not find project root path for " + filePath);
373371
return;
374372
}
375-
const namespaceName = utils.getNamespaceNameFromConfigFile(projectRootPath);
376-
if (namespaceName.kind === "error") {
377-
if (debug())
378-
console.log("Getting namespace config errored for " + filePath);
373+
const project = projectsFiles.get(projectRootPath);
374+
if (project == null) {
375+
if (debug()) console.log("Did not find open project for " + filePath);
379376
return;
380377
}
381-
const bscBinaryLocation = utils.findBscExeBinary(projectRootPath);
378+
const workspaceRootPath = projectRootPath
379+
? utils.findProjectRootOfFile(projectRootPath)
380+
: null;
381+
382+
const bscBinaryLocation = project.bscBinaryLocation;
382383
if (bscBinaryLocation == null) {
383384
if (debug())
384385
console.log("Could not find bsc binary location for " + filePath);
@@ -387,28 +388,15 @@ function triggerIncrementalCompilationOfFile(
387388
const ext = filePath.endsWith(".resi") ? ".resi" : ".res";
388389
const moduleName = path.basename(filePath, ext);
389390
const moduleNameNamespaced =
390-
namespaceName.result !== ""
391-
? `${moduleName}-${namespaceName.result}`
391+
project.namespaceName != null
392+
? `${moduleName}-${project.namespaceName}`
392393
: moduleName;
393394

394395
const incrementalFolderPath = path.join(
395396
projectRootPath,
396397
INCREMENTAL_FILE_FOLDER_LOCATION
397398
);
398399

399-
let rescriptVersion = "";
400-
try {
401-
rescriptVersion = cp
402-
.execFileSync(bscBinaryLocation, ["-version"])
403-
.toString()
404-
.trim();
405-
} catch (e) {
406-
console.error(e);
407-
}
408-
if (rescriptVersion.startsWith("ReScript ")) {
409-
rescriptVersion = rescriptVersion.replace("ReScript ", "");
410-
}
411-
412400
let originalTypeFileLocation = path.resolve(
413401
projectRootPath,
414402
"lib/bs",
@@ -436,7 +424,6 @@ function triggerIncrementalCompilationOfFile(
436424
callArgs: Promise.resolve([]),
437425
bscBinaryLocation,
438426
incrementalFolderPath,
439-
rescriptVersion,
440427
},
441428
buildRewatch: null,
442429
buildNinja: null,
@@ -488,6 +475,16 @@ function verifyTriggerToken(filePath: string, triggerToken: number): boolean {
488475
);
489476
}
490477
async function figureOutBscArgs(entry: IncrementallyCompiledFileInfo) {
478+
const project = projectsFiles.get(entry.project.rootPath);
479+
if (project?.rescriptVersion == null) {
480+
if (debug()) {
481+
console.log(
482+
"Found no project (or ReScript version) for " +
483+
entry.file.sourceFilePath
484+
);
485+
}
486+
return null;
487+
}
491488
const res = await getBscArgs(entry);
492489
if (res == null) return null;
493490
let astArgs: Array<Array<string>> = [];
@@ -547,7 +544,7 @@ async function figureOutBscArgs(entry: IncrementallyCompiledFileInfo) {
547544
});
548545

549546
callArgs.push("-color", "never");
550-
if (parseInt(entry.project.rescriptVersion.split(".")[0] ?? "10") >= 11) {
547+
if (parseInt(project.rescriptVersion.split(".")[0] ?? "10") >= 11) {
551548
// Only available in v11+
552549
callArgs.push("-ignore-parse-errors");
553550
}

‎server/src/projectFiles.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as cp from "node:child_process";
2+
import * as p from "vscode-languageserver-protocol";
3+
4+
export type filesDiagnostics = {
5+
[key: string]: p.Diagnostic[];
6+
};
7+
8+
interface projectFiles {
9+
openFiles: Set<string>;
10+
filesWithDiagnostics: Set<string>;
11+
filesDiagnostics: filesDiagnostics;
12+
rescriptVersion: string | undefined;
13+
bscBinaryLocation: string | null;
14+
namespaceName: string | null;
15+
16+
bsbWatcherByEditor: null | cp.ChildProcess;
17+
18+
// This keeps track of whether we've prompted the user to start a build
19+
// automatically, if there's no build currently running for the project. We
20+
// only want to prompt the user about this once, or it becomes
21+
// annoying.
22+
// The type `never` means that we won't show the prompt if the project is inside node_modules
23+
hasPromptedToStartBuild: boolean | "never";
24+
}
25+
26+
export let projectsFiles: Map<string, projectFiles> = // project root path
27+
new Map();

‎server/src/server.ts

+12-20
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ import * as c from "./constants";
2222
import * as chokidar from "chokidar";
2323
import { assert } from "console";
2424
import { fileURLToPath } from "url";
25-
import * as cp from "node:child_process";
2625
import { WorkspaceEdit } from "vscode-languageserver";
27-
import { filesDiagnostics } from "./utils";
2826
import { onErrorReported } from "./errorReporter";
2927
import * as ic from "./incrementalCompilation";
3028
import config, { extensionConfiguration } from "./config";
29+
import { projectsFiles } from "./projectFiles";
3130

3231
// This holds client capabilities specific to our extension, and not necessarily
3332
// related to the LS protocol. It's for enabling/disabling features that might
@@ -49,23 +48,7 @@ let serverSentRequestIdCounter = 0;
4948
// https://microsoft.github.io/language-server-protocol/specification#exit
5049
let shutdownRequestAlreadyReceived = false;
5150
let stupidFileContentCache: Map<string, string> = new Map();
52-
let projectsFiles: Map<
53-
string, // project root path
54-
{
55-
openFiles: Set<string>;
56-
filesWithDiagnostics: Set<string>;
57-
filesDiagnostics: filesDiagnostics;
58-
59-
bsbWatcherByEditor: null | cp.ChildProcess;
60-
61-
// This keeps track of whether we've prompted the user to start a build
62-
// automatically, if there's no build currently running for the project. We
63-
// only want to prompt the user about this once, or it becomes
64-
// annoying.
65-
// The type `never` means that we won't show the prompt if the project is inside node_modules
66-
hasPromptedToStartBuild: boolean | "never";
67-
}
68-
> = new Map();
51+
6952
// ^ caching AND states AND distributed system. Why does LSP has to be stupid like this
7053

7154
// This keeps track of code actions extracted from diagnostics.
@@ -275,11 +258,18 @@ let openedFile = (fileUri: string, fileContent: string) => {
275258
if (config.extensionConfiguration.incrementalTypechecking?.enabled) {
276259
ic.recreateIncrementalFileFolder(projectRootPath);
277260
}
261+
const namespaceName =
262+
utils.getNamespaceNameFromConfigFile(projectRootPath);
263+
278264
projectRootState = {
279265
openFiles: new Set(),
280266
filesWithDiagnostics: new Set(),
281267
filesDiagnostics: {},
268+
namespaceName:
269+
namespaceName.kind === "success" ? namespaceName.result : null,
270+
rescriptVersion: utils.findReScriptVersion(projectRootPath),
282271
bsbWatcherByEditor: null,
272+
bscBinaryLocation: utils.findBscExeBinary(projectRootPath),
283273
hasPromptedToStartBuild: /(\/|\\)node_modules(\/|\\)/.test(
284274
projectRootPath
285275
)
@@ -811,7 +801,9 @@ function format(msg: p.RequestMessage): Array<p.Message> {
811801
let code = getOpenedFileContent(params.textDocument.uri);
812802

813803
let projectRootPath = utils.findProjectRootOfFile(filePath);
814-
let bscExeBinaryPath = utils.findBscExeBinary(projectRootPath);
804+
let project =
805+
projectRootPath != null ? projectsFiles.get(projectRootPath) : null;
806+
let bscExeBinaryPath = project?.bscBinaryLocation ?? null;
815807

816808
let formattedResult = utils.formatCode(
817809
bscExeBinaryPath,

‎server/src/utils.ts

+49-15
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as c from "./constants";
1414
import * as lookup from "./lookup";
1515
import { reportError } from "./errorReporter";
1616
import config from "./config";
17+
import { filesDiagnostics, projectsFiles } from "./projectFiles";
1718

1819
let tempFilePrefix = "rescript_format_file_" + process.pid + "_";
1920
let tempFileId = 0;
@@ -24,9 +25,7 @@ export let createFileInTempDir = (extension = "") => {
2425
return path.join(os.tmpdir(), tempFileName);
2526
};
2627

27-
// TODO: races here?
28-
// TODO: this doesn't handle file:/// scheme
29-
export let findProjectRootOfFile = (
28+
let findProjectRootOfFileInDir = (
3029
source: p.DocumentUri
3130
): null | p.DocumentUri => {
3231
let dir = path.dirname(source);
@@ -40,11 +39,41 @@ export let findProjectRootOfFile = (
4039
// reached top
4140
return null;
4241
} else {
43-
return findProjectRootOfFile(dir);
42+
return findProjectRootOfFileInDir(dir);
4443
}
4544
}
4645
};
4746

47+
// TODO: races here?
48+
// TODO: this doesn't handle file:/// scheme
49+
export let findProjectRootOfFile = (
50+
source: p.DocumentUri
51+
): null | p.DocumentUri => {
52+
// First look in project files
53+
let foundRootFromProjectFiles: string | null = null;
54+
55+
for (const rootPath of projectsFiles.keys()) {
56+
if (source.startsWith(rootPath)) {
57+
// Prefer the longest path (most nested)
58+
if (
59+
foundRootFromProjectFiles == null ||
60+
rootPath.length > foundRootFromProjectFiles.length
61+
) {
62+
foundRootFromProjectFiles = rootPath;
63+
}
64+
}
65+
}
66+
67+
if (foundRootFromProjectFiles != null) {
68+
return foundRootFromProjectFiles;
69+
} else {
70+
const isDir = path.extname(source) === "";
71+
return findProjectRootOfFileInDir(
72+
isDir ? path.join(source, "dummy.res") : source
73+
);
74+
}
75+
};
76+
4877
// Check if binaryName exists inside binaryDirPath and return the joined path.
4978
export let findBinary = (
5079
binaryDirPath: p.DocumentUri | null,
@@ -138,7 +167,9 @@ export let formatCode = (
138167
}
139168
};
140169

141-
let findReScriptVersion = (filePath: p.DocumentUri): string | undefined => {
170+
export let findReScriptVersion = (
171+
filePath: p.DocumentUri
172+
): string | undefined => {
142173
let projectRoot = findProjectRootOfFile(filePath);
143174
if (projectRoot == null) {
144175
return undefined;
@@ -161,25 +192,31 @@ let findReScriptVersion = (filePath: p.DocumentUri): string | undefined => {
161192
}
162193
};
163194

195+
let binaryPath: string | null = null;
196+
if (fs.existsSync(c.analysisDevPath)) {
197+
binaryPath = c.analysisDevPath;
198+
} else if (fs.existsSync(c.analysisProdPath)) {
199+
binaryPath = c.analysisProdPath;
200+
} else {
201+
}
202+
164203
export let runAnalysisAfterSanityCheck = (
165204
filePath: p.DocumentUri,
166205
args: Array<any>,
167206
projectRequired = false
168207
) => {
169-
let binaryPath;
170-
if (fs.existsSync(c.analysisDevPath)) {
171-
binaryPath = c.analysisDevPath;
172-
} else if (fs.existsSync(c.analysisProdPath)) {
173-
binaryPath = c.analysisProdPath;
174-
} else {
208+
if (binaryPath == null) {
175209
return null;
176210
}
177211

178212
let projectRootPath = findProjectRootOfFile(filePath);
179213
if (projectRootPath == null && projectRequired) {
180214
return null;
181215
}
182-
let rescriptVersion = findReScriptVersion(filePath);
216+
let rescriptVersion =
217+
projectsFiles.get(projectRootPath ?? "")?.rescriptVersion ??
218+
findReScriptVersion(filePath);
219+
183220
let options: childProcess.ExecFileSyncOptions = {
184221
cwd: projectRootPath || undefined,
185222
maxBuffer: Infinity,
@@ -449,9 +486,6 @@ let parseFileAndRange = (fileAndRange: string) => {
449486
};
450487

451488
// main parsing logic
452-
export type filesDiagnostics = {
453-
[key: string]: p.Diagnostic[];
454-
};
455489
type parsedCompilerLogResult = {
456490
done: boolean;
457491
result: filesDiagnostics;

0 commit comments

Comments
 (0)
Please sign in to comment.