perf: optimize app startup and background lifecycles

This commit is contained in:
inman
2026-08-19 00:56:15 +08:00
parent 31332f6d76
commit 927e13349b
121 changed files with 4359 additions and 1400 deletions

View File

@@ -122,6 +122,20 @@ export async function presentAgentBrowser(input: {
return requireSuccess(response).browser;
}
export async function setAgentBrowserDiagnostics(input: {
projectPath: string;
enabled: boolean;
}): Promise<AgentBrowserSnapshot> {
const response = await hostApiFetch<BrowserEnvelope>(
'/api/agent-browser/diagnostics',
jsonBody({
project_path: input.projectPath,
enabled: input.enabled,
}),
);
return requireSuccess(response).browser;
}
export async function navigateAgentBrowser(input: {
projectPath: string;
action: AgentBrowserNavigateAction;

View File

@@ -0,0 +1,34 @@
import type { AiHardwareOverview } from '@/lib/ai-hardware';
export const AI_HARDWARE_NAVIGATION_EVENT = 'makelore:ai-hardware-navigation';
export const AI_HARDWARE_OVERVIEW_EVENT = 'makelore:ai-hardware-overview';
export type AiHardwareNavigationDetail = {
agentId?: string | null;
openCreate?: boolean;
};
export function readAiHardwareAgentId(search?: string): string | null {
if (typeof window === 'undefined' && search === undefined) return null;
const value = new URLSearchParams(search ?? window.location.search).get('agent')?.trim();
return value || null;
}
export function syncAiHardwareAgentUrl(agentId: string | null): void {
if (typeof window === 'undefined') return;
const url = new URL(window.location.href);
if (agentId) url.searchParams.set('agent', agentId);
else url.searchParams.delete('agent');
url.searchParams.delete('create');
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
}
export function announceAiHardwareNavigation(detail: AiHardwareNavigationDetail): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent<AiHardwareNavigationDetail>(AI_HARDWARE_NAVIGATION_EVENT, { detail }));
}
export function announceAiHardwareOverview(overview: AiHardwareOverview): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent<AiHardwareOverview>(AI_HARDWARE_OVERVIEW_EVENT, { detail: overview }));
}

View File

