Skip to content

에디터 unfocused 시 dispatch tick이 ~3초로 떨어져 짧은 timeout 호출이 실패함 #28

Description

unity-cli connector — 에디터 unfocused 시 요청 처리 지연 이슈

저장소: youngwoocho02/unity-cli
버전: com.youngwoocho02.unity-cli-connector v0.3.15 (unity-connector path)
보고일: 2026-04-30


1. 한 줄 요약

Unity 에디터가 OS 포커스를 잃으면 HTTP 요청 처리 tick 간격이 약 16 ms → 약 3,000 ms 로 떨어져, 짧은 timeout 으로 호출하는 클라이언트가 자주 실패합니다. 포트는 정상 listening, listener 스레드도 살아있지만 dispatch 가 EditorApplication.update 에만 의존하기 때문입니다.


2. 환경

항목
Unity Editor 6000.4.3f1 (URP, HDRP 동시 설치)
Plugin com.youngwoocho02.unity-cli-connector v0.3.15 (Git URL 의존성)
OS Windows 10 Pro 19045
호출 클라이언트 PowerShell 5.1 Invoke-RestMethod (Go HTTP client 도 동일 증상으로 추정)
동시 실행 인스턴스 Unity 에디터 3개 (포트 8090 / 8091 / 8092 자동 할당)

3. 사용자가 관찰한 문제

"시간이 지나면 unity-cli connector 연결을 찾지 못하는건지 실행을 못한다."

  • 호출 직후에는 응답이 잘 오는데, 잠시 다른 창으로 작업하다 돌아오면 같은 호출이 timeout.
  • 재시도하면 또 한참 뒤에 응답 오거나 연속 timeout.
  • 에디터를 클릭해 포커스를 줘 두면 한동안 정상.
  • Claude Code 의 PreToolUse 훅(check-unity-port.ps1, dataPath 검증) 도 위와 같이 connector 가 응답을 안 줄 때 dataPath 매칭을 건너뛰고 fail-open 됨 → 검증 누락.

4. 재현 절차

  1. Unity 6000.4.3f1 에디터 1개 실행 → 콘솔에 [UnityCliConnector] HTTP server started on port 8090 확인.

  2. 에디터 창을 다른 창 뒤로 보내거나 minimize → OS 포커스 상실.

  3. 외부에서 다음 호출을 timeout 2~3 초로 실행:

    $body = '{"command":"exec","params":{"code":"return UnityEngine.Application.dataPath;"}}'
    Invoke-RestMethod -Uri "http://127.0.0.1:8090/command" `
      -Method Post -ContentType "application/json" `
      -Body $body -TimeoutSec 3

    The operation has timed out.

  4. 같은 호출을 -TimeoutSec 30 으로 재시도 → 성공.


5. 측정 결과

5번 연속 호출하여 응답에 담긴 Time.realtimeSinceStartup 의 차이를 측정 (에디터 unfocused 상태):

Call 응답 timestamp (s) Δ (직전 호출 대비)
1 478.140
2 481.170 +3.030 s
3 484.230 +3.060 s
4 487.270 +3.040 s
5 490.490 +3.220 s

EditorApplication.update 가 평균 3 초마다 1회만 호출됨 (≈ 0.3 Hz).
포커스가 있을 때는 같은 측정이 ms 단위(≈ 60 Hz) 로 나옵니다.

동시에 조회한 에디터 상태 (8090, exec 으로 직접 질의)

isApplicationActive   = False        ← OS 포커스 상실
EditorApplication.isFocused = False
isPlaying / isPaused  = False
isCompiling           = False        ← 도메인 리로드 무관
isUpdating            = False        ← Asset Import 무관
interactionMode       = 1 (NoThrottling)   ← 이미 최적값
runInBackground       = False        ← Player 설정, 에디터엔 무영향

컴파일·Asset Import·Throttling 설정 모두 정상인데도 unfocused 만으로 tick 이 떨어집니다.


6. 코드 분석 — Editor/HttpServer.cs

// L47
EditorApplication.update += ProcessQueue;

// L120-124
static void ProcessQueue()
{
    while (s_Queue.TryDequeue(out var item))
        ProcessItem(item);
}

// L210-218 (HandleRequest 내부)
var tcs = new TaskCompletionSource<object>();
s_Queue.Enqueue(new WorkItem { Command = command, Parameters = parameters, Tcs = tcs });
ForceEditorUpdate();              // = InternalEditorUtility.RepaintAllViews()
result = await tcs.Task;          // ← main thread 가 tick 할 때까지 대기

