mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 18:35:17 +00:00
Compare 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