diff --git a/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go b/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go index 806c6d3e29..4a9129b410 100644 --- a/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go +++ b/cli/project-runner/internal/projectrunner/compile_fresh_recovery_test.go @@ -80,7 +80,6 @@ func (scenario *compileRecoveryScenario) deps() compileWaitDeps { deps := compileWaitTestDeps(scenario.query) deps.sendCompile = scenario.send deps.freshWaitPollInterval = time.Millisecond - deps.startStallFocusThreshold = time.Hour return deps } diff --git a/cli/project-runner/internal/projectrunner/compile_wait.go b/cli/project-runner/internal/projectrunner/compile_wait.go index 5a587909c4..5e1d49e91c 100644 --- a/cli/project-runner/internal/projectrunner/compile_wait.go +++ b/cli/project-runner/internal/projectrunner/compile_wait.go @@ -38,10 +38,6 @@ const ( compileWaitPollInterval = clicore.ToolReadinessPoll compileStatusProbeTimeout = clicore.ToolReadinessProbeTimeout compileResponseTimeout = 2 * time.Second - // Why 10s: AutoTickPump keeps Unity ticking while idle, but cannot lift OS - // background throttling. After this long with no compile activity, focusing - // the Editor is the only recovery that has been observed to work. - compileStartStallFocusThreshold = 10 * time.Second ) type compileCompletionOptions struct { @@ -193,19 +189,8 @@ func waitForCompileCompletionWithDeps( attempts := 0 var lastStatus compileStatusResponse observedStatus := false - activityObserved := false var lastErr error lastObservationKey := "" - focusController := newConnectionRetryFocusController( - options.connection, - clicore.CompileCommandName, - compileWaitFocusDeps(deps), - ) - defer func() { - restoreContext, cancel := context.WithTimeout(context.Background(), focusRestoreTimeout) - defer cancel() - focusController.restore(restoreContext) - }() logCompileStatusPollStart(options, startedAt, deadline) interim := newCompileWaitInterimState(compileWaitNow(deps)) @@ -222,7 +207,6 @@ func waitForCompileCompletionWithDeps( attempts++ status, err := deps.queryCompileStatus(ctx, options.connection, options.requestID) lastErr = err - activityObserved = noteCompileActivityStarted(status, err, activityObserved) if err == nil && status.Ready && status.HasResult && len(status.Result) > 0 { logCompileStatusPollObservedIfChanged(options, startedAt, attempts, status, nil, &lastObservationKey) logCompileStatusPollComplete(options, startedAt, attempts, status) @@ -238,11 +222,6 @@ func waitForCompileCompletionWithDeps( } logCompileStatusPollObservedIfChanged(options, startedAt, attempts, status, err, &lastObservationKey) observeCompileWaitInterim(&interim, deps, status, err) - // Why: queryCompileStatus can return after the wait deadline. Focusing then - // would steal window order for a wait that has already timed out. - if time.Now().Before(deadline) { - maybeAttemptCompileStartStallFocus(ctx, startedAt, activityObserved, lastErr, focusController, deps) - } select { case <-ctx.Done(): diff --git a/cli/project-runner/internal/projectrunner/compile_wait_deps.go b/cli/project-runner/internal/projectrunner/compile_wait_deps.go index b63f259aa6..959b6807a3 100644 --- a/cli/project-runner/internal/projectrunner/compile_wait_deps.go +++ b/cli/project-runner/internal/projectrunner/compile_wait_deps.go @@ -25,19 +25,9 @@ type compileWaitDeps struct { now func() time.Time interimReportInterval time.Duration reportInterim compileWaitInterimReporter - // Zero keeps compileStartStallFocusThreshold. Tests shorten it so they do not wait 10s. - startStallFocusThreshold time.Duration // Zero keeps compileWaitPollInterval for a fresh compile's status wait. Tests shorten it so // they do not wait 1s between status queries. freshWaitPollInterval time.Duration - focus connectionRetryDeps -} - -func compileStartStallFocusThresholdFor(deps compileWaitDeps) time.Duration { - if deps.startStallFocusThreshold > 0 { - return deps.startStallFocusThreshold - } - return compileStartStallFocusThreshold } func freshWaitPollIntervalFor(deps compileWaitDeps) time.Duration { @@ -47,17 +37,6 @@ func freshWaitPollIntervalFor(deps compileWaitDeps) time.Duration { return compileWaitPollInterval } -func compileWaitFocusDeps(deps compileWaitDeps) connectionRetryDeps { - merged := defaultConnectionRetryDeps() - if deps.focus.findRunningUnityProcess != nil { - merged.findRunningUnityProcess = deps.focus.findRunningUnityProcess - } - if deps.focus.focusUnityProcess != nil { - merged.focusUnityProcess = deps.focus.focusUnityProcess - } - return merged -} - func compileSendOrDefault(deps compileWaitDeps) compileSendFunc { if deps.sendCompile != nil { return deps.sendCompile diff --git a/cli/project-runner/internal/projectrunner/compile_wait_focus.go b/cli/project-runner/internal/projectrunner/compile_wait_focus.go deleted file mode 100644 index 584d3ed427..0000000000 --- a/cli/project-runner/internal/projectrunner/compile_wait_focus.go +++ /dev/null @@ -1,43 +0,0 @@ -package projectrunner - -import ( - "context" - "errors" - "time" -) - -var errCompileStartStallNoActivity = errors.New("compile status has not shown activity") - -func compileActivityHasStarted(status compileStatusResponse) bool { - return status.IsCompiling || status.IsUpdating || status.IsDomainReloadInProgress || status.HasResult -} - -func noteCompileActivityStarted(status compileStatusResponse, err error, activityObserved bool) bool { - if err != nil || activityObserved { - return activityObserved - } - return compileActivityHasStarted(status) -} - -func maybeAttemptCompileStartStallFocus( - ctx context.Context, - startedAt time.Time, - activityObserved bool, - lastErr error, - controller *connectionRetryFocusController, - deps compileWaitDeps, -) { - // Why: domain-reload silence after compile has started is normal. Focusing then - // would steal window order from the user for a healthy wait. - if activityObserved { - return - } - if time.Since(startedAt) < compileStartStallFocusThresholdFor(deps) { - return - } - cause := lastErr - if cause == nil { - cause = errCompileStartStallNoActivity - } - controller.tryFocus(ctx, focusReasonCompileStartStall, cause) -} diff --git a/cli/project-runner/internal/projectrunner/compile_wait_test.go b/cli/project-runner/internal/projectrunner/compile_wait_test.go index 0adb3132f3..1c146001ee 100644 --- a/cli/project-runner/internal/projectrunner/compile_wait_test.go +++ b/cli/project-runner/internal/projectrunner/compile_wait_test.go @@ -943,190 +943,6 @@ func TestWaitForCompileCompletionReturnsLastStatusOnTimeout(t *testing.T) { } } -// Verifies all-idle compile status past the start-stall threshold focuses Unity once -// with reason compile_start_stall. -func TestWaitForCompileCompletionFocusesOnceWhenStatusStaysIdle(t *testing.T) { - enableCliVibeLog(t) - connection := compileWaitTestConnection(t) - deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { - return compileStatusResponse{}, nil - }) - probe := attachCompileWaitFocusProbe(&deps) - - _, completed, _, err := waitForCompileCompletionWithDeps(context.Background(), compileCompletionOptions{ - connection: connection, - requestID: "compile_start_stall_idle", - timeout: 200 * time.Millisecond, - pollInterval: 5 * time.Millisecond, - }, deps) - if err != nil { - t.Fatalf("waitForCompileCompletion failed: %v", err) - } - if completed { - t.Fatal("idle compile wait should time out") - } - if probe.focusCount != 1 { - t.Fatalf("focus attempts mismatch: got %d want 1", probe.focusCount) - } - - logContent := readOnlyCliVibeLog(t, connection.ProjectRoot) - attemptEntries := cliVibeEntriesForOperation(t, logContent, "cli_connection_retry_focus_attempt") - if len(attemptEntries) != 1 { - t.Fatalf("expected exactly 1 focus attempt log, got %d:\n%s", len(attemptEntries), logContent) - } - reason := vibeLogContextString(t, attemptEntries[0], "reason") - expectedReason := "compile_start_stall" - if reason != expectedReason { - t.Fatalf("focus reason mismatch: got %q want %q", reason, expectedReason) - } -} - -// Verifies observing IsCompiling before the threshold suppresses focus even when later polls fail. -func TestWaitForCompileCompletionDoesNotFocusAfterActivityStarted(t *testing.T) { - connection := compileWaitTestConnection(t) - callCount := 0 - deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { - callCount++ - if callCount == 1 { - return compileStatusResponse{IsCompiling: true}, nil - } - return compileStatusResponse{}, fmt.Errorf("status poll failed") - }) - probe := attachCompileWaitFocusProbe(&deps) - - _, completed, _, err := waitForCompileCompletionWithDeps(context.Background(), compileCompletionOptions{ - connection: connection, - requestID: "compile_start_stall_activity", - timeout: 200 * time.Millisecond, - pollInterval: 5 * time.Millisecond, - }, deps) - if err != nil { - t.Fatalf("waitForCompileCompletion failed: %v", err) - } - if completed { - t.Fatal("compile wait should time out after activity then silence") - } - if probe.focusCount != 0 { - t.Fatalf("focus attempts mismatch: got %d want 0", probe.focusCount) - } -} - -// Verifies probe errors alone past the threshold still focus Unity once. -func TestWaitForCompileCompletionFocusesOnceWhenProbesKeepFailing(t *testing.T) { - connection := compileWaitTestConnection(t) - deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { - return compileStatusResponse{}, fmt.Errorf("status probe timeout") - }) - probe := attachCompileWaitFocusProbe(&deps) - - _, completed, _, err := waitForCompileCompletionWithDeps(context.Background(), compileCompletionOptions{ - connection: connection, - requestID: "compile_start_stall_probe_error", - timeout: 200 * time.Millisecond, - pollInterval: 5 * time.Millisecond, - }, deps) - if err != nil { - t.Fatalf("waitForCompileCompletion failed: %v", err) - } - if completed { - t.Fatal("probe-error compile wait should time out") - } - if probe.focusCount != 1 { - t.Fatalf("focus attempts mismatch: got %d want 1", probe.focusCount) - } -} - -// Verifies a compile wait that focused Unity restores the previous front window on completion. -func TestWaitForCompileCompletionRestoresFocusOnCompletion(t *testing.T) { - connection := compileWaitTestConnection(t) - callCount := 0 - deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { - callCount++ - if callCount < 8 { - return compileStatusResponse{}, nil - } - return compileStatusResponse{ - Ready: true, - HasResult: true, - Result: json.RawMessage(`{"Success":true}`), - }, nil - }) - probe := attachCompileWaitFocusProbe(&deps) - - result, completed, _, err := waitForCompileCompletionWithDeps(context.Background(), compileCompletionOptions{ - connection: connection, - requestID: "compile_start_stall_restore", - timeout: 200 * time.Millisecond, - pollInterval: 5 * time.Millisecond, - }, deps) - if err != nil { - t.Fatalf("waitForCompileCompletion failed: %v", err) - } - if !completed { - t.Fatal("compile wait should complete after the stall") - } - if string(result) != `{"Success":true}` { - t.Fatalf("result mismatch: %s", result) - } - if probe.focusCount != 1 { - t.Fatalf("focus attempts mismatch: got %d want 1", probe.focusCount) - } - if probe.restoreCount != 1 { - t.Fatalf("restore calls mismatch: got %d want 1", probe.restoreCount) - } -} - -// Verifies a status probe that returns after the wait deadline does not focus Unity. -func TestWaitForCompileCompletionDoesNotFocusAfterDeadline(t *testing.T) { - connection := compileWaitTestConnection(t) - deps := compileWaitTestDeps(func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { - time.Sleep(250 * time.Millisecond) - return compileStatusResponse{}, nil - }) - probe := attachCompileWaitFocusProbe(&deps) - - _, completed, _, err := waitForCompileCompletionWithDeps(context.Background(), compileCompletionOptions{ - connection: connection, - requestID: "compile_start_stall_after_deadline", - timeout: 200 * time.Millisecond, - pollInterval: 5 * time.Millisecond, - }, deps) - if err != nil { - t.Fatalf("waitForCompileCompletion failed: %v", err) - } - if completed { - t.Fatal("deadline-crossing probe should time out") - } - if probe.focusCount != 0 { - t.Fatalf("focus attempts mismatch: got %d want 0", probe.focusCount) - } -} - -// Verifies compile activity is any of IsCompiling, IsUpdating, domain reload, or HasResult, -// and that Ready alone does not count. -func TestCompileActivityHasStarted(t *testing.T) { - cases := []struct { - name string - status compileStatusResponse - want bool - }{ - {name: "all false", status: compileStatusResponse{}, want: false}, - {name: "IsCompiling", status: compileStatusResponse{IsCompiling: true}, want: true}, - {name: "IsUpdating", status: compileStatusResponse{IsUpdating: true}, want: true}, - {name: "IsDomainReloadInProgress", status: compileStatusResponse{IsDomainReloadInProgress: true}, want: true}, - {name: "HasResult", status: compileStatusResponse{HasResult: true}, want: true}, - {name: "Ready alone is not activity", status: compileStatusResponse{Ready: true}, want: false}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := compileActivityHasStarted(tc.status) - if got != tc.want { - t.Fatalf("compileActivityHasStarted mismatch: got %v want %v status=%#v", got, tc.want, tc.status) - } - }) - } -} - // assertCompileWaitTimeoutEnvelopeDetails checks the run/attach wiring that passes // lastStatus and WaitedMs into the COMPILE_WAIT_TIMEOUT stderr envelope. func assertCompileWaitTimeoutEnvelopeDetails(t *testing.T, stderr []byte, wantCompiling bool) { @@ -1180,28 +996,6 @@ func compileWaitTestDeps( } } -type compileWaitFocusProbe struct { - focusCount int - restoreCount int -} - -func attachCompileWaitFocusProbe(deps *compileWaitDeps) *compileWaitFocusProbe { - probe := &compileWaitFocusProbe{} - deps.startStallFocusThreshold = 20 * time.Millisecond - deps.focus = defaultConnectionRetryDeps() - deps.focus.findRunningUnityProcess = func(context.Context, string) (*clicore.UnityProcess, error) { - return &clicore.UnityProcess{Pid: 4242}, nil - } - deps.focus.focusUnityProcess = func(context.Context, int) (clicore.RestoreFocusFunc, error) { - probe.focusCount++ - return func(context.Context) error { - probe.restoreCount++ - return nil - }, nil - } - return probe -} - func vibeLogContextString(t *testing.T, entry map[string]any, key string) string { t.Helper() contextMap, ok := entry["context"].(map[string]any) diff --git a/cli/project-runner/internal/projectrunner/connection_retry.go b/cli/project-runner/internal/projectrunner/connection_retry.go index 494c68f808..4d5929e6e7 100644 --- a/cli/project-runner/internal/projectrunner/connection_retry.go +++ b/cli/project-runner/internal/projectrunner/connection_retry.go @@ -52,7 +52,6 @@ const ( focusReasonHeartbeatSilenceTimeout connectionRetryFocusReason = "heartbeat_silence_timeout" focusReasonFinalResponseTimeout connectionRetryFocusReason = "final_response_timeout" focusReasonBusyStall connectionRetryFocusReason = "busy_stall" - focusReasonCompileStartStall connectionRetryFocusReason = "compile_start_stall" ) // Why: domain reload on large projects can keep the IPC endpoint down well past the diff --git a/docs/adr/0012-hold-a-macos-activity-while-a-command-runs.md b/docs/adr/0012-hold-a-macos-activity-while-a-command-runs.md index 65c3fc4622..1817d2a0bb 100644 --- a/docs/adr/0012-hold-a-macos-activity-while-a-command-runs.md +++ b/docs/adr/0012-hold-a-macos-activity-while-a-command-runs.md @@ -68,6 +68,7 @@ retains it before its own autorelease pool drains and releases it after `endActi - Windows and Linux have not been investigated; they hold nothing. - A Begin and End pair costs about 4 µs inside the Editor (median of 100 pairs; the slowest took 0.11 ms). +- `uloop compile` no longer brings the Editor to the front when compilation has not started within ten seconds. That rescue (#2341) existed for this suppression; with the activity held, a compile that has not started is a compile the Editor has not reached yet, and bringing it to the front only moves the user's windows. ## Reversal condition