핵심 흐름:

  1. HTTP listener (ListenLoop) 는 백그라운드 스레드 — 포트는 항상 listening, OS focus 와 무관.
  2. 요청 도착 시 ConcurrentQueue 에 enqueue → main thread 의 EditorApplication.update 콜백에서 dequeue 후 dispatch.
  3. ForceEditorUpdate()RepaintAllViews() 를 호출해 update 를 깨우려 시도하지만, 에디터 창이 모두 unfocused / occluded 인 상황에선 OS 가 paint message 를 거의 보내지 않아 효과가 없음.
  4. → 결과적으로 응답은 다음 update tick (≈ 3 s 후) 까지 대기.

7. 근본 원인

Windows 가 background process 의 timer message resolution 을 강하게 throttle 하고, Unity 에디터의 main loop tick 도 이와 함께 늦춰집니다. Edit > Preferences → General → Interaction Mode = No Throttling 으로 두어도 background 상태에서는 ~3 s 간격이 관찰됩니다 (위 측정 결과).

Connector 의 dispatch 가 main-thread tick 에만 결합되어 있기 때문에, 이 OS-레벨 throttling 이 그대로 응답 latency 로 노출됩니다.

훅(Claude Code PreToolUse) 자체는 정상 동작이며 본 이슈와 무관 — connector 가 응답을 안 줄 때 fail-open 으로 통과하므로 timeout 을 만들지 않음.


8. 제안 (수정 후보)

다음 중 하나 또는 조합:

A. delayCall 병행 등록 (가장 작은 변경)

EditorApplication.update    += ProcessQueue;
EditorApplication.delayCall += ProcessQueue;  // 큐에 들어오자마자 즉시 한 번 더 dispatch 시도

delayCall 은 다음 editor message pump 시 한 번 호출되며, 호출 후 자동 해제되므로 enqueue 시마다 다시 등록해야 합니다.

B. enqueue 직후 main-thread tick 강제

ForceEditorUpdate() 를 다음으로 교체 또는 추가:

static void ForceEditorUpdate()
{
    try { EditorApplication.QueuePlayerLoopUpdate(); } catch { }
    try { UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } catch { }
}

QueuePlayerLoopUpdate() 는 다음 frame 의 PlayerLoop 실행을 강제 예약하여 EditorApplication.update 호출 가능성을 높입니다. (단 background 에선 Windows timer 자체가 늦어 100% 보장은 안 됨.)

C. Background-safe 명령은 main-thread 우회

exec 의 C# 컴파일은 어차피 main thread 가 필요하지만, 상태 조회성 명령 (/version, /ping, dataPath 같은 read-only) 은 main thread 마샬링 없이 listener 스레드에서 직접 응답하면 unfocused 시에도 ms 단위로 응답 가능. → dataPath 검증 훅 같은 정적 query 케이스의 timeout 가능성을 0 으로 만듦.

D. 별도 timer 로 main-thread tick 펌프

System.Threading.Timer 를 listener 활성 동안 가동하여 50–100 ms 간격으로 EditorApplication.QueuePlayerLoopUpdate() 를 호출. 단 idle 시 CPU 비용 증가 → 큐가 비어있을 때 일시 정지하는 분기 필요.


9. 클라이언트측 회피책 (참고)

수정 전까지 사용 가능한 우회:

  • timeout 을 10 초 이상으로 설정 (-TimeoutSec 30 권장).
  • 작업 중 Unity 에디터 창을 다른 모니터에 보이게 두기 (포커스 없어도 visible 이면 paint 가 발생해 tick 빨라짐).
  • 동시 실행 에디터 인스턴스 최소화.

10. 첨부 가능한 추가 자료

요청 시 제공 가능:

  • 8090 / 8091 / 8092 인스턴스의 Editor.log 발췌 (connector start/stop 라인 정상)
  • focused 상태에서의 동일 측정 (Δ 가 ms 단위로 떨어지는지 비교)
  • [InitializeOnLoad] 도메인 리로드 시 stop/start 동작 로그

11. 결론

이슈의 본질은 dispatch 가 EditorApplication.update 단일 경로에만 묶여 있다는 점입니다. Listener 와 큐는 정상 동작하므로 dispatch 트리거를 강화하면 (제안 A + C 권장) unfocused 환경에서도 안정적으로 ms 단위 응답이 가능합니다.

이슈 검토 부탁드립니다 🙏

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions