perf(layout): track panel resizing at a stepped cadence
This commit is contained in:
1 parent
a78fc48a7b
commit
8eba12ea2f
17 files changed
+705
-34
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const fs = {
|
||||
exists: vi.fn(),
|
||||
readTextFile: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
|
||||
vi.mock("@tauri-apps/api/path", () => ({
|
||||
homeDir: () => Promise.resolve("/home/tester"),
|
||||
join: (_home: string, ...parts: string[]) => Promise.resolve(["/home/tester", ...parts].join("/")),
|
||||
}));
|
||||
vi.mock("@tauri-apps/plugin-fs", () => ({
|
||||
exists: (path: string) => fs.exists(path),
|
||||
readTextFile: (path: string) => fs.readTextFile(path),
|
||||
}));
|
||||
|
||||
async function loadFresh() {
|
||||
vi.resetModules();
|
||||
const module = await import("../uiTuning");
|
||||
await module.loadUiTuning();
|
||||
return module;
|
||||
}
|
||||
|
||||
describe("uiTuning", () => {
|
||||
beforeEach(() => {
|
||||
fs.exists.mockReset();
|
||||
fs.readTextFile.mockReset();
|
||||
});
|
||||
|
||||
it("uses the defaults when ~/.dbx/ui-tuning.json is absent", async () => {
|
||||
fs.exists.mockResolvedValue(false);
|
||||
const { uiTuning, DEFAULT_UI_TUNING } = await loadFresh();
|
||||
expect(uiTuning.value.panelResizeTrackEveryFrames).toBe(DEFAULT_UI_TUNING.panelResizeTrackEveryFrames);
|
||||
});
|
||||
|
||||
it("clamps out-of-range values from the tuning file", async () => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs.readTextFile.mockResolvedValue(JSON.stringify({ panelResizeTrackEveryFrames: 99 }));
|
||||
const { uiTuning } = await loadFresh();
|
||||
expect(uiTuning.value.panelResizeTrackEveryFrames).toBe(10);
|
||||
});
|
||||
|
||||
it("falls back to defaults on malformed json", async () => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs.readTextFile.mockResolvedValue("{ not json");
|
||||
const { uiTuning, DEFAULT_UI_TUNING } = await loadFresh();
|
||||
expect(uiTuning.value.panelResizeTrackEveryFrames).toBe(DEFAULT_UI_TUNING.panelResizeTrackEveryFrames);
|
||||
});
|
||||
|
||||
it("accepts a valid in-range value", async () => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs.readTextFile.mockResolvedValue(JSON.stringify({ panelResizeTrackEveryFrames: 2 }));
|
||||
const { uiTuning } = await loadFresh();
|
||||
expect(uiTuning.value.panelResizeTrackEveryFrames).toBe(2);
|
||||
expect(fs.exists).toHaveBeenCalledWith("/home/tester/.dbx/ui-tuning.json");
|
||||
expect(fs.readTextFile).toHaveBeenCalledWith("/home/tester/.dbx/ui-tuning.json");
|
||||
});
|
||||
|
||||
it.each(["exists", "readTextFile"] as const)("keeps defaults when %s access is denied", async (command) => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs[command].mockRejectedValue(new Error("path not allowed"));
|
||||
const { uiTuning, DEFAULT_UI_TUNING, loadUiTuning } = await loadFresh();
|
||||
expect(uiTuning.value).toEqual(DEFAULT_UI_TUNING);
|
||||
await loadUiTuning();
|
||||
expect(fs.exists).toHaveBeenCalledTimes(1);
|
||||
expect(fs.readTextFile).toHaveBeenCalledTimes(command === "exists" ? 0 : 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
/**
|
||||
* Global "a panel divider is being dragged" state.
|
||||
*
|
||||
* Resizing side panels or splitter panes changes the width of the CodeMirror
|
||||
* editor and the DataGrid on every frame. Both subtrees react to that with
|
||||
* expensive per-frame work (CodeMirror's requestMeasure walking every visible
|
||||
* line, ResizeObserver chains reading clientWidth/scrollWidth, which forces
|
||||
* synchronous layout for the whole document). Heavy components subscribe to
|
||||
* this flag to freeze measurement while a drag is in flight and re-sync once
|
||||
* it finishes, turning hundreds of forced reflows into a single relayout.
|
||||
*/
|
||||
const isPanelResizing = ref(false);
|
||||
const pendingAfterResize = new Set<() => void>();
|
||||
|
||||
function setPanelResizingResyncClass(resizing: boolean) {
|
||||
if (typeof document === "undefined") return;
|
||||
document.body.classList.toggle("dbx-is-resizing", resizing);
|
||||
}
|
||||
|
||||
/** Marks a drag as in flight. Idempotent: safe to call on every move event. */
|
||||
export function beginPanelResize() {
|
||||
if (isPanelResizing.value) return;
|
||||
isPanelResizing.value = true;
|
||||
setPanelResizingResyncClass(true);
|
||||
}
|
||||
|
||||
/** Ends the current drag (if any) and flushes registered re-sync callbacks. */
|
||||
export function endPanelResize() {
|
||||
if (!isPanelResizing.value) return;
|
||||
isPanelResizing.value = false;
|
||||
setPanelResizingResyncClass(false);
|
||||
const callbacks = [...pendingAfterResize];
|
||||
pendingAfterResize.clear();
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
callback();
|
||||
} catch {
|
||||
// A failed re-sync must not block the other subscribers.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a measurement callback to run when the in-flight drag ends.
|
||||
* Returns whether the caller should skip its own work right now — i.e.
|
||||
* `true` means "a drag is active, your callback was queued".
|
||||
*/
|
||||
export function deferUntilPanelResizeEnd(callback: () => void): boolean {
|
||||
if (!isPanelResizing.value) return false;
|
||||
pendingAfterResize.add(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
export { isPanelResizing };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readonly, ref } from "vue";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
|
||||
/**
|
||||
* Runtime UI tuning knobs, loaded once from `~/.dbx/ui-tuning.json` at
|
||||
* startup (desktop runtime only; web builds keep the defaults).
|
||||
*
|
||||
* These are power-user performance dials, deliberately not part of the
|
||||
* settings UI: edit the JSON file and restart the app to apply.
|
||||
*/
|
||||
export interface UiTuning {
|
||||
/**
|
||||
* While a panel divider is dragged, expensive subtrees (CodeMirror editor
|
||||
* box, DataGrid canvas) are pinned to an explicit pixel size and only
|
||||
* re-measured every N animation frames. 1 = update on every frame (full
|
||||
* tracking), 5 ≈ 12fps stepping, 10 ≈ 6fps. Clamped to 1..10.
|
||||
*/
|
||||
panelResizeTrackEveryFrames: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_UI_TUNING: UiTuning = {
|
||||
panelResizeTrackEveryFrames: 5,
|
||||
};
|
||||
|
||||
const MAX_TRACK_EVERY_FRAMES = 10;
|
||||
|
||||
const state = ref<UiTuning>({ ...DEFAULT_UI_TUNING });
|
||||
export const uiTuning = readonly(state);
|
||||
|
||||
function sanitizeFrames(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return DEFAULT_UI_TUNING.panelResizeTrackEveryFrames;
|
||||
return Math.min(MAX_TRACK_EVERY_FRAMES, Math.max(1, Math.round(parsed)));
|
||||
}
|
||||
|
||||
let loading: Promise<void> | null = null;
|
||||
|
||||
/** Reads `~/.dbx/ui-tuning.json` once; safe to call repeatedly. */
|
||||
export function loadUiTuning(): Promise<void> {
|
||||
loading ??= (async () => {
|
||||
if (!isTauriRuntime()) return;
|
||||
try {
|
||||
const { homeDir, join } = await import("@tauri-apps/api/path");
|
||||
const { exists, readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const file = await join(await homeDir(), ".dbx", "ui-tuning.json");
|
||||
if (!(await exists(file))) return;
|
||||
const parsed = JSON.parse(await readTextFile(file)) as Partial<UiTuning>;
|
||||
state.value = {
|
||||
panelResizeTrackEveryFrames: sanitizeFrames(parsed.panelResizeTrackEveryFrames),
|
||||
};
|
||||
console.info("[ui-tuning] loaded", file, state.value);
|
||||
} catch (error) {
|
||||
console.info("[ui-tuning] using defaults (could not read ~/.dbx/ui-tuning.json)", error);
|
||||
}
|
||||
})();
|
||||
return loading;
|
||||
}
|
||||
Reference in new issue
Block a user