perf: cut ControlApp re-renders and lazy-load VFX packs

Move audio scrub/volume and brush drafts off root React ticks, coalesce overlay layout IPC, cache machine fingerprint and asset URLs, share one scene overlay host, and stop idle Pixi ticker. Also harden console against EPIPE on window load failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-23 11:58:16 +08:00
parent 32a5479086
commit d9fbecf5a7
29 changed files with 1825 additions and 585 deletions
+4
View File
@@ -3,6 +3,10 @@ import path from 'node:path';
import { app, BrowserWindow, dialog, Menu, protocol } from 'electron';
import { installStdoutEpipeGuards } from './safeConsole';
installStdoutEpipeGuards();
import { openDialogFilterLabel } from '../shared/appBranding';
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
import {
+3 -2
View File
@@ -25,7 +25,8 @@ export function clearLegacyDeviceId(userData: string): void {
/**
* Идентификатор устройства для лицензии: отпечаток физической машины.
* Одинаков для всех пользователей ОС на одном ПК (Windows/macOS/Linux).
* `userData` — путь для дискового кэша fingerprint (без повторного reg/wmic).
*/
export function getOrCreateDeviceId(_userData?: string): string {
return resolveMachineFingerprint();
export function getOrCreateDeviceId(userData?: string): string {
return resolveMachineFingerprint(userData ? { userData } : {});
}
@@ -5,6 +5,7 @@ import path from 'node:path';
import test from 'node:test';
import {
clearMachineFingerprintMemoryCache,
hashMachineRawId,
machineWideIdPath,
parseMacIOPlatformUUID,
@@ -12,6 +13,7 @@ import {
parseWmicUuid,
resolveMachineFingerprint,
} from './machineFingerprint';
import { machineFingerprintCachePath } from './paths';
void test('hashMachineRawId: стабилен и не зависит от регистра GUID', () => {
const a = hashMachineRawId('win32', 'ABCDEF00-1111-2222-3333-444455556666');
@@ -46,6 +48,7 @@ void test('parseWmicUuid', () => {
});
void test('resolveMachineFingerprint: override через DND_LICENSE_DEVICE_ID', () => {
clearMachineFingerprintMemoryCache();
const id = resolveMachineFingerprint({
platform: 'linux',
env: { DND_LICENSE_DEVICE_ID: 'override-device-id-12345' },
@@ -54,6 +57,7 @@ void test('resolveMachineFingerprint: override через DND_LICENSE_DEVICE_ID'
});
void test('resolveMachineFingerprint: Windows MachineGuid → одинаковый hash', () => {
clearMachineFingerprintMemoryCache();
const exec = () =>
`
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
@@ -64,6 +68,7 @@ HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
env: {},
execFileSync: exec as never,
});
clearMachineFingerprintMemoryCache();
const b = resolveMachineFingerprint({
platform: 'win32',
env: {},
@@ -74,6 +79,7 @@ HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
});
void test('resolveMachineFingerprint: Linux /etc/machine-id', () => {
clearMachineFingerprintMemoryCache();
const id = resolveMachineFingerprint({
platform: 'linux',
env: {},
@@ -86,6 +92,7 @@ void test('resolveMachineFingerprint: Linux /etc/machine-id', () => {
});
void test('resolveMachineFingerprint: fallback в machine-wide путь', () => {
clearMachineFingerprintMemoryCache();
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'machine-fp-'));
const env = { PROGRAMDATA: tmp };
const p = machineWideIdPath('win32', env);
@@ -96,6 +103,7 @@ void test('resolveMachineFingerprint: fallback в machine-wide путь', () =>
throw new Error('no reg');
},
});
clearMachineFingerprintMemoryCache();
const id2 = resolveMachineFingerprint({
platform: 'win32',
env,
@@ -107,3 +115,37 @@ void test('resolveMachineFingerprint: fallback в machine-wide путь', () =>
assert.ok(fs.existsSync(p));
fs.rmSync(tmp, { recursive: true, force: true });
});
void test('resolveMachineFingerprint: disk cache — без повторного exec', () => {
clearMachineFingerprintMemoryCache();
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'machine-fp-cache-'));
const expected = hashMachineRawId('win32', 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890');
let execCalls = 0;
const exec = () => {
execCalls += 1;
return `
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography
MachineGuid REG_SZ A1B2C3D4-E5F6-7890-ABCD-EF1234567890
`;
};
const a = resolveMachineFingerprint({
platform: 'win32',
env: {},
userData: tmp,
execFileSync: exec as never,
});
assert.equal(a, expected);
assert.equal(execCalls, 1);
assert.ok(fs.existsSync(machineFingerprintCachePath(tmp)));
clearMachineFingerprintMemoryCache();
const b = resolveMachineFingerprint({
platform: 'win32',
env: {},
userData: tmp,
execFileSync: exec as never,
});
assert.equal(b, expected);
assert.equal(execCalls, 1, 'второй вызов читает disk cache, без reg/wmic');
fs.rmSync(tmp, { recursive: true, force: true });
});
+69 -2
View File
@@ -4,6 +4,8 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { machineFingerprintCachePath } from './paths';
type ExecFile = (
file: string,
args: readonly string[],
@@ -13,6 +15,8 @@ type ExecFile = (
export type MachineFingerprintDeps = {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
/** Electron userData — для дискового кэша hashed fingerprint. */
userData?: string;
execFileSync?: ExecFile;
readFileSync?: (p: string, encoding: 'utf8') => string;
existsSync?: (p: string) => boolean;
@@ -20,6 +24,18 @@ export type MachineFingerprintDeps = {
writeFileSync?: (p: string, data: string, opts?: { mode?: number }) => void;
};
/** Process-level кэш: повторные вызовы в том же процессе без I/O. */
let memoryFingerprint: string | null = null;
/** Только для тестов. */
export function clearMachineFingerprintMemoryCache(): void {
memoryFingerprint = null;
}
function isPlausiblyFingerprint(id: string): boolean {
return id.length >= 8 && id.length <= 128 && !/\s/.test(id);
}
const HASH_PREFIX = 'TTRPGPlayer.machine.v1\0';
/** Стабильный opaque id из сырого машинного идентификатора ОС. */
@@ -149,9 +165,38 @@ function readOrCreateMachineWideFallback(
}
}
function readDiskFingerprintCache(
cacheFile: string,
readFile: (p: string, encoding: 'utf8') => string,
existsSync: (p: string) => boolean,
): string | null {
try {
if (!existsSync(cacheFile)) return null;
const cached = readFile(cacheFile, 'utf8').trim();
return isPlausiblyFingerprint(cached) ? cached : null;
} catch {
return null;
}
}
function writeDiskFingerprintCache(
cacheFile: string,
fingerprint: string,
mkdirSync: (p: string, opts: { recursive: boolean }) => void,
writeFileSync: (p: string, data: string, opts?: { mode?: number }) => void,
): void {
try {
mkdirSync(path.dirname(cacheFile), { recursive: true });
writeFileSync(cacheFile, `${fingerprint}\n`, { mode: 0o644 });
} catch {
/* кэш необязателен */
}
}
/**
* Стабильный идентификатор физической машины (одинаковый для всех пользователей ОС на одном ПК).
* Источники: Windows MachineGuid, macOS IOPlatformUUID, Linux /etc/machine-id.
* Кэш: память процесса → userData/machine.fingerprint → sync probe ОС только при miss.
*/
export function resolveMachineFingerprint(deps: MachineFingerprintDeps = {}): string {
const platform = deps.platform ?? process.platform;
@@ -167,7 +212,24 @@ export function resolveMachineFingerprint(deps: MachineFingerprintDeps = {}): st
});
const override = env.DND_LICENSE_DEVICE_ID?.trim();
if (override && override.length >= 8) return override;
if (override && override.length >= 8) {
memoryFingerprint = override;
return override;
}
if (memoryFingerprint && isPlausiblyFingerprint(memoryFingerprint)) {
return memoryFingerprint;
}
const userData = deps.userData?.trim();
const cacheFile = userData ? machineFingerprintCachePath(userData) : null;
if (cacheFile) {
const fromDisk = readDiskFingerprintCache(cacheFile, readFile, existsSync);
if (fromDisk) {
memoryFingerprint = fromDisk;
return fromDisk;
}
}
let raw: string | null = null;
if (platform === 'win32') raw = readWindowsRawId(exec);
@@ -189,5 +251,10 @@ export function resolveMachineFingerprint(deps: MachineFingerprintDeps = {}): st
);
}
return hashMachineRawId(platform, raw);
const fingerprint = hashMachineRawId(platform, raw);
memoryFingerprint = fingerprint;
if (cacheFile) {
writeDiskFingerprintCache(cacheFile, fingerprint, mkdirSync, writeFileSync);
}
return fingerprint;
}
+5
View File
@@ -14,6 +14,11 @@ export function deviceIdPath(userData: string): string {
return path.join(userData, 'device.id');
}
/** Кэш hashed machine fingerprint — без повторного reg/wmic/ioreg на каждом старте. */
export function machineFingerprintCachePath(userData: string): string {
return path.join(userData, 'machine.fingerprint');
}
export function preferencesPath(userData: string): string {
return path.join(userData, 'preferences.json');
}
+16
View File
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
void test('main: EPIPE guards ставятся до app.whenReady', () => {
const index = fs.readFileSync(path.join(here, 'index.ts'), 'utf8');
const safe = fs.readFileSync(path.join(here, 'safeConsole.ts'), 'utf8');
assert.ok(index.includes('installStdoutEpipeGuards'));
assert.ok(index.indexOf('installStdoutEpipeGuards()') < index.indexOf('app.requestSingleInstanceLock'));
assert.ok(safe.includes('EPIPE'));
assert.ok(safe.includes('safeConsoleError'));
});
+26
View File
@@ -0,0 +1,26 @@
/**
* В Electron (особенно после рестарта в dev) stdout/stderr часто уже закрыты.
* Обычный `console.error` тогда даёт EPIPE и валит main process диалогом Uncaught Exception.
*/
function isBrokenPipe(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException | undefined)?.code;
return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED';
}
export function installStdoutEpipeGuards(): void {
for (const stream of [process.stdout, process.stderr]) {
stream?.on('error', (err: NodeJS.ErrnoException) => {
if (isBrokenPipe(err)) return;
});
}
}
export function safeConsoleError(...args: unknown[]): void {
try {
console.error(...args);
} catch (err) {
if (isBrokenPipe(err)) return;
throw err;
}
}
@@ -73,3 +73,10 @@ void test('createWindows: показ окна — не только ready-to-sho
assert.ok(src.includes('ensureWindowBecomesVisible'));
assert.ok(src.includes('did-finish-load'));
});
void test('createWindows: логи окон не валят main через EPIPE', () => {
const src = readCreateWindows();
assert.ok(src.includes('safeConsoleError'));
assert.ok(src.includes('errorCode === -3'));
assert.ok(src.includes('isMainFrame'));
});
+9 -4
View File
@@ -5,6 +5,8 @@ import { app, BrowserWindow, screen } from 'electron';
import { windowChromeTitle } from '../../shared/appBranding';
import { ipcChannels } from '../../shared/ipc/contracts';
import { safeConsoleError } from '../safeConsole';
import { getBootSplashWindow } from './bootWindow';
import { loadBrandingWindowIcon } from './brandingIcon';
@@ -245,13 +247,16 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
}
win.webContents.on('preload-error', (_event, preloadPath, error) => {
console.error(`[preload-error] ${preloadPath}:`, error);
safeConsoleError(`[preload-error] ${preloadPath}:`, error);
});
win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
console.error(`[did-fail-load] ${String(errorCode)} ${errorDescription} ${validatedURL}`);
win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
// -3 ERR_ABORTED: частый артефакт при navigate/maximize/закрытии — не шумим.
if (errorCode === -3) return;
if (!isMainFrame) return;
safeConsoleError(`[did-fail-load] ${String(errorCode)} ${errorDescription} ${validatedURL}`);
});
win.webContents.on('render-process-gone', (_event, details) => {
console.error('[render-process-gone]', details.reason, details.exitCode);
safeConsoleError('[render-process-gone]', details.reason, details.exitCode);
});
if (!deferEditor) {