дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(debugger): share breakpoint flushes across concurrent starts (#2171)
Starting several debug sessions with preset breakpoints could hang before any adapter started: each start ran its own flush and kept adding sends to the others' queue. Concurrent starts now share one drain, which includes edits made during a send, and a failed drain can be retried. Exception, function and data breakpoints use finite sends and do not have this loop.
Цей коміт міститься в:
1 джерело
cad33ef119
коміт
6d8e41c2c1
2 files changed
+63
-2
No files matched your search
@@ -1,5 +1,11 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { addBreakpoint, loadBreakpoints, removeAllBreakpoints, useBreakpoints } from "../breakpoints/store";
|
||||
import {
|
||||
addBreakpoint,
|
||||
flushBreakpoints,
|
||||
loadBreakpoints,
|
||||
removeAllBreakpoints,
|
||||
useBreakpoints,
|
||||
} from "../breakpoints/store";
|
||||
import { handleDebugEvent, startPreparedSession, startSession, useDebug } from "../state";
|
||||
import type { LaunchConfig } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
@@ -167,3 +173,48 @@ it("does not start if the initial breakpoint sync fails", async () => {
|
||||
expect(await startSession(config("launch"))).toBeNull();
|
||||
expect(ipc.debugStart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["launch", "attach"])("shares the breakpoint drain between concurrent %s starts", async (request) => {
|
||||
let release!: () => void;
|
||||
ipc.debugSetBreakpoints.mockImplementation(() => {
|
||||
// Bound the broken loop so a regression fails instead of hanging the test runner.
|
||||
if (ipc.debugSetBreakpoints.mock.calls.length > 8) throw new Error("breakpoint flush did not settle");
|
||||
if (ipc.debugSetBreakpoints.mock.calls.length === 1)
|
||||
return new Promise((resolve) => {
|
||||
release = () => resolve({ status: "ok", data: [] });
|
||||
});
|
||||
return Promise.resolve({ status: "ok", data: [] });
|
||||
});
|
||||
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 42 }).mockResolvedValueOnce({ status: "ok", data: 43 });
|
||||
addBreakpoint("/p/Main.java", 4);
|
||||
const starting = Promise.all([startSession(config(request)), startSession(config(request))]);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(ipc.debugStart).not.toHaveBeenCalled();
|
||||
addBreakpoint("/p/Main.java", 7);
|
||||
release();
|
||||
expect(await starting).toEqual([42, 43]);
|
||||
expect(ipc.debugSetBreakpoints).toHaveBeenCalledTimes(2);
|
||||
expect(ipc.debugSetBreakpoints.mock.calls[1]![1]).toMatchObject([{ line: 4 }, { line: 7 }]);
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
expect(ipc.debugSetBreakpoints).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not resend unchanged breakpoints for concurrent flushes", async () => {
|
||||
ipc.debugSetBreakpoints.mockImplementation(async () => {
|
||||
if (ipc.debugSetBreakpoints.mock.calls.length > 8) throw new Error("breakpoint flush did not settle");
|
||||
return { status: "ok", data: [] };
|
||||
});
|
||||
addBreakpoint("/p/Main.java", 4);
|
||||
await Promise.all([flushBreakpoints(), flushBreakpoints(), flushBreakpoints()]);
|
||||
expect(ipc.debugSetBreakpoints).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("releases a failed shared drain so a later start can retry", async () => {
|
||||
ipc.debugSetBreakpoints.mockRejectedValueOnce(new Error("breakpoint sync failed"));
|
||||
addBreakpoint("/p/Main.java", 4);
|
||||
expect(await Promise.all([startSession(config("launch")), startSession(config("attach"))])).toEqual([null, null]);
|
||||
expect(ipc.debugStart).not.toHaveBeenCalled();
|
||||
ipc.debugSetBreakpoints.mockResolvedValue({ status: "ok", data: [] });
|
||||
expect(await startSession(config("launch"))).toBe(42);
|
||||
expect(ipc.debugStart).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -96,6 +96,7 @@ const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
/** Ids in the order last sent per file (backend states are index-aligned). */
|
||||
const sent = new Map<string, string[]>();
|
||||
const inFlight = new Map<string, Promise<void>>();
|
||||
let flushing: Promise<void> | null = null;
|
||||
const SYNC_DELAY = 120;
|
||||
|
||||
export function scheduleSync(path: string) {
|
||||
@@ -110,7 +111,16 @@ export function scheduleSync(path: string) {
|
||||
}
|
||||
|
||||
/** Drain debounced edits before the backend takes its startup breakpoint snapshot. */
|
||||
export async function flushBreakpoints() {
|
||||
export function flushBreakpoints(): Promise<void> {
|
||||
// Concurrent session starts must not keep adding work to each other's drain.
|
||||
if (flushing) return flushing;
|
||||
flushing = drainBreakpoints().finally(() => {
|
||||
flushing = null;
|
||||
});
|
||||
return flushing;
|
||||
}
|
||||
|
||||
async function drainBreakpoints() {
|
||||
let paths = new Set([...timers.keys(), ...inFlight.keys(), ...Object.keys(useBreakpoints.getState().byFile)]);
|
||||
while (paths.size) {
|
||||
for (const timer of timers.values()) clearTimeout(timer);
|
||||
|
||||
Reference in new issue
Заблокувати користувача