@@ -28,6 +28,23 @@ export async function ensureHostApiToken(): Promise<string> {
return token;
}
export type DesktopActivityModule = 'programming' | 'painting' | 'learning' | 'robot' | 'other';
export function reportDesktopActivity(input: {
visible: boolean;
module: DesktopActivityModule | null;
}): Promise<unknown> {
return invokeIpc('lifecycle:activity', input);
}
export function setDesktopBackgroundLease(input: {
id: string;
kind: string;
active: boolean;
}): Promise<unknown> {
return invokeIpc('lifecycle:lease', input);
}
async function getHostApiBaseUrl(): Promise<string> {
if (hostApiBaseResolved) return cachedHostApiBase;
try {
@@ -63,6 +80,7 @@ type HostApiProxyData = {
ok?: boolean;
json?: unknown;
text?: string;
transport?: 'dispatcher' | 'loopback';
};
function headersToRecord(headers?: HeadersInit): Record<string, string> {
@@ -72,6 +90,20 @@ function headersToRecord(headers?: HeadersInit): Record<string, string> {
return { ...headers };
}
function telemetryRoute(path: string): string {
const pathname = path.split('?', 1)[0] || '/';
return pathname
.split('/')
.map((segment, index) => {
if (!segment) return '';
// Keep diagnostics aggregate-only: IDs, slugs and arbitrary path
// segments must never become local telemetry or console output.
if (index <= 2 && /^[a-z0-9._-]{1,32}$/i.test(segment)) return segment;
return ':id';
})
.join('/') || '/';
}
async function parseResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
@@ -133,9 +165,9 @@ function parseUnifiedProxyResponse<T>(
}
trackUiEvent('hostapi.fetch', {
path,
route: telemetryRoute(path),
method,
source: 'ipc-proxy',
source: data.transport === 'dispatcher' ? 'ipc-dispatcher' : 'ipc-proxy',
durationMs: Date.now() - startedAt,
status: data.status ?? 200,
});
@@ -165,7 +197,7 @@ function parseLegacyProxyResponse<T>(
}
trackUiEvent('hostapi.fetch', {
path,
route: telemetryRoute(path),
method,
source: 'ipc-proxy-legacy',
durationMs: Date.now() - startedAt,
@@ -215,7 +247,7 @@ export async function hostApiFetch<T>(path: string, init?: RequestInit): Promise
const normalized = normalizeAppError(error, { source: 'ipc-proxy', path, method });
const message = normalized.message;
trackUiEvent('hostapi.fetch_error', {
path,
route: telemetryRoute(path),
method,
source: 'ipc-proxy',
durationMs: Date.now() - startedAt,
@@ -227,7 +259,7 @@ export async function hostApiFetch<T>(path: string, init?: RequestInit): Promise
}
if (!allowLocalhostFallback()) {
trackUiEvent('hostapi.fetch_error', {
path,
route: telemetryRoute(path),
method,
source: 'ipc-proxy',
durationMs: Date.now() - startedAt,
@@ -252,7 +284,7 @@ export async function hostApiFetch<T>(path: string, init?: RequestInit): Promise
},
});
trackUiEvent('hostapi.fetch', {
path,
route: telemetryRoute(path),
method,
source: 'browser-fallback',
durationMs: Date.now() - startedAt,

View File

@@ -11,6 +11,9 @@ const HOST_EVENT_TO_IPC_CHANNEL: Record<string, string> = {
'agent-browser:show': 'agent-browser:show',
'agent-browser:state': 'agent-browser:state',
'auth:session-changed': 'auth:session-changed',
'lifecycle:pause': 'lifecycle:pause',
'lifecycle:sleep': 'lifecycle:sleep',
'release-job:status': 'release-job:status',
};
function getEventSource(): EventSource {

View File

@@ -0,0 +1,91 @@
import { invokeIpc } from '@/lib/api-client';
type LongTaskAggregate = {
count: number;
totalDurationMs: number;
maxDurationMs: number;
};
type MainPerformanceSnapshot = {
capturedAt?: number;
eventLoopDelayMs?: number;
processes?: Array<unknown>;
windows?: number;
webContents?: number;
gpu?: Record<string, string>;
background?: {
activity?: { visible?: boolean; module?: string | null };
leaseCount?: number;
};
};
/**
* Installs development-only, aggregate renderer diagnostics. No URLs, page
* content, prompts or file names are retained or sent to Main. Production
* builds return a no-op cleanup so this cannot become a hidden polling task.
*/
export function installRendererPerformanceDiagnostics(): () => void {
if (typeof window === 'undefined' || window.electron?.isDev !== true) {
return () => undefined;
}
const longTasks: LongTaskAggregate = {
count: 0,
totalDurationMs: 0,
maxDurationMs: 0,
};
let firstPaintMs: number | null = null;
let disposed = false;
let paintFrame = 0;
const observer = typeof PerformanceObserver === 'undefined'
? null
: new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const duration = Math.round(entry.duration * 100) / 100;
longTasks.count += 1;
longTasks.totalDurationMs += duration;
longTasks.maxDurationMs = Math.max(longTasks.maxDurationMs, duration);
}
});
try {
observer?.observe({ type: 'longtask', buffered: true } as PerformanceObserverInit);
} catch {
// Chromium versions without the Long Tasks entry type are supported.
}
paintFrame = window.requestAnimationFrame(() => {
firstPaintMs = Math.round(performance.now() * 100) / 100;
});
const report = () => {
if (disposed) return;
const renderer = {
firstPaintMs,
longTasks: {
count: longTasks.count,
totalDurationMs: Math.round(longTasks.totalDurationMs * 100) / 100,
maxDurationMs: Math.round(longTasks.maxDurationMs * 100) / 100,
},
};
void invokeIpc<MainPerformanceSnapshot>('app:performance')
.then((main) => {
if (!disposed) {
console.debug('[performance]', { renderer, main });
}
})
.catch(() => undefined);
};
// A low-frequency sample is enough for the development panel and avoids
// adding another active request loop to normal renderer operation.
const timer = window.setInterval(report, 30_000);
return () => {
disposed = true;
window.cancelAnimationFrame(paintFrame);
window.clearInterval(timer);
observer?.disconnect();
};
}

View File

@@ -28,6 +28,10 @@ export function trackUiEvent(event: string, payload: TelemetryPayload = {}): voi
const normalizedPayload = {
...payload,
};
const isDevelopment = typeof window !== 'undefined' && Boolean(window.electron?.isDev);
const isHighFrequencyHostRequest = event === 'hostapi.fetch';
const shouldRecordHistory = isDevelopment || !isHighFrequencyHostRequest;
if (!shouldRecordHistory) return;
const ts = new Date().toISOString();
const entry: UiTelemetryEntry = {
id: nextEntryId,
@@ -51,7 +55,9 @@ export function trackUiEvent(event: string, payload: TelemetryPayload = {}): voi
};
// Local-only telemetry for UX diagnostics.
console.info(`[ui-metric] ${event} ${safeStringify(logPayload)}`);
if (isDevelopment || event.endsWith('_error')) {
console.info(`[ui-metric] ${event} ${safeStringify(logPayload)}`);
}
}
export function getUiCounter(event: string): number {

View File

@@ -158,6 +158,20 @@ export type WorksProjectSourcePublishResult = {
bindingWarning?: WorksSubmissionBindingWarning;
};
export type ReleaseJobStatus = {
jobId: string;
projectId: string;
state: 'queued' | 'running' | 'complete' | 'failed' | 'cancelled';
phase: 'queued' | 'packaging' | 'installing' | 'building' | 'archiving' | 'complete';
percent: number;
errorCode?: string;
errorMessage?: string;
result?: {
package: WorksStaticPackageSummary;
contract: Record<string, unknown>;
};
};
export type WorksSpeechTranscription = {
text: string;
model: string;
@@ -464,6 +478,38 @@ export async function publishWorksProjectSource(
};
}
export async function startWorksReleaseJob(projectId: string): Promise<ReleaseJobStatus> {
const response = await hostApiFetch<{ success: boolean; job?: ReleaseJobStatus; error?: string; code?: string }>(
'/api/works/projects/release-jobs',
{ method: 'POST', body: JSON.stringify({ projectId }) },
);
if (!response.success || !response.job) {
throw new WorksSquareApiError(response.error || 'Failed to start release build', undefined, response.code);
}
return response.job;
}
export async function getWorksReleaseJob(jobId: string): Promise<ReleaseJobStatus> {
const response = await hostApiFetch<{ success: boolean; job?: ReleaseJobStatus; error?: string; code?: string }>(
`/api/works/projects/release-jobs/${encodeURIComponent(jobId)}`,
);
if (!response.success || !response.job) {
throw new WorksSquareApiError(response.error || 'Failed to read release build status', undefined, response.code);
}
return response.job;
}
export async function cancelWorksReleaseJob(jobId: string): Promise<ReleaseJobStatus> {
const response = await hostApiFetch<{ success: boolean; job?: ReleaseJobStatus; error?: string; code?: string }>(
`/api/works/projects/release-jobs/${encodeURIComponent(jobId)}/cancel`,
{ method: 'POST', body: JSON.stringify({}) },
);
if (!response.success || !response.job) {
throw new WorksSquareApiError(response.error || 'Failed to cancel release build', undefined, response.code);
}
return response.job;
}
export async function fetchWorksProjectVersions(
accessToken: string,
appId: string,