@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 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 schedule = function () {
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 () { timer = null; hideRemoteGearIcon(); }, 120);
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(schedule);
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
- lruPut(readCache, key, hit, READ_CACHE_MAX);
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
- lruPut(readCache, key, { content: rawOk.text, binary: rawOk.binary, truncated: truncated, size: st.size, mtime: st.mtime, epoch: epoch, at: Date.now() }, READ_CACHE_MAX);
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
- lruPut(readCache, key, { content: decoded.text, binary: decoded.binary, truncated: truncated, size: st.size, mtime: st.mtime, epoch: epoch, at: Date.now() }, READ_CACHE_MAX);
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
- // 远程命令可能改任意文件(插件不可见)→ 整代失效读/列举缓存(findings §4.3a)。
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/revert/cherry-pick 等会改工作区文件 整代失效读/列举缓存
2165
- bumpCacheEpoch(profile);
2183
+ // 仅变更类子命令(stage→add / unstage→reset / commit / checkout / discardcheckout /
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.0",
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",