perf: reduce renderer and repository polling work
ForgeFlow quality gate / quality (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-08-01 14:24:53 +02:00
parent 258f0b1324
commit 13f4fe7cd0
5 changed files with 105 additions and 61 deletions
+4 -4
View File
@@ -144,7 +144,7 @@ dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
e64f7257d478955c675a133b3735b6afe138a69d2ad090898061e56f557c43e5 9926 src/main/production-acceptance-harness.cjs
e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 src/main/repository-monitor.cjs
fc7a5a2a42579a311f60a96bec4776942bf3f63428928820029b0dcf4874d1aa 2745 src/main/repository-monitor.cjs
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
@@ -164,13 +164,13 @@ d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js
100077a82f14d5252753d017369ecef48ed6d00532166ae5a0b356ead549e02b 24087 src/renderer/app.js
edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 src/renderer/app.js
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js
977af9585074f0a404830c5a93de6939b4897d2eb98ba8ae75c3cf0023dea673 5782 src/renderer/events.js
eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js
@@ -214,7 +214,7 @@ caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392
0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 tests/production-acceptance.test.mjs
629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
62e6d3df7051778124648c808caec634a6b2c439660dc556392ea10beec926d1 1870 tests/repository-monitor.test.mjs
ebd3c0825bc9e2f1690cfd51e93a96bd33e939eb9c546aa373dd948b8cf71a69 7781 tests/repository-service.test.mjs
d49c772e3c7ddaa12dc5a1d4fc4cb474a4d99ae06fa5dab5a6cf1c44acb9ed6f 3463 tests/security-validation.test.mjs
bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 tests/semver.test.mjs
+6 -1
View File
@@ -40,7 +40,10 @@ class RepositoryMonitor {
if (this.running || !this.paths.length) return;
this.running = true;
try {
for (const localPath of this.paths) {
const queue = [...this.paths];
const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
while (queue.length) {
const localPath = queue.shift();
if (this.paused.has(localPath)) continue;
try {
const status = await this.git.status(localPath);
@@ -61,6 +64,8 @@ class RepositoryMonitor {
}
}
}
});
await Promise.all(workers);
} finally {
this.running = false;
}
+9
View File
@@ -173,6 +173,7 @@ const ui = {
setupValidation: null,
activeDeployment: null,
operationPollTimer: null,
inputRenderTimer: null,
isMock: false,
refreshError: null,
autoRefreshPending: false,
@@ -190,6 +191,14 @@ const ui = {
auditEvents: [],
};
function scheduleInputRender(delay = 120) {
if (ui.inputRenderTimer) clearTimeout(ui.inputRenderTimer);
ui.inputRenderTimer = setTimeout(() => {
ui.inputRenderTimer = null;
render();
}, delay);
}
function selectedRepository() {
return (
ui.repositories.find(
+40 -31
View File
@@ -17,20 +17,32 @@ app.addEventListener("click", async (event) => {
app.addEventListener("input", (event) => {
if (event.target.id === "global-search") {
ui.search = event.target.value;
render();
document.querySelector("#global-search")?.focus();
scheduleInputRender();
} else if (event.target.id === "repo-filter") {
ui.repoSearch = event.target.value;
render();
document.querySelector("#repo-filter")?.focus();
scheduleInputRender();
} else if (event.target.id === "commit-message") {
ui.commitMessage = event.target.value;
const position = event.target.selectionStart;
render();
const next = document.querySelector("#commit-message");
if (next) {
next.focus();
next.setSelectionRange(position, position);
const repository = selectedRepository();
const hasSelection = Boolean(ui.selectedFiles.size || repository?.localStatus?.counts?.staged);
const ready = Boolean(hasSelection && ui.commitMessage.trim());
const blocker = !hasSelection
? "Select files or stage one or more hunks."
: ready
? ui.selectedFiles.size
? "Ready to commit. ForgeFlow stages the selected files automatically."
: "Ready to commit only the reviewed staged hunks."
: "Enter a commit message to enable commit and push.";
const readiness = document.querySelector(".commit-readiness");
if (readiness) {
readiness.classList.toggle("ready", ready);
readiness.classList.toggle("blocked", !ready);
readiness.innerHTML = `${icon(ready ? "check" : "warning")}<span>${escapeHtml(blocker)}</span>`;
}
for (const button of document.querySelectorAll('[data-action="commit-push"], [data-action="commit-only"]')) {
button.disabled = !ready;
if (ready) button.removeAttribute("title");
else button.title = blocker;
}
} else if (event.target.id === "setup-url")
ui.setupDraft.baseUrl = event.target.value;
@@ -38,7 +50,7 @@ app.addEventListener("input", (event) => {
ui.setupDraft.token = event.target.value;
else if (event.target.id === "palette-input") {
ui.paletteQuery = event.target.value;
render();
scheduleInputRender(60);
}
});
@@ -102,35 +114,32 @@ document.addEventListener("keydown", (event) => {
}
});
let pointerAnimationFrame = null;
let pendingPointer = null;
document.addEventListener("pointermove", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
pendingPointer = { target: event.target, clientX: event.clientX, clientY: event.clientY };
if (pointerAnimationFrame) return;
pointerAnimationFrame = requestAnimationFrame(() => {
pointerAnimationFrame = null;
const current = pendingPointer;
pendingPointer = null;
if (!current) return;
const illustration = current.target.closest?.("[data-project-illustration]");
if (illustration) {
const bounds = illustration.getBoundingClientRect();
illustration.style.setProperty(
"--tilt-x",
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`,
);
illustration.style.setProperty(
"--tilt-y",
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`,
);
illustration.style.setProperty("--tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`);
illustration.style.setProperty("--tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`);
}
const diffPanel = event.target.closest?.(".diff-panel");
const diffPanel = current.target.closest?.(".diff-panel");
const atmosphere = diffPanel?.querySelector("[data-diff-atmosphere]");
if (atmosphere) {
const bounds = diffPanel.getBoundingClientRect();
atmosphere.style.setProperty(
"--diff-tilt-x",
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`,
);
atmosphere.style.setProperty(
"--diff-tilt-y",
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`,
);
atmosphere.style.setProperty("--diff-tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`);
atmosphere.style.setProperty("--diff-tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`);
}
});
});
document.addEventListener("pointerout", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
if (illustration && !illustration.contains(event.relatedTarget)) {
+21
View File
@@ -28,3 +28,24 @@ test('repository monitor establishes a baseline and emits only on later changes'
await monitor.tick();
assert.equal(changes.length, 2);
});
test('repository monitor checks multiple repositories concurrently with a bounded worker pool', async () => {
let active = 0;
let peak = 0;
const git = {
status: async (localPath) => {
active += 1;
peak = Math.max(peak, active);
await new Promise((resolve) => setTimeout(resolve, 15));
active -= 1;
return { localPath, revision: 1 };
},
statusFingerprint: (status) => String(status.revision)
};
const store = { data: { preferences: { autoRefresh: true, repositoryPollSeconds: 2 } } };
const monitor = new RepositoryMonitor({ store, git });
monitor.setPaths(Array.from({ length: 10 }, (_, index) => `/repo-${index}`));
await monitor.tick();
assert.equal(peak, 4);
assert.equal(active, 0);
assert.equal(monitor.fingerprints.size, 10);
});