Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ view inside the editor.
- Android emulator frames are sourced from emulator gRPC; loopback browsers use raw RGBA over WebRTC, and non-loopback browsers use VideoToolbox-encoded H.264
- Full simulator control & inspection using private iOS accessibility APIs and Android UIAutomator - available using `simdeck` CLI
- Real-time screen `describe` command using accessibility view tree - available in token-efficient format for agents
- Simulator app performance gauges for CPU, memory, disk writes, network throughput, hang signals, and stack sampling
- CoreSimulator chrome asset rendering for device bezels
- NativeScript, React Native, Flutter, UIKit and SwiftUI runtime inspector plugins to debug app's view hierarchy live
- `simdeck/test` for fast JS-based app tests that can query accessibility state and drive simulator controls
Expand Down Expand Up @@ -159,6 +160,9 @@ simdeck rotate-left <udid>
simdeck rotate-right <udid>
simdeck chrome-profile <udid>
simdeck logs <udid> --seconds 30 --limit 200
simdeck processes <udid>
simdeck stats <udid> --watch
simdeck sample <udid> --seconds 3
```

`boot` uses SimDeck's private CoreSimulator boot path so it can start devices
Expand Down
47 changes: 47 additions & 0 deletions client/src/api/simulators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type {
ChromeDevToolsTargetDiscovery,
ChromeProfile,
InspectorRequestResponse,
SimulatorPerformanceResponse,
SimulatorLogsResponse,
SimulatorMetadata,
SimulatorProcessListResponse,
SimulatorStateResponse,
SimulatorsResponse,
StackSampleResponse,
WebKitTargetDiscovery,
} from "./types";

Expand Down Expand Up @@ -87,6 +90,50 @@ export async function fetchSimulatorLogs(
);
}

export async function fetchSimulatorProcesses(
udid: string,
options: RequestInit = {},
): Promise<SimulatorProcessListResponse> {
return apiRequest<SimulatorProcessListResponse>(
`/api/simulators/${encodeURIComponent(udid)}/processes`,
options,
);
}

export async function fetchSimulatorPerformance(
udid: string,
options: {
pid?: number | null;
windowMs?: number;
request?: RequestInit;
} = {},
): Promise<SimulatorPerformanceResponse> {
const params = new URLSearchParams();
if (options.pid != null) {
params.set("pid", String(options.pid));
}
if (options.windowMs != null) {
params.set("windowMs", String(options.windowMs));
}
const query = params.size > 0 ? `?${params}` : "";
return apiRequest<SimulatorPerformanceResponse>(
`/api/simulators/${encodeURIComponent(udid)}/performance${query}`,
options.request ?? {},
);
}

export async function sampleSimulatorProcess(
udid: string,
pid: number,
seconds = 3,
): Promise<StackSampleResponse> {
const params = new URLSearchParams({ seconds: String(seconds) });
return apiRequest<StackSampleResponse>(
`/api/simulators/${encodeURIComponent(udid)}/processes/${pid}/sample?${params}`,
{ method: "POST" },
);
}

export async function fetchWebKitTargets(
udid: string,
options: RequestInit = {},
Expand Down
81 changes: 81 additions & 0 deletions client/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,87 @@ export interface SimulatorLogsResponse {
entries: SimulatorLogEntry[];
}

export interface PerformanceProcess {
pid: number;
parentPid: number;
process: string;
role: string;
state: string;
appName?: string | null;
bundleIdentifier?: string | null;
command: string;
isForeground: boolean;
}

export interface PerformanceHangStatus {
state: string;
staleMs?: number | null;
reason: string;
}

export interface PerformanceSample {
pid: number;
timestampMs: number;
cpuPercent: number;
memoryResidentBytes?: number | null;
memoryFootprintBytes?: number | null;
memoryPeakFootprintBytes?: number | null;
diskReadBytes?: number | null;
diskWriteBytes?: number | null;
diskReadBytesPerSecond?: number | null;
diskWriteBytesPerSecond?: number | null;
networkReceivedBytes?: number | null;
networkSentBytes?: number | null;
networkReceivedBytesPerSecond?: number | null;
networkSentBytesPerSecond?: number | null;
networkConnectionCount?: number | null;
networkEstablishedConnectionCount?: number | null;
networkEndpoints: string[];
hang: PerformanceHangStatus;
}

export interface PerformanceEvent {
category: string;
level: string;
message: string;
pid: number | null;
process: string;
subsystem: string;
timestamp: string;
}

export interface SimulatorPerformanceResponse {
udid: string;
sampledAt: number;
selectedPid?: number | null;
foregroundProcess?: SimulatorForegroundApp | null;
processes: PerformanceProcess[];
current?: PerformanceSample | null;
history: PerformanceSample[];
events: PerformanceEvent[];
warnings: string[];
}

export interface SimulatorProcessListResponse {
udid: string;
foregroundProcess?: SimulatorForegroundApp | null;
processes: PerformanceProcess[];
}

export interface StackSampleReport {
pid: number;
seconds: number;
sampledAt: number;
report: string;
stderr: string;
truncated: boolean;
}

export interface StackSampleResponse {
udid: string;
sample: StackSampleReport;
}

export interface InspectorRequestResponse<T = unknown> {
inspector?: Record<string, unknown>;
result: T;
Expand Down
32 changes: 30 additions & 2 deletions client/src/features/accessibility/AccessibilityInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
UIKitScriptResult,
} from "../../api/types";
import { ConsolePanel } from "./ConsolePanel";
import { PerformancePanel } from "./PerformancePanel";
import {
ancestorAccessibilityIds,
accessibilityIdentifier,
Expand Down Expand Up @@ -39,7 +40,7 @@ interface AccessibilityInspectorProps {
visible: boolean;
}

type InspectorTab = "console" | "inspector";
type InspectorTab = "console" | "inspector" | "performance";

export function AccessibilityInspector({
availableSources,
Expand Down Expand Up @@ -245,13 +246,27 @@ export function AccessibilityInspector({
>
<ConsoleIcon />
</button>
<button
aria-label="Performance"
className={`tbtn icon-btn ${activeTab === "performance" ? "active" : ""}`}
onClick={() => setActiveTab("performance")}
title="Performance"
type="button"
>
<PerformanceIcon />
</button>
</div>
{activeTab === "console" ? (
<ConsolePanel
accessibilityRoots={roots}
selectedSimulator={selectedSimulator}
visible={visible && activeTab === "console"}
/>
) : activeTab === "performance" ? (
<PerformancePanel
selectedSimulator={selectedSimulator}
visible={visible && activeTab === "performance"}
/>
) : (
<div className="hierarchy-tree">
{sourceOptions.length > 0 ? (
Expand Down Expand Up @@ -457,6 +472,19 @@ function ConsoleIcon() {
);
}

function PerformanceIcon() {
return (
<svg fill="none" height="16" viewBox="0 0 16 16" width="16">
<path
d="M2.5 11.5h11M3.5 10V6.5M6.5 10V3.5M9.5 10V7.5M12.5 10V5"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="1.45"
/>
</svg>
);
}

function NodeDetails({
node,
selectedSimulator,
Expand Down Expand Up @@ -941,5 +969,5 @@ function readStoredTab(): InspectorTab {
return "inspector";
}
const tab = window.localStorage.getItem("xcw-hierarchy-active-tab");
return tab === "console" ? "console" : "inspector";
return tab === "console" || tab === "performance" ? tab : "inspector";
}
Loading
Loading