Skip to content

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

@BomB1961

Description

@BomB1961

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 단위 응답이 가능합니다.

이슈 검토 부탁드립니다 🙏

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