perf: reduce renderer and repository polling work
ForgeFlow quality gate / quality (push) Canceled after 0s
ForgeFlow quality gate / quality (push) Canceled after 0s
This commit is contained in:
+4
-4
@@ -144,7 +144,7 @@ dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442
|
|||||||
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
|
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
|
||||||
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
|
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
|
||||||
e64f7257d478955c675a133b3735b6afe138a69d2ad090898061e56f557c43e5 9926 src/main/production-acceptance-harness.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
|
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
|
||||||
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
|
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
|
||||||
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
|
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
|
||||||
@@ -164,13 +164,13 @@ d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706
|
|||||||
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
||||||
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
||||||
5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.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
|
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
|
||||||
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
||||||
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
|
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
|
||||||
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
|
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
|
||||||
1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js
|
1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js
|
||||||
977af9585074f0a404830c5a93de6939b4897d2eb98ba8ae75c3cf0023dea673 5782 src/renderer/events.js
|
eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js
|
||||||
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
||||||
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
||||||
9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js
|
9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js
|
||||||
@@ -214,7 +214,7 @@ caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392
|
|||||||
0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 tests/production-acceptance.test.mjs
|
0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 tests/production-acceptance.test.mjs
|
||||||
629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
|
629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
|
||||||
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.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
|
ebd3c0825bc9e2f1690cfd51e93a96bd33e939eb9c546aa373dd948b8cf71a69 7781 tests/repository-service.test.mjs
|
||||||
d49c772e3c7ddaa12dc5a1d4fc4cb474a4d99ae06fa5dab5a6cf1c44acb9ed6f 3463 tests/security-validation.test.mjs
|
d49c772e3c7ddaa12dc5a1d4fc4cb474a4d99ae06fa5dab5a6cf1c44acb9ed6f 3463 tests/security-validation.test.mjs
|
||||||
bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 tests/semver.test.mjs
|
bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 tests/semver.test.mjs
|
||||||
|
|||||||
@@ -40,27 +40,32 @@ class RepositoryMonitor {
|
|||||||
if (this.running || !this.paths.length) return;
|
if (this.running || !this.paths.length) return;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
try {
|
try {
|
||||||
for (const localPath of this.paths) {
|
const queue = [...this.paths];
|
||||||
if (this.paused.has(localPath)) continue;
|
const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
|
||||||
try {
|
while (queue.length) {
|
||||||
const status = await this.git.status(localPath);
|
const localPath = queue.shift();
|
||||||
const next = this.git.statusFingerprint(status);
|
if (this.paused.has(localPath)) continue;
|
||||||
const previous = this.fingerprints.get(localPath);
|
try {
|
||||||
this.fingerprints.set(localPath, next);
|
const status = await this.git.status(localPath);
|
||||||
if (previous && previous !== next) {
|
const next = this.git.statusFingerprint(status);
|
||||||
await this.diagnostics?.debug('repository-monitor.changed', { localPath, head: status.head, branch: status.branch?.head, counts: status.counts });
|
const previous = this.fingerprints.get(localPath);
|
||||||
this.onChange?.({ localPath, status, reason: 'working-tree-changed' });
|
this.fingerprints.set(localPath, next);
|
||||||
}
|
if (previous && previous !== next) {
|
||||||
} catch (error) {
|
await this.diagnostics?.debug('repository-monitor.changed', { localPath, head: status.head, branch: status.branch?.head, counts: status.counts });
|
||||||
const next = `error:${error.message}`;
|
this.onChange?.({ localPath, status, reason: 'working-tree-changed' });
|
||||||
const previous = this.fingerprints.get(localPath);
|
}
|
||||||
this.fingerprints.set(localPath, next);
|
} catch (error) {
|
||||||
if (previous && previous !== next) {
|
const next = `error:${error.message}`;
|
||||||
await this.diagnostics?.warning('repository-monitor.unavailable', { localPath, message: error.message });
|
const previous = this.fingerprints.get(localPath);
|
||||||
this.onChange?.({ localPath, error: error.message, reason: 'repository-unavailable' });
|
this.fingerprints.set(localPath, next);
|
||||||
|
if (previous && previous !== next) {
|
||||||
|
await this.diagnostics?.warning('repository-monitor.unavailable', { localPath, message: error.message });
|
||||||
|
this.onChange?.({ localPath, error: error.message, reason: 'repository-unavailable' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
} finally {
|
} finally {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const ui = {
|
|||||||
setupValidation: null,
|
setupValidation: null,
|
||||||
activeDeployment: null,
|
activeDeployment: null,
|
||||||
operationPollTimer: null,
|
operationPollTimer: null,
|
||||||
|
inputRenderTimer: null,
|
||||||
isMock: false,
|
isMock: false,
|
||||||
refreshError: null,
|
refreshError: null,
|
||||||
autoRefreshPending: false,
|
autoRefreshPending: false,
|
||||||
@@ -190,6 +191,14 @@ const ui = {
|
|||||||
auditEvents: [],
|
auditEvents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function scheduleInputRender(delay = 120) {
|
||||||
|
if (ui.inputRenderTimer) clearTimeout(ui.inputRenderTimer);
|
||||||
|
ui.inputRenderTimer = setTimeout(() => {
|
||||||
|
ui.inputRenderTimer = null;
|
||||||
|
render();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
function selectedRepository() {
|
function selectedRepository() {
|
||||||
return (
|
return (
|
||||||
ui.repositories.find(
|
ui.repositories.find(
|
||||||
|
|||||||
+47
-38
@@ -17,20 +17,32 @@ app.addEventListener("click", async (event) => {
|
|||||||
app.addEventListener("input", (event) => {
|
app.addEventListener("input", (event) => {
|
||||||
if (event.target.id === "global-search") {
|
if (event.target.id === "global-search") {
|
||||||
ui.search = event.target.value;
|
ui.search = event.target.value;
|
||||||
render();
|
scheduleInputRender();
|
||||||
document.querySelector("#global-search")?.focus();
|
|
||||||
} else if (event.target.id === "repo-filter") {
|
} else if (event.target.id === "repo-filter") {
|
||||||
ui.repoSearch = event.target.value;
|
ui.repoSearch = event.target.value;
|
||||||
render();
|
scheduleInputRender();
|
||||||
document.querySelector("#repo-filter")?.focus();
|
|
||||||
} else if (event.target.id === "commit-message") {
|
} else if (event.target.id === "commit-message") {
|
||||||
ui.commitMessage = event.target.value;
|
ui.commitMessage = event.target.value;
|
||||||
const position = event.target.selectionStart;
|
const repository = selectedRepository();
|
||||||
render();
|
const hasSelection = Boolean(ui.selectedFiles.size || repository?.localStatus?.counts?.staged);
|
||||||
const next = document.querySelector("#commit-message");
|
const ready = Boolean(hasSelection && ui.commitMessage.trim());
|
||||||
if (next) {
|
const blocker = !hasSelection
|
||||||
next.focus();
|
? "Select files or stage one or more hunks."
|
||||||
next.setSelectionRange(position, position);
|
: 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")
|
} else if (event.target.id === "setup-url")
|
||||||
ui.setupDraft.baseUrl = event.target.value;
|
ui.setupDraft.baseUrl = event.target.value;
|
||||||
@@ -38,7 +50,7 @@ app.addEventListener("input", (event) => {
|
|||||||
ui.setupDraft.token = event.target.value;
|
ui.setupDraft.token = event.target.value;
|
||||||
else if (event.target.id === "palette-input") {
|
else if (event.target.id === "palette-input") {
|
||||||
ui.paletteQuery = event.target.value;
|
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) => {
|
document.addEventListener("pointermove", (event) => {
|
||||||
const illustration = event.target.closest?.("[data-project-illustration]");
|
pendingPointer = { target: event.target, clientX: event.clientX, clientY: event.clientY };
|
||||||
if (illustration) {
|
if (pointerAnimationFrame) return;
|
||||||
const bounds = illustration.getBoundingClientRect();
|
pointerAnimationFrame = requestAnimationFrame(() => {
|
||||||
illustration.style.setProperty(
|
pointerAnimationFrame = null;
|
||||||
"--tilt-x",
|
const current = pendingPointer;
|
||||||
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`,
|
pendingPointer = null;
|
||||||
);
|
if (!current) return;
|
||||||
illustration.style.setProperty(
|
const illustration = current.target.closest?.("[data-project-illustration]");
|
||||||
"--tilt-y",
|
if (illustration) {
|
||||||
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`,
|
const bounds = illustration.getBoundingClientRect();
|
||||||
);
|
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]");
|
const atmosphere = diffPanel?.querySelector("[data-diff-atmosphere]");
|
||||||
if (atmosphere) {
|
if (atmosphere) {
|
||||||
const bounds = diffPanel.getBoundingClientRect();
|
const bounds = diffPanel.getBoundingClientRect();
|
||||||
atmosphere.style.setProperty(
|
atmosphere.style.setProperty("--diff-tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`);
|
||||||
"--diff-tilt-x",
|
atmosphere.style.setProperty("--diff-tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`);
|
||||||
`${((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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("pointerout", (event) => {
|
document.addEventListener("pointerout", (event) => {
|
||||||
const illustration = event.target.closest?.("[data-project-illustration]");
|
const illustration = event.target.closest?.("[data-project-illustration]");
|
||||||
if (illustration && !illustration.contains(event.relatedTarget)) {
|
if (illustration && !illustration.contains(event.relatedTarget)) {
|
||||||
|
|||||||
@@ -28,3 +28,24 @@ test('repository monitor establishes a baseline and emits only on later changes'
|
|||||||
await monitor.tick();
|
await monitor.tick();
|
||||||
assert.equal(changes.length, 2);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user