mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 17:22:47 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2eb85340 |
@@ -0,0 +1,55 @@
|
||||
# Android input delay diagnostic
|
||||
|
||||
Diagnostic evidence for https://github.com/getpaseo/paseo/issues/4408, not a fix or a CI test.
|
||||
|
||||
Observed with the published Android 0.11.4 APK on an Android 16 emulator and a main
|
||||
daemon at 7c7b6555f17a88cffddf484129cd04ad79704f59. The fixture has 400 active
|
||||
workspaces sharing one checkout, plus a synthetic 110-turn conversation and a
|
||||
five-minute mock-provider stream. Clean/dirty transitions emitted 400 workspace
|
||||
updates each. The JavaScript thread reached 96–100% CPU, and tapping Open menu
|
||||
did not expose Close sidebar within five seconds. The same process stayed alive.
|
||||
|
||||
This combines the many-workspace and streaming reports. It does not isolate which
|
||||
work causes the delay, reproduce an allocation crash, or establish that the
|
||||
contributor's byte-counting optimization fixes it. An emulator is not the affected
|
||||
physical phone. No forced memory pressure, forced garbage collection, or fake
|
||||
client responses are used. The daemon's existing mock provider supplies synthetic
|
||||
agent output through the normal transport.
|
||||
|
||||
## Run
|
||||
|
||||
Use a disposable emulator with the published app installed and agent-device
|
||||
0.20.3 or later. Preserve its previous state before changing its app installation.
|
||||
Build the repository's server dependencies first. All commands below run from
|
||||
the repository root in a disposable worktree.
|
||||
|
||||
1. Start `npx tsx packages/app/e2e/mobile/workspace-update-freeze/fixture.ts`.
|
||||
It writes an isolated home under `.dev/workspace-update-freeze`, starts an
|
||||
isolated daemon, and exposes it on local TCP port 45151. Do not use an existing
|
||||
daemon or point this script at real data.
|
||||
2. Forward that port to the emulator, open the app with an agent-device session,
|
||||
and add a direct connection to that isolated daemon. Open the sidebar and
|
||||
verify that the Android reproduction project and workspace rows are visible.
|
||||
3. Start `npx tsx packages/app/e2e/mobile/workspace-update-freeze/stream.ts`.
|
||||
Wait for `HISTORY_READY`. The script writes the agent ID to
|
||||
`.dev/workspace-update-freeze/history-ready` and starts the five-minute stream.
|
||||
4. Open `paseo://h/diag515a/agent/<agent-id>` with agent-device, using the generated
|
||||
ID. Dismiss the notification prompt if present. Verify that the agent's text
|
||||
and Stop agent button are visible, then scroll the conversation.
|
||||
5. Start `node packages/app/e2e/mobile/workspace-update-freeze/toggle.mjs` in a
|
||||
separate terminal. It toggles the fixture's tracked file every three seconds
|
||||
for 90 seconds. `events.json` records workspace updates observed by an actual
|
||||
subscribed daemon client; initial subscription updates are included.
|
||||
6. While the toggles and stream are active, run:
|
||||
|
||||
```sh
|
||||
node packages/app/e2e/mobile/workspace-update-freeze/android.mjs <device-session>
|
||||
```
|
||||
|
||||
It taps Open menu and requires Close sidebar within five seconds. The observed
|
||||
failure was a timeout for Close sidebar while Open menu remained visible.
|
||||
This asserts an input delay, not an application crash or its root cause.
|
||||
|
||||
Stop the fixture with SIGTERM to close its client, proxy, and isolated daemon.
|
||||
Close the agent-device session and restore the emulator's prior state afterward.
|
||||
Do not interpret a passing run as disproving the physical-device reports.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const session = process.argv[2];
|
||||
if (!session) throw new Error("Pass the agent-device session connected to the Android fixture");
|
||||
|
||||
function device(...args) {
|
||||
return execFileSync("agent-device", [...args, "--session", session], {
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
device("wait", 'label="Open menu"', "5000");
|
||||
device("press", 'label="Open menu"', "--settle");
|
||||
device("wait", 'label="Close sidebar"', "5000");
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
|
||||
import { startIsolatedHostDaemon } from "../../support/helpers/isolated-host-daemon";
|
||||
import { connectDaemonClient } from "../../support/helpers/daemon-client-loader";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import net from "node:net";
|
||||
|
||||
interface FixtureClient {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
fetchWorkspaces(options: {
|
||||
page: { limit: number };
|
||||
subscribe: Record<string, never>;
|
||||
}): Promise<{ entries: unknown[] }>;
|
||||
subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.env.E2E_METRO_PORT = "8098";
|
||||
const root = path.resolve(".dev/workspace-update-freeze");
|
||||
const cwd = path.join(root, "repo");
|
||||
const home = path.join(root, "home");
|
||||
await mkdir(cwd, { recursive: true });
|
||||
await mkdir(home + "/projects", { recursive: true });
|
||||
for (const args of [
|
||||
["init", "-b", "main"],
|
||||
["config", "user.name", "Repro"],
|
||||
["config", "user.email", "repro@example.com"],
|
||||
])
|
||||
execFileSync("git", args, { cwd });
|
||||
await writeFile(cwd + "/README.md", "initial\n");
|
||||
execFileSync("git", ["add", "README.md"], { cwd });
|
||||
execFileSync("git", ["commit", "--allow-empty", "-m", "initial"], { cwd });
|
||||
const date = "2026-03-01T00:00:00.000Z";
|
||||
const project = {
|
||||
projectId: "prj_diag515a",
|
||||
rootPath: cwd,
|
||||
kind: "git",
|
||||
displayName: "Android reproduction",
|
||||
projectKey: null,
|
||||
customName: null,
|
||||
customIconRevision: null,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
archivedAt: null,
|
||||
};
|
||||
const workspaces = Array.from({ length: 400 }, (_, i) => ({
|
||||
workspaceId: `wks_diag515a_${i}`,
|
||||
projectId: project.projectId,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
displayName: `Workspace ${String(i).padStart(3, "0")}`,
|
||||
title: null,
|
||||
branch: null,
|
||||
worktreeRoot: null,
|
||||
baseBranch: "main",
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: cwd,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: null,
|
||||
pinnedAt: null,
|
||||
}));
|
||||
await writeFile(home + "/projects/projects.json", JSON.stringify([project]));
|
||||
await writeFile(home + "/projects/workspaces.json", JSON.stringify(workspaces));
|
||||
const daemon = await startIsolatedHostDaemon("diag515a", { paseoHome: home, preserveHome: true });
|
||||
const client = await connectDaemonClient<FixtureClient>({
|
||||
clientIdPrefix: "diag515a",
|
||||
port: daemon.port,
|
||||
});
|
||||
const events: { at: number; payload: unknown }[] = [];
|
||||
client.subscribeRawMessages((m) => {
|
||||
if (m.type === "workspace_update") events.push({ at: Date.now(), payload: m.payload });
|
||||
});
|
||||
const initial = await client.fetchWorkspaces({ page: { limit: 200 }, subscribe: {} });
|
||||
console.log("INITIAL", initial.entries.length);
|
||||
const proxy = net.createServer((socket) => {
|
||||
const up = net.connect(daemon.port, "127.0.0.1");
|
||||
socket.pipe(up);
|
||||
up.pipe(socket);
|
||||
socket.on("error", () => up.destroy());
|
||||
up.on("error", () => socket.destroy());
|
||||
});
|
||||
await new Promise<void>((resolve) => proxy.listen(45151, "127.0.0.1", resolve));
|
||||
await writeFile(
|
||||
root + "/ready.json",
|
||||
JSON.stringify({ port: daemon.port, cwd, pid: daemon.getPid() }),
|
||||
);
|
||||
console.log("READY", daemon.port);
|
||||
const timer = setInterval(async () => {
|
||||
await writeFile(root + "/events.json", JSON.stringify(events));
|
||||
}, 1000);
|
||||
process.on("SIGTERM", async () => {
|
||||
clearInterval(timer);
|
||||
proxy.close();
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
process.exit(0);
|
||||
});
|
||||
await new Promise(() => {});
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { SeedDaemonClient } from "../../support/helpers/seed-client";
|
||||
import { connectDaemonClient } from "../../support/helpers/daemon-client-loader";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
async function main() {
|
||||
const { port, cwd } = JSON.parse(
|
||||
await readFile(".dev/workspace-update-freeze/ready.json", "utf8"),
|
||||
);
|
||||
const client = await connectDaemonClient<SeedDaemonClient>({
|
||||
clientIdPrefix: "stream515a",
|
||||
port,
|
||||
});
|
||||
try {
|
||||
const agent = await client.createAgent({
|
||||
provider: "mock",
|
||||
cwd,
|
||||
workspaceId: "wks_diag515a_0",
|
||||
title: "Android stream reproduction",
|
||||
modeId: "load-test",
|
||||
model: "five-minute-stream",
|
||||
});
|
||||
await writeFile(".dev/workspace-update-freeze/agent.json", JSON.stringify(agent));
|
||||
for (let i = 0; i < 110; i++) {
|
||||
await client.sendAgentMessage(
|
||||
agent.id,
|
||||
`history ${i}: emit 1 coalesced agent stream updates`,
|
||||
);
|
||||
await client.waitForFinish(agent.id, 15000);
|
||||
}
|
||||
console.log("HISTORY_READY", agent.id);
|
||||
await writeFile(".dev/workspace-update-freeze/history-ready", agent.id);
|
||||
await client.sendAgentMessage(agent.id, "Stream a realistic long answer with code blocks.");
|
||||
await client.waitForFinish(agent.id, 360000);
|
||||
console.log("FINISHED");
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(".dev/workspace-update-freeze");
|
||||
const { cwd } = JSON.parse(await readFile(path.join(root, "ready.json"), "utf8"));
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
await writeFile(path.join(cwd, "README.md"), index % 2 ? "changed first\n" : "initial\n");
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
Reference in New Issue
Block a user