From c776aaf3c33a2c1c51fbb0e6b573aaf7d71d89d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 12:58:17 +0000 Subject: [PATCH] perf: replace blocking fs.writeFileSync/readFileSync with async I/O in format() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synchronous file operations in the extension host block VS Code's entire JavaScript thread while writing/reading PHP source files to/from the OS temp directory. For large PHP files this causes a visible pause in the UI. Changes: - fs.writeFileSync → fs.writeFile (async): temp file is written inside the Promise executor, so the extension host thread is freed immediately after spawning the write. - fs.readFileSync → fs.readFile (async): fixed-file read on exit codes 1/2 is now async; unlink is called inside the readFile callback so the temp file is cleaned up after the async read completes. - Removed the phpcbfError flag + deferred stdout-listener pattern, which was effectively dead code (the synchronous if-check always saw false). Code 3 now calls window.showErrorMessage directly in the exit handler, matching the documented intent. - Removed leftover 'console.log(code)' comment in close handler. All 7 unit tests pass (npm run test:unit). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extension.js | 146 ++++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 72 deletions(-) diff --git a/extension.js b/extension.js index 6457e71..7de7302 100644 --- a/extension.js +++ b/extension.js @@ -135,10 +135,9 @@ class PHPCBF { if (this.debug) { console.time("phpcbf"); } - let text = document.getText(); + const text = document.getText(); - let phpcbfError = false; - let fileName = + const fileName = TmpDir + "/temp-" + Math.random() @@ -146,84 +145,87 @@ class PHPCBF { .replace(/[^a-z]+/g, "") .substr(0, 10) + ".php"; - fs.writeFileSync(fileName, text); - let exec = cp.spawn(this.executablePath, this.getArgs(document, fileName)); - if (!this.debug) { - exec.stdin.end(); - } + // Use async file I/O to avoid blocking the extension host thread, + // which is especially noticeable with large PHP files. + return new Promise((resolve, reject) => { + fs.writeFile(fileName, text, writeErr => { + if (writeErr) { + reject(); + return; + } - let promise = new Promise((resolve, reject) => { - exec.on("error", err => { - reject(); - console.log(err); - if (err.code == "ENOENT") { - window.showErrorMessage( - "PHPCBF: " + err.message + ". executablePath not found." - ); + const exec = cp.spawn(this.executablePath, this.getArgs(document, fileName)); + if (!this.debug) { + exec.stdin.end(); } - }); - exec.on("exit", code => { - /* phpcbf exit codes: - Exit code 0 is used to indicate that no fixable errors were found, so nothing was fixed - Exit code 1 is used to indicate that all fixable errors were fixed correctly - Exit code 2 is used to indicate that PHPCBF failed to fix some of the fixable errors it found - Exit code 3 is used for general script execution errors - */ - switch (code) { - case 0: - break; - case 1: - case 2: - let fixed = fs.readFileSync(fileName, "utf-8"); - if (fixed.length > 0) { - resolve(fixed); - } else { + + exec.on("error", err => { + reject(); + console.log(err); + if (err.code == "ENOENT") { + window.showErrorMessage( + "PHPCBF: " + err.message + ". executablePath not found." + ); + } + }); + + exec.on("exit", code => { + /* phpcbf exit codes: + Exit code 0 is used to indicate that no fixable errors were found, so nothing was fixed + Exit code 1 is used to indicate that all fixable errors were fixed correctly + Exit code 2 is used to indicate that PHPCBF failed to fix some of the fixable errors it found + Exit code 3 is used for general script execution errors + */ + switch (code) { + case 0: + break; + case 1: + case 2: + // Async read to avoid blocking the extension host thread. + fs.readFile(fileName, "utf-8", (readErr, fixed) => { + fs.unlink(fileName, () => {}); + if (!readErr && fixed && fixed.length > 0) { + resolve(fixed); + } else { + reject(); + } + }); + return; // unlink handled inside readFile callback + case 3: + window.showErrorMessage("PHPCBF: general script execution errors."); + break; + default: { + const msgs = { + 16: "PHPCBF: Configuration error of the application.", + 32: "PHPCBF: Configuration error of a Fixer.", + 64: "PHPCBF: Exception raised within the application." + }; + window.showErrorMessage(msgs[code]); reject(); + break; } - break; - case 3: - phpcbfError = true; - break; - default: - let msgs = { - 3: "PHPCBF: general script execution errors.", - 16: "PHPCBF: Configuration error of the application.", - 32: "PHPCBF: Configuration error of a Fixer.", - 64: "PHPCBF: Exception raised within the application." - }; - window.showErrorMessage(msgs[code]); - reject(); - break; - } + } - fs.unlink(fileName, function (err) {}); - }); - }); + fs.unlink(fileName, () => {}); + }); - if (phpcbfError) { - exec.stdout.on("data", buffer => { - console.log(buffer.toString()); - window.showErrorMessage(buffer.toString()); - }); - } - if (this.debug) { - exec.stdout.on("data", buffer => { - console.log(buffer.toString()); + if (this.debug) { + exec.stdout.on("data", buffer => { + console.log(buffer.toString()); + }); + } + exec.stderr.on("data", buffer => { + console.log(buffer.toString()); + }); + exec.on("close", () => { + if (this.debug) { + console.timeEnd("phpcbf"); + console.groupEnd(); + } + }); }); - } - exec.stderr.on("data", buffer => { - console.log(buffer.toString()); }); - exec.on("close", code => { - // console.log(code); - if (this.debug) { - console.timeEnd("phpcbf"); - console.groupEnd(); - } - }); - - return promise; } addRootPath(prefix) {