@zhangfengshun/dsh-remote-ssh 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/client.js +40 -14
- package/lib/index.js +28 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [2.3.1] — 修复 2.3.0 上线后的界面卡顿(GC 压力 + 客户端全页扫描)
|
|
6
|
+
### 修复
|
|
7
|
+
- **读缓存字节预算(review m1)**:单条结果 >1MiB 不再缓存(结果照常返回),缓存总量上限 32MB 超限逐出最旧条目——消除大文件驻留字符串造成的 GC 压力(最坏 ~270MB → ≤32MB)。
|
|
8
|
+
- **设置导航齿轮观察器改为增量扫描**:此前任何 DOM 变动都会触发全文档 `querySelectorAll("button")` 扫描(流式聊天期间持续打断主线程);现改为只扫描新增子树、节流 500ms、后台页签零开销,稳态成本≈0。
|
|
9
|
+
- **exec 失败不再 bump epoch(review m3)**:失败的命令通常无副作用,不再无谓打断整代缓存(部分副作用由 ≤5s TTL 兜底)。
|
|
10
|
+
- **git 只读命令不再整代失效(review m4)**:仅 add/reset/commit/checkout/revert/cherry-pick 变更类子命令 bump,status/diff/log/branch/show 不再打断缓存。
|
|
11
|
+
|
|
5
12
|
## [2.3.0] — 远程文件打开提速:单往返合并读 + 结果缓存(实测 ≈1.3×~5×)
|
|
6
13
|
### 性能
|
|
7
14
|
- **fs.read 单往返合并读**:size/mtime 与文件内容合并为一条池化命令(`__DSH_ST__` 帧,`_` 不在 base64 字母表故标记天然抗噪),去掉每文件一次的 stat 往返。真实 HPC 实测(100KB 文件,各 10 次 p50):raw 直传 396.6→301.8ms、base64 443.8→337.9ms,约 **1.31×**;1MB 大文件同步受益。>4MB 仍 `head -c 4MiB` 截断,二进制探测与解码语义不变。
|
package/lib/client.js
CHANGED
|
@@ -1115,32 +1115,58 @@ window.__ModuleLoader__.load({
|
|
|
1115
1115
|
// 不可靠。这里改为按条目文本(settings.nav)定位设置导航按钮,隐藏其 svg 图标;
|
|
1116
1116
|
// 设置面板按需挂载、语言会切换,用 MutationObserver + 语言订阅自动重跑。
|
|
1117
1117
|
if (slots) {
|
|
1118
|
+
function checkCell(btn) {
|
|
1119
|
+
var label = i18n.t("settings.nav");
|
|
1120
|
+
var text = (btn.textContent || "").trim();
|
|
1121
|
+
if (text !== label.trim() || text === "") return false;
|
|
1122
|
+
var icon = btn.querySelector("svg");
|
|
1123
|
+
if (icon) { icon.style.display = "none"; return true; }
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
function checkCellIn(root) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (root.tagName && String(root.tagName).toLowerCase() === "button") checkCell(root);
|
|
1129
|
+
var cells = root.querySelectorAll("button");
|
|
1130
|
+
for (var i = 0; i < cells.length; i++) checkCell(cells[i]);
|
|
1131
|
+
} catch (e) {}
|
|
1132
|
+
}
|
|
1118
1133
|
function hideRemoteGearIcon() {
|
|
1119
1134
|
try {
|
|
1120
|
-
var label = i18n.t("settings.nav");
|
|
1121
1135
|
var buttons = document.querySelectorAll("button");
|
|
1122
|
-
for (var i = 0; i < buttons.length; i++)
|
|
1123
|
-
var btn = buttons[i];
|
|
1124
|
-
var text = (btn.textContent || "").trim();
|
|
1125
|
-
if (text === label.trim() && text !== "") {
|
|
1126
|
-
var icon = btn.querySelector("svg");
|
|
1127
|
-
if (icon) { icon.style.display = "none"; return true; }
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1136
|
+
for (var i = 0; i < buttons.length; i++) checkCell(buttons[i]);
|
|
1130
1137
|
} catch (e) {}
|
|
1131
|
-
return false;
|
|
1132
1138
|
}
|
|
1133
1139
|
ctx.effect(function () {
|
|
1134
1140
|
if (typeof document === "undefined") return;
|
|
1135
1141
|
hideRemoteGearIcon();
|
|
1136
1142
|
var timer = null;
|
|
1137
|
-
var
|
|
1143
|
+
var pendingRoots = null;
|
|
1144
|
+
var schedule = function (root) {
|
|
1145
|
+
if (root && root.nodeType === 1) (pendingRoots || (pendingRoots = [])).push(root);
|
|
1138
1146
|
if (timer) return;
|
|
1139
|
-
timer = setTimeout(function () {
|
|
1147
|
+
timer = setTimeout(function () {
|
|
1148
|
+
timer = null;
|
|
1149
|
+
var roots = pendingRoots;
|
|
1150
|
+
pendingRoots = null;
|
|
1151
|
+
if (document.hidden) return; // 后台页签零开销
|
|
1152
|
+
if (roots && roots.length) {
|
|
1153
|
+
// 只扫新增子树:流式聊天期间不做全文档扫描(稳态成本≈0)
|
|
1154
|
+
for (var i = 0; i < roots.length; i++) {
|
|
1155
|
+
if (roots[i].isConnected) checkCellIn(roots[i]);
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
hideRemoteGearIcon(); // 语言切换等无根场景全量扫一次
|
|
1159
|
+
}
|
|
1160
|
+
}, 500);
|
|
1140
1161
|
};
|
|
1141
|
-
var mo = new MutationObserver(
|
|
1162
|
+
var mo = new MutationObserver(function (muts) {
|
|
1163
|
+
for (var i = 0; i < muts.length; i++) {
|
|
1164
|
+
var added = muts[i].addedNodes;
|
|
1165
|
+
for (var j = 0; j < added.length; j++) schedule(added[j]);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1142
1168
|
mo.observe(document.body, { childList: true, subtree: true });
|
|
1143
|
-
var off = i18n.subscribe(schedule);
|
|
1169
|
+
var off = i18n.subscribe(function () { schedule(null); });
|
|
1144
1170
|
return function () {
|
|
1145
1171
|
mo.disconnect();
|
|
1146
1172
|
if (off) off();
|
package/lib/index.js
CHANGED
|
@@ -518,6 +518,23 @@ function lruPut(map, key, val, max) {
|
|
|
518
518
|
while (map.size > max) map.delete(map.keys().next().value);
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
/** 读缓存字节预算(review m1):单条 >1MiB 不缓存、总量 ≤32MB,防止大文件驻留
|
|
522
|
+
* 造成 GC 压力拖慢整个宿主进程。超预算时从最旧条目开始逐出。 */
|
|
523
|
+
const READ_CACHE_MAX_ENTRY_BYTES = 1024 * 1024;
|
|
524
|
+
const READ_CACHE_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
525
|
+
function readCachePut(key, entry, max) {
|
|
526
|
+
entry.bytes = Buffer.byteLength(entry.content, "utf8");
|
|
527
|
+
if (entry.bytes > READ_CACHE_MAX_ENTRY_BYTES) return; // 大文件不缓存(结果照常返回)
|
|
528
|
+
lruPut(readCache, key, entry, max);
|
|
529
|
+
let total = 0;
|
|
530
|
+
for (const v of readCache.values()) total += v.bytes || 0;
|
|
531
|
+
while (total > READ_CACHE_TOTAL_BYTES && readCache.size > 1) {
|
|
532
|
+
const oldest = readCache.keys().next().value;
|
|
533
|
+
total -= readCache.get(oldest).bytes || 0;
|
|
534
|
+
readCache.delete(oldest);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
521
538
|
function normalizeCachePath(path) {
|
|
522
539
|
let s = String(path || "");
|
|
523
540
|
while (s.length > 1 && s.charCodeAt(s.length - 1) === 47) s = s.slice(0, -1);
|
|
@@ -689,7 +706,7 @@ async function remoteReadFile(runner, p, path) {
|
|
|
689
706
|
}
|
|
690
707
|
if (rst.framed && rst.size === hit.size && rst.mtime === hit.mtime) {
|
|
691
708
|
hit.at = Date.now();
|
|
692
|
-
|
|
709
|
+
readCachePut(key, hit, READ_CACHE_MAX);
|
|
693
710
|
return readResultOk(path, hit.content, hit.binary, hit.truncated);
|
|
694
711
|
}
|
|
695
712
|
}
|
|
@@ -705,7 +722,7 @@ async function remoteReadFile(runner, p, path) {
|
|
|
705
722
|
if (rawOk) {
|
|
706
723
|
const truncated = st.size !== null && st.size > MAX_BYTES;
|
|
707
724
|
if (!r.truncated) {
|
|
708
|
-
|
|
725
|
+
readCachePut(key, { content: rawOk.text, binary: rawOk.binary, truncated: truncated, size: st.size, mtime: st.mtime, epoch: epoch, at: Date.now() }, READ_CACHE_MAX);
|
|
709
726
|
}
|
|
710
727
|
return readResultOk(path, rawOk.text, rawOk.binary, truncated || !!r.truncated);
|
|
711
728
|
}
|
|
@@ -731,7 +748,7 @@ async function remoteReadFile(runner, p, path) {
|
|
|
731
748
|
}
|
|
732
749
|
const truncated = st.framed && st.size !== null && st.size > MAX_BYTES;
|
|
733
750
|
if (st.framed && !r.truncated) {
|
|
734
|
-
|
|
751
|
+
readCachePut(key, { content: decoded.text, binary: decoded.binary, truncated: truncated, size: st.size, mtime: st.mtime, epoch: epoch, at: Date.now() }, READ_CACHE_MAX);
|
|
735
752
|
}
|
|
736
753
|
return readResultOk(path, decoded.text, decoded.binary, truncated || !!r.truncated);
|
|
737
754
|
}
|
|
@@ -1306,9 +1323,10 @@ function apply(ctx, config) {
|
|
|
1306
1323
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
1307
1324
|
if (!args || !args.command) return { ok: false, error: "command 为必填项" };
|
|
1308
1325
|
// 走持久会话池(stderr 分离):首调用建立连接后,后续调用毫秒级返回。
|
|
1309
|
-
// 远程命令可能改任意文件(插件不可见)→
|
|
1326
|
+
// 远程命令可能改任意文件(插件不可见)→ 成功后整代失效读/列举缓存(findings §4.3a);
|
|
1327
|
+
// 失败的命令通常无副作用,不 bump 以免无谓打断缓存(部分副作用由 ≤5s TTL 兜底)。
|
|
1310
1328
|
const r = await runPooled(p, args.command, args.stdin, undefined, true);
|
|
1311
|
-
bumpCacheEpoch(p);
|
|
1329
|
+
if (r.ok) bumpCacheEpoch(p);
|
|
1312
1330
|
return r;
|
|
1313
1331
|
},
|
|
1314
1332
|
listDir: async (args) => {
|
|
@@ -2157,12 +2175,15 @@ function apply(ctx, config) {
|
|
|
2157
2175
|
}
|
|
2158
2176
|
|
|
2159
2177
|
/** 远端 git 执行(远程工作区):在 remoteDir 里跑同一 git 语义。 */
|
|
2178
|
+
const GIT_MUTATING_FIRST_TOKENS = new Set(["add", "reset", "commit", "checkout", "revert", "cherry-pick"]);
|
|
2160
2179
|
async function runGitRemote(profile, remoteDir, args) {
|
|
2161
2180
|
const quoted = args.map((a) => shellQuote(a)).join(" ");
|
|
2162
2181
|
const r = await runPooled(profile, "git -C " + shellQuotePath(remoteDir) + " --no-pager -c color.ui=false " + quoted, undefined, 8 * 1024 * 1024);
|
|
2163
2182
|
if (!r.ok) throw { code: "git-error", message: String(r.stdout || r.error || "").trim() || "remote git exited with " + r.exitCode };
|
|
2164
|
-
// commit/checkout/
|
|
2165
|
-
|
|
2183
|
+
// 仅变更类子命令(stage→add / unstage→reset / commit / checkout / discard→checkout /
|
|
2184
|
+
// revert / cherry-pick)会改工作区文件 → 整代失效读/列举缓存;
|
|
2185
|
+
// status/diff/log/branch/show 等只读命令不再打断缓存(review m4)。
|
|
2186
|
+
if (GIT_MUTATING_FIRST_TOKENS.has(String(args[0] || ""))) bumpCacheEpoch(profile);
|
|
2166
2187
|
return r.stdout;
|
|
2167
2188
|
}
|
|
2168
2189
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|