mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-26 02:53:21 +00:00
Add Issue 1189 Fast Setup diagnostic builds
This commit is contained in:
1 parent
2c2b9c90e4
commit
b533bb2e28
9 files changed
+347
-7
No files matched your search
@@ -0,0 +1,178 @@
|
||||
export type Issue1189Variant = "normal" | "serial";
|
||||
export type Issue1189Phase = "boot" | "fetch" | "reflect" | "finalise" | "complete" | "paused";
|
||||
|
||||
type DiagnosticEvent = { at: number; phase: Issue1189Phase; note?: string };
|
||||
type DiagnosticRun = {
|
||||
id: string;
|
||||
variant: Issue1189Variant;
|
||||
startedAt: number;
|
||||
updatedAt: number;
|
||||
phase: Issue1189Phase;
|
||||
status: "active" | "complete" | "paused";
|
||||
restartRequested: boolean;
|
||||
activeFiles: number;
|
||||
peakActiveFiles: number;
|
||||
activeBytes: number;
|
||||
peakActiveBytes: number;
|
||||
processedFiles: number;
|
||||
largestFile: number;
|
||||
peakTimerDelayMs: number;
|
||||
events: DiagnosticEvent[];
|
||||
};
|
||||
|
||||
type DiagnosticRecord = {
|
||||
schema: 1;
|
||||
current: DiagnosticRun;
|
||||
previous?: DiagnosticRun;
|
||||
resumeAllowed: boolean;
|
||||
};
|
||||
|
||||
export interface Issue1189DiagnosticStorage {
|
||||
getDeviceLocalConfig(key: string): string | null;
|
||||
setDeviceLocalConfig(key: string, value: string): void;
|
||||
}
|
||||
|
||||
const INTERVAL_MS = 1000;
|
||||
const HISTORY_LIMIT = 24;
|
||||
|
||||
function parseRecord(raw: string | null): DiagnosticRecord | undefined {
|
||||
if (!raw) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as DiagnosticRecord;
|
||||
if (parsed?.schema === 1 && parsed.current?.events instanceof Array) return parsed;
|
||||
} catch {
|
||||
// A damaged optional diagnostic record must never prevent plug-in startup.
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createIssue1189Diagnostics(
|
||||
storage: Issue1189DiagnosticStorage,
|
||||
vaultName: string,
|
||||
variant: Issue1189Variant,
|
||||
now: () => number = Date.now
|
||||
) {
|
||||
const key = `issue-1189-diagnostics-${vaultName}`;
|
||||
let raw: string | null = null;
|
||||
try {
|
||||
raw = storage.getDeviceLocalConfig(key);
|
||||
} catch {
|
||||
// Diagnosis is optional when the browser's local store is unavailable.
|
||||
}
|
||||
const saved = parseRecord(raw);
|
||||
const previous =
|
||||
(saved?.current.phase === "boot" || saved?.current.phase === "paused") && saved.previous
|
||||
? saved.previous
|
||||
: saved?.current;
|
||||
const resumeAllowed = saved?.resumeAllowed === true;
|
||||
const startedAt = now();
|
||||
const current: DiagnosticRun = {
|
||||
id: `${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
variant,
|
||||
startedAt,
|
||||
updatedAt: startedAt,
|
||||
phase: "boot",
|
||||
status: "active",
|
||||
restartRequested: false,
|
||||
activeFiles: 0,
|
||||
peakActiveFiles: 0,
|
||||
activeBytes: 0,
|
||||
peakActiveBytes: 0,
|
||||
processedFiles: 0,
|
||||
largestFile: 0,
|
||||
peakTimerDelayMs: 0,
|
||||
events: [{ at: startedAt, phase: "boot" }],
|
||||
};
|
||||
const record: DiagnosticRecord = { schema: 1, current, previous, resumeAllowed: false };
|
||||
let lastPersistedAt = 0;
|
||||
let lastStorageError = "";
|
||||
const persist = () => {
|
||||
current.updatedAt = now();
|
||||
try {
|
||||
storage.setDeviceLocalConfig(key, JSON.stringify(record));
|
||||
lastPersistedAt = current.updatedAt;
|
||||
lastStorageError = "";
|
||||
} catch (error) {
|
||||
lastStorageError = error instanceof Error ? error.name : String(error);
|
||||
}
|
||||
};
|
||||
const event = (phase: Issue1189Phase, note?: string) => {
|
||||
current.phase = phase;
|
||||
current.events.push({ at: now(), phase, note });
|
||||
if (current.events.length > HISTORY_LIMIT) current.events.shift();
|
||||
persist();
|
||||
};
|
||||
persist();
|
||||
|
||||
return {
|
||||
variant,
|
||||
shouldPauseFetch: () =>
|
||||
!resumeAllowed &&
|
||||
previous?.status === "active" &&
|
||||
!previous.restartRequested &&
|
||||
(previous.phase === "fetch" || previous.phase === "reflect" || previous.phase === "finalise"),
|
||||
pause() {
|
||||
current.status = "paused";
|
||||
event("paused", "An earlier Fast Setup attempt did not finish.");
|
||||
},
|
||||
allowResume() {
|
||||
record.resumeAllowed = true;
|
||||
persist();
|
||||
},
|
||||
markPhase: event,
|
||||
markRestartRequested() {
|
||||
current.restartRequested = true;
|
||||
persist();
|
||||
},
|
||||
markComplete() {
|
||||
current.status = "complete";
|
||||
event("complete");
|
||||
},
|
||||
onFileActivity({ phase, size }: { phase: "start" | "finish"; size: number }) {
|
||||
const firstFile = current.activeFiles === 0 && current.processedFiles === 0;
|
||||
const previousPeakFiles = current.peakActiveFiles;
|
||||
const previousLargestFile = current.largestFile;
|
||||
if (phase === "start") {
|
||||
current.activeFiles++;
|
||||
current.activeBytes += size;
|
||||
current.peakActiveFiles = Math.max(current.peakActiveFiles, current.activeFiles);
|
||||
current.peakActiveBytes = Math.max(current.peakActiveBytes, current.activeBytes);
|
||||
current.largestFile = Math.max(current.largestFile, size);
|
||||
} else {
|
||||
current.activeFiles = Math.max(0, current.activeFiles - 1);
|
||||
current.activeBytes = Math.max(0, current.activeBytes - size);
|
||||
current.processedFiles++;
|
||||
}
|
||||
if (
|
||||
(phase === "start" &&
|
||||
(firstFile || current.peakActiveFiles > previousPeakFiles || (size >= 4_000_000 && size > previousLargestFile))) ||
|
||||
now() - lastPersistedAt >= INTERVAL_MS
|
||||
) {
|
||||
persist();
|
||||
}
|
||||
},
|
||||
startTimer(setTimer: (callback: () => void, delay: number) => number, clearTimer: (id: number) => void) {
|
||||
let expected = now() + INTERVAL_MS;
|
||||
let stopped = false;
|
||||
let timerId = 0;
|
||||
const tick = () => {
|
||||
if (stopped) return;
|
||||
const actual = now();
|
||||
current.peakTimerDelayMs = Math.max(current.peakTimerDelayMs, Math.max(0, actual - expected));
|
||||
if (actual - lastPersistedAt >= INTERVAL_MS) persist();
|
||||
expected = actual + INTERVAL_MS;
|
||||
timerId = setTimer(tick, INTERVAL_MS);
|
||||
};
|
||||
timerId = setTimer(tick, INTERVAL_MS);
|
||||
return () => {
|
||||
stopped = true;
|
||||
clearTimer(timerId);
|
||||
};
|
||||
},
|
||||
report() {
|
||||
return JSON.stringify({ ...record, storageError: lastStorageError || undefined }, null, 2);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type Issue1189Diagnostics = ReturnType<typeof createIssue1189Diagnostics>;
|
||||
Reference in new issue
Block